├── .gitignore ├── configure.ajaxterm.bin ├── ajaxterm.bin ├── qweb ├── __init__.py ├── static.py ├── fcgi.py └── qweb.py ├── configure.makefile ├── configure.initd.gentoo ├── Makefile ├── ajaxterm.html ├── ajaxterm.initd ├── configure.initd.debian ├── ajaxterm.1 ├── configure ├── configure.initd.redhat ├── ajaxterm.css ├── sarissa_dhtml.js ├── README.txt ├── ajaxterm.js ├── ajaxterm.py ├── ajaxenterm.py └── sarissa.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /configure.ajaxterm.bin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PYTHONPATH=%(lib)s exec %(lib)s/ajaxterm.py $@ 3 | -------------------------------------------------------------------------------- /ajaxterm.bin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PYTHONPATH=/usr/local/share/ajaxterm exec /usr/local/share/ajaxterm/ajaxterm.py $@ 3 | -------------------------------------------------------------------------------- /qweb/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.3 2 | # vim:set noet ts=4 foldlevel=0: 3 | from qweb import * 4 | import static 5 | -------------------------------------------------------------------------------- /configure.makefile: -------------------------------------------------------------------------------- 1 | build: 2 | true 3 | 4 | install: 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 | 15 | clean: 16 | rm ajaxterm.bin 17 | rm ajaxterm.initd 18 | rm ajaxterm.1.gz 19 | rm Makefile 20 | 21 | -------------------------------------------------------------------------------- /configure.initd.gentoo: -------------------------------------------------------------------------------- 1 | #!/sbin/runscript 2 | 3 | # AjaxTerm Gentoo script, 08 May 2006 Mark Gillespie 4 | 5 | DAEMON=%(bin)s/ajaxterm 6 | PORT=%(port)s 7 | PIDFILE=/var/run/ajaxterm.pid 8 | 9 | depend() 10 | { 11 | need net 12 | } 13 | 14 | start() 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 | 21 | stop() 22 | { 23 | ebegin "Stopping AjaxTerm" 24 | start-stop-daemon --stop --pidfile $PIDFILE 25 | rm -f $PIDFILE 26 | eend $? 27 | } 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | true 3 | 4 | install: 5 | install -d "/usr/local/bin" 6 | install -d "/usr/local/share/ajaxterm" 7 | install ajaxterm.bin "/usr/local/bin/ajaxterm" 8 | install ajaxterm.initd "/etc/init.d/ajaxterm" 9 | install -m 644 ajaxterm.css ajaxterm.html ajaxterm.js qweb.py sarissa.js sarissa_dhtml.js "/usr/local/share/ajaxterm" 10 | install -m 755 ajaxterm.py "/usr/local/share/ajaxterm" 11 | gzip --best -c ajaxterm.1 > ajaxterm.1.gz 12 | install -d "/usr/local/share/man/man1" 13 | install ajaxterm.1.gz "/usr/local/share/man/man1" 14 | 15 | clean: 16 | rm ajaxterm.bin 17 | rm ajaxterm.initd 18 | rm ajaxterm.1.gz 19 | rm Makefile 20 | 21 | -------------------------------------------------------------------------------- /ajaxterm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ajaxterm 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /ajaxterm.initd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin 4 | DAEMON=/usr/local/bin/ajaxterm 5 | PORT=8022 6 | PIDFILE=/var/run/ajaxterm.pid 7 | 8 | [ -x "$DAEMON" ] || exit 0 9 | 10 | #. /lib/lsb/init-functions 11 | 12 | case "$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 | ;; 31 | esac 32 | 33 | : 34 | -------------------------------------------------------------------------------- /configure.initd.debian: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin 4 | DAEMON=%(bin)s/ajaxterm 5 | PORT=%(port)s 6 | PIDFILE=/var/run/ajaxterm.pid 7 | 8 | [ -x "$DAEMON" ] || exit 0 9 | 10 | #. /lib/lsb/init-functions 11 | 12 | case "$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 | ;; 31 | esac 32 | 33 | : 34 | -------------------------------------------------------------------------------- /ajaxterm.1: -------------------------------------------------------------------------------- 1 | .TH ajaxterm "1" "Jul 2006" "ajaxterm 0.7" "User commands" 2 | .SH NAME 3 | ajaxterm \- Web based terminal written in python 4 | 5 | .SH DESCRITPION 6 | \fBajaxterm\fR is a web based terminal written in python and some AJAX 7 | javascript for client side. 8 | It can use almost any web browser and even works through firewalls. 9 | 10 | .SH USAGE 11 | \fBajaxterm\fR [options] 12 | 13 | .SH OPTIONS 14 | A 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 21 | Antony Lesuisse 22 | 23 | This manual page was written for the Debian system by 24 | Julien Valroff (but may be used by others). 25 | 26 | .SH "REPORTING BUGS" 27 | Report any bugs to the author: Antony Lesuisse 28 | 29 | .SH COPYRIGHT 30 | Copyright Antony Lesuisse 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 36 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import optparse,os 4 | 5 | parser = optparse.OptionParser() 6 | parser.add_option("", "--prefix", dest="prefix",default="/usr/local",help="installation prefix (default: /usr/local)") 7 | parser.add_option("", "--confdir", dest="confdir", default="/etc",help="configuration files directory prefix (default: /etc)") 8 | parser.add_option("", "--port", dest="port", default="8022", help="set the listening TCP port (default: 8022)") 9 | parser.add_option("", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh localhost)") 10 | (o, a) = parser.parse_args() 11 | 12 | print "Configuring prefix=",o.prefix," port=",o.port 13 | 14 | etc=o.confdir 15 | port=o.port 16 | cmd=o.cmd 17 | bin=os.path.join(o.prefix,"bin") 18 | lib=os.path.join(o.prefix,"share/ajaxterm") 19 | man=os.path.join(o.prefix,"share/man/man1") 20 | 21 | file("ajaxterm.bin","w").write(file("configure.ajaxterm.bin").read()%locals()) 22 | file("Makefile","w").write(file("configure.makefile").read()%locals()) 23 | 24 | if os.path.isfile("/etc/gentoo-release"): 25 | file("ajaxterm.initd","w").write(file("configure.initd.gentoo").read()%locals()) 26 | elif 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()) 28 | else: 29 | file("ajaxterm.initd","w").write(file("configure.initd.debian").read()%locals()) 30 | 31 | os.system("chmod a+x ajaxterm.bin") 32 | os.system("chmod a+x ajaxterm.initd") 33 | -------------------------------------------------------------------------------- /configure.initd.redhat: -------------------------------------------------------------------------------- 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 | 13 | if [ -f /etc/sysconfig/ajaxterm ]; then 14 | . /etc/sysconfig/ajaxterm 15 | fi 16 | 17 | ajaxterm=/usr/local/bin/ajaxterm 18 | prog=ajaxterm 19 | pidfile=${PIDFILE-/var/run/ajaxterm.pid} 20 | lockfile=${LOCKFILE-/var/lock/subsys/ajaxterm} 21 | port=${PORT-8022} 22 | user=${xUSER-nobody} 23 | RETVAL=0 24 | 25 | 26 | start() { 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 | } 34 | stop() { 35 | echo -n $"Stopping $prog: " 36 | killproc $ajaxterm 37 | RETVAL=$? 38 | echo 39 | [ $RETVAL = 0 ] && rm -f ${lockfile} ${pidfile} 40 | } 41 | reload() { 42 | echo -n $"Reloading $prog: " 43 | killproc $ajaxterm -HUP 44 | RETVAL=$? 45 | echo 46 | } 47 | 48 | # See how we were called. 49 | case "$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 73 | esac 74 | 75 | exit $RETVAL 76 | -------------------------------------------------------------------------------- /ajaxterm.css: -------------------------------------------------------------------------------- 1 | pre.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 | } 11 | pre.stat span { 12 | padding: 0px; 13 | } 14 | pre.stat .on { 15 | background-color: #080; 16 | font-weight: bold; 17 | color: white; 18 | cursor: pointer; 19 | } 20 | pre.stat .off { 21 | background-color: #888; 22 | font-weight: bold; 23 | color: white; 24 | cursor: pointer; 25 | } 26 | pre.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 | } 36 | pre.term span.f0 { color: #000; } 37 | pre.term span.f1 { color: #b00; } 38 | pre.term span.f2 { color: #0b0; } 39 | pre.term span.f3 { color: #bb0; } 40 | pre.term span.f4 { color: #00b; } 41 | pre.term span.f5 { color: #b0b; } 42 | pre.term span.f6 { color: #0bb; } 43 | pre.term span.f7 { color: #bbb; } 44 | pre.term span.f8 { color: #666; } 45 | pre.term span.f9 { color: #f00; } 46 | pre.term span.f10 { color: #0f0; } 47 | pre.term span.f11 { color: #ff0; } 48 | pre.term span.f12 { color: #00f; } 49 | pre.term span.f13 { color: #f0f; } 50 | pre.term span.f14 { color: #0ff; } 51 | pre.term span.f15 { color: #fff; } 52 | pre.term span.b0 { background-color: #000; } 53 | pre.term span.b1 { background-color: #b00; } 54 | pre.term span.b2 { background-color: #0b0; } 55 | pre.term span.b3 { background-color: #bb0; } 56 | pre.term span.b4 { background-color: #00b; } 57 | pre.term span.b5 { background-color: #b0b; } 58 | pre.term span.b6 { background-color: #0bb; } 59 | pre.term span.b7 { background-color: #bbb; } 60 | 61 | body { background-color: #888; } 62 | #term { 63 | float: left; 64 | } 65 | -------------------------------------------------------------------------------- /sarissa_dhtml.js: -------------------------------------------------------------------------------- 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 | */ 38 | Sarissa.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 | */ 67 | Sarissa.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 | 106 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 |

Ajaxterm

2 | 3 |

Intro

4 |
  5 | Ajaxterm is a web based terminal. It was totally inspired and works almost
  6 | exactly like http://anyterm.org/ except it's much easier to install (see
  7 | comparaison with anyterm below).
  8 | 
  9 | Ajaxterm written in python (and some AJAX javascript for client side) and
 10 | depends only on python2.3 or better.
 11 | 
 12 | Ajaxterm is '''very simple to install''' on Linux, MacOS X, FreeBSD, Solaris,
 13 | cygwin and any Unix that runs python2.3.
 14 | 
 15 | Ajaxterm was written by Antony Lesuisse (email: al AT udev.org), License Public
 16 | Domain.
 17 | 
18 | 19 | 20 | 21 |

News

22 |
 23 |  - 2008-11-13: v0.11 switch to git, apply minor patches
 24 |  - 2006-10-29: v0.10 allow space in login, cgi launch fix, redhat init
 25 |  - 2006-07-12: v0.9 change uid, daemon fix (Daniel Fischer)
 26 |  - 2006-07-04: v0.8 add login support to ssh (Sven Geggus), change max width to 256
 27 |  - 2006-05-31: v0.7 minor fixes, daemon option
 28 |  - 2006-05-23: v0.6 Applied debian and gentoo patches, renamed to Ajaxterm, default port 8022
 29 | 
30 | 31 | 32 | 33 |

Download and Install

34 |
 35 | 
 36 |  - Release: /ajaxterm/files/Ajaxterm-0.10.tar.gz
 37 | 
 38 | To install Ajaxterm issue the following commands:
 39 | 
 40 |     wget http://antony.lesuisse.org/ajaxterm/files/Ajaxterm-0.10.tar.gz
 41 |     tar zxvf Ajaxterm-0.10.tar.gz
 42 |     cd Ajaxterm-0.10
 43 |     ./ajaxterm.py
 44 | 
 45 | Then point your browser to this URL : http://localhost:8022/
 46 | 
47 | 48 | 49 | 50 |

Screenshot

51 | 52 | ajaxterm screenshot 53 | 54 | 55 | 56 |

Documentation and Caveats

57 |
 58 |  * Ajaxterm only support latin1, if you use Ubuntu or any LANG==en_US.UTF-8
 59 |    distribution don't forget to "unset LANG".
 60 | 
 61 |  * If run as root ajaxterm will run /bin/login, otherwise it will run ssh
 62 |    localhost. To use an other command use the -c option.
 63 | 
 64 |  * By default Ajaxterm only listen at 127.0.0.1:8022. For remote access, it is
 65 |    strongly recommended to use '''https SSL/TLS''', and that is simple to
 66 |    configure if you use the apache web server using mod_proxy.[[BR]][[BR]]
 67 |    Using ssl will also speed up ajaxterm (probably because of keepalive).[[BR]][[BR]]
 68 |    Here is an configuration example:
 69 | 
 70 |     Listen 443
 71 |     NameVirtualHost *:443
 72 | 
 73 |     <VirtualHost *:443>
 74 |        ServerName localhost
 75 |        SSLEngine On
 76 |        SSLCertificateKeyFile ssl/apache.pem
 77 |        SSLCertificateFile ssl/apache.pem
 78 | 
 79 |        ProxyRequests Off
 80 |        <Proxy *>
 81 |                Order deny,allow
 82 |                Allow from all
 83 |        </Proxy>
 84 |        ProxyPass /ajaxterm/ http://localhost:8022/
 85 |        ProxyPassReverse /ajaxterm/ http://localhost:8022/
 86 |     </VirtualHost>
 87 | 
 88 |  * Using GET HTTP request seems to speed up ajaxterm, just click on GET in the
 89 |    interface, but be warned that your keystrokes might be loggued (by apache or
 90 |    any proxy). I usually enable it after the login.
 91 | 
 92 |  * Ajaxterm commandline usage:
 93 | 
 94 |     usage: ajaxterm.py [options]
 95 | 
 96 |     options:
 97 |       -h, --help            show this help message and exit
 98 |       -pPORT, --port=PORT   Set the TCP port (default: 8022)
 99 |       -cCMD, --command=CMD  set the command (default: /bin/login or ssh localhost)
100 |       -l, --log             log requests to stderr (default: quiet mode)
101 |       -d, --daemon          run as daemon in the background
102 |       -PPIDFILE, --pidfile=PIDFILE
103 |                             set the pidfile (default: /var/run/ajaxterm.pid)
104 |       -iINDEX_FILE, --index=INDEX_FILE
105 |                             default index file (default: ajaxterm.html)
106 |       -uUID, --uid=UID      Set the daemon's user id
107 | 
108 |  * Ajaxterm was first written as a demo for qweb (my web framework), but
109 |    actually doesn't use many features of qweb.
110 | 
111 |  * Compared to anyterm:
112 |    * There are no partial updates, ajaxterm updates either all the screen or
113 |      nothing. That make the code simpler and I also think it's faster. HTTP
114 |      replies are always gzencoded. When used in 80x25 mode, almost all of
115 |      them are below the 1500 bytes (size of an ethernet frame) and we just
116 |      replace the screen with the reply (no javascript string handling).
117 |    * Ajaxterm polls the server for updates with an exponentially growing
118 |      timeout when the screen hasn't changed. The timeout is also resetted as
119 |      soon as a key is pressed. Anyterm blocks on a pending request and use a
120 |      parallel connection for keypresses. The anyterm approch is better
121 |      when there aren't any keypress.
122 | 
123 |  * Ajaxterm files are released in the Public Domain, (except
124 |  [http://sarissa.sourceforge.net/doc/ sarissa*] which are LGPL).
125 | 
126 | 127 | 128 | 129 |

TODO

130 |
131 |  * insert mode ESC [ 4 h
132 |  * change size x,y from gui (sending signal)
133 |  * vt102 graphic codepage
134 |  * use innerHTML or prototype instead of sarissa
135 | 
136 | -------------------------------------------------------------------------------- /qweb/static.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.3 2 | # vim:set noet ts=4 foldlevel=0: 3 | 4 | # TODO support ranges 5 | 6 | """A QWeb Component to serve static content 7 | 8 | Serve static contents, directories, zipfiles or python modules 9 | 10 | """ 11 | 12 | import calendar,cgi,md5,mimetypes,os,stat,sys,time,urllib,zipfile 13 | 14 | def get_module_data(module,path): 15 | m=sys.modules[module] 16 | l=getattr(m,'__loader__',None) 17 | d=os.path.dirname(m.__file__) 18 | fname=os.path.join(d,path) 19 | if l: 20 | return l.get_data(fname) 21 | else: 22 | return file(fname).read() 23 | 24 | def path_clean(path): 25 | path=path.replace('\\','/') 26 | pl=[i for i in path.split('/') if (i!='..' and i!='')] 27 | return '/'.join(pl) 28 | 29 | def path_join(*l): 30 | return path_clean(os.path.join(*l)) 31 | 32 | class Entry: 33 | def __init__(self,path,type,mtime,size,data=None): 34 | self.path=path 35 | self.name=os.path.basename(path) 36 | self.type=type 37 | self.mtime=mtime 38 | self.size=size 39 | self.data=data 40 | 41 | class StaticBase: 42 | def __init__(self, urlroot="/", listdir=1): 43 | self.urlroot=urlroot 44 | self.listdir=listdir 45 | 46 | self.type_map=mimetypes.types_map.copy() 47 | self.type_map['.csv']='text/csv' 48 | self.type_map['.htm']='text/html; charset=UTF-8' 49 | self.type_map['.html']='text/html; charset=UTF-8' 50 | self.type_map['.svg']='image/svg+xml' 51 | self.type_map['.svgz']='image/svg+xml' 52 | self.gzencode={".css":1, ".js":1, ".htm":1, ".html":1, ".txt":1, ".xml":1} 53 | 54 | def serve_dir(self,req,path): 55 | if not req.PATH_INFO.endswith('/'): 56 | uri = req.FULL_PATH + '/' 57 | req.http_redirect(uri,1) 58 | else: 59 | l=self.fs_listdir(path) 60 | l.sort() 61 | body='

Listing directory '+path+'

..
\n' 62 | for i in l: 63 | name=i.name 64 | if i.type=="dir": 65 | name+='/' 66 | body+='%s
\n'%(name,name) 67 | return body 68 | def serve_file(self,req,path,entry): 69 | if req.SESSION!=None: 70 | req.SESSION.session_limit_cache=0 71 | lastmod=time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(entry.mtime)) 72 | etag=md5.new(lastmod).hexdigest()[:16] 73 | req.response_headers['Last-Modified']=lastmod 74 | req.response_headers['ETag']=etag 75 | # cached output 76 | if lastmod==req.environ.get('HTTP_IF_MODIFIED_SINCE',"") or etag==req.environ.get('HTTP_IF_NONE_MATCH',""): 77 | req.response_status='304 Not Modified' 78 | # normal output 79 | else: 80 | ext = os.path.splitext(path)[1].lower() 81 | ctype = self.type_map.get(ext, 'application/octet-stream') 82 | req.response_headers['Content-Type']=ctype 83 | if entry.data!=None: 84 | f=entry.data 85 | else: 86 | f=self.fs_getfile(path) 87 | if not isinstance(f,str): 88 | f=f.read() 89 | if self.gzencode.has_key(ext): 90 | req.response_gzencode=1 91 | req.response_headers['Content-Length']=str(len(f)) 92 | req.write(f) 93 | def process(self, req, inline=1): 94 | path=path_clean(req.PATH_INFO[len(self.urlroot):]) 95 | e=self.fs_stat(path) 96 | if e: 97 | if e.type=="dir" and self.listdir: 98 | body=self.serve_dir(req, path) 99 | if inline: 100 | return {'head':"",'body':body} 101 | else: 102 | req.write(body) 103 | elif e.type=="file": 104 | self.serve_file(req,path,e) 105 | else: 106 | req.http_404() 107 | 108 | class StaticDir(StaticBase): 109 | def __init__(self, urlroot="/", root=".", listdir=1): 110 | self.root=root 111 | StaticBase.__init__(self,urlroot,listdir) 112 | def fs_stat(self,path): 113 | fs_path = os.path.join(self.root,path) 114 | try: 115 | st = os.stat(fs_path) 116 | if stat.S_ISDIR(st.st_mode): 117 | type="dir" 118 | else: 119 | type="file" 120 | return Entry(path,type,st.st_mtime,st.st_size) 121 | except os.error: 122 | return None 123 | def fs_getfile(self,path): 124 | fs_path = os.path.join(self.root,path) 125 | return file(fs_path,'rb') 126 | def fs_listdir(self,path): 127 | fs_path = os.path.join(self.root,path) 128 | return [self.fs_stat(os.path.join(fs_path,i)) for i in os.listdir(fs_path)] 129 | 130 | class StaticZip(StaticBase): 131 | def __init__(self, urlroot="/", zipname="",ziproot="/", listdir=1): 132 | StaticBase.__init__(self,urlroot,listdir) 133 | self.zipfile=zipfile.ZipFile(zipname) 134 | self.zipmtime=os.path.getmtime(zipname) 135 | self.ziproot=path_clean(ziproot) 136 | self.zipdir={} 137 | self.zipentry={} 138 | 139 | for zi in self.zipfile.infolist(): 140 | if not zi.filename.endswith('/'): 141 | self.zipentry[zi.filename]=Entry(zi.filename,"file",self.zipmtime,zi.file_size) 142 | 143 | if listdir: 144 | # Build a directory index 145 | for k,v in self.zipentry.items(): 146 | d=os.path.dirname(k) 147 | n=os.path.basename(k) 148 | if d in self.zipdir: 149 | self.zipdir[d][n]=v 150 | else: 151 | self.zipdir[d]={n:v} 152 | i=d 153 | while len(i): 154 | d=os.path.dirname(i) 155 | n=os.path.basename(i) 156 | e=Entry(i,"dir",self.zipmtime,0) 157 | if d in self.zipdir: 158 | self.zipdir[d][n]=e 159 | else: 160 | self.zipdir[d]={n:e} 161 | i=d 162 | def fs_stat(self,path): 163 | fs_path = path_join(self.ziproot,path) 164 | if fs_path in self.zipentry: 165 | return self.zipentry[fs_path] 166 | elif fs_path in self.zipdir: 167 | return Entry(path,"dir",self.zipmtime,0) 168 | else: 169 | return None 170 | def fs_getfile(self,path): 171 | fs_path = path_join(self.ziproot,path) 172 | return self.zipfile.read(fs_path) 173 | def fs_listdir(self,path): 174 | fs_path = path_join(self.ziproot,path) 175 | return self.zipdir[fs_path].values() 176 | 177 | class StaticModule(StaticBase): 178 | def __init__(self, urlroot="/", module="", module_root="/", listdir=0): 179 | StaticBase.__init__(self,urlroot,listdir) 180 | self.module=module 181 | self.mtime=time.time() 182 | self.module_root=path_clean(module_root) 183 | def fs_stat(self,path): 184 | name=path_join(self.module_root,path) 185 | try: 186 | d=get_module_data(self.module,name) 187 | e=Entry(path,"file",self.mtime,len(d)) 188 | e.data=d 189 | return e 190 | except IOError,e: 191 | return None 192 | 193 | #---------------------------------------------------------- 194 | # OLD version: Pure WSGI 195 | #---------------------------------------------------------- 196 | class WSGIStaticServe: 197 | def __init__(self, urlroot="/", root=".", listdir=1, banner=''): 198 | self.urlroot=urlroot 199 | self.root="." 200 | self.listdir=listdir 201 | self.banner=banner 202 | self.type_map=mimetypes.types_map.copy() 203 | self.type_map['.csv']='text/csv' 204 | def __call__(self, environ, start_response): 205 | pi=environ.get("PATH_INFO","") 206 | path = os.path.normpath("./" + pi[len(self.urlroot):] ) 207 | if sys.platform=="win32": 208 | path="/".join(path.split('\\')) 209 | assert path[0]!='/' 210 | fullpath = os.path.join(self.root, path) 211 | if os.path.isdir(fullpath) and self.listdir: 212 | # redirects for directories 213 | if not pi.endswith('/'): 214 | uri = urllib.quote(environ["SCRIPT_NAME"] + environ["PATH_INFO"]) + '/' 215 | start_response("301 Moved Permanently", [("Content-type", "text/html"),("Location",uri)]) 216 | return [] 217 | body=self.banner 218 | body+='

Listing directory '+path+'

..
\n' 219 | l=os.listdir(fullpath) 220 | l.sort() 221 | for i in l: 222 | if os.path.isdir(os.path.join(fullpath,i)): 223 | body+='%s/
\n'%(i,i) 224 | else: 225 | body+='%s
\n'%(i,i) 226 | start_response("200 OK", [("Content-type", "text/html")]) 227 | return [body] 228 | elif os.path.isfile(fullpath): 229 | f = open(fullpath,'rb') 230 | ext = os.path.splitext(fullpath)[1].lower() 231 | ctype = self.type_map.get(ext,'application/octet-stream') 232 | start_response("200 OK", [("Content-type", ctype)]) 233 | return [f.read()] 234 | else: 235 | start_response("404 Not Found", [("Content-type", "text/html")]) 236 | return ['

404 Not Found

'] 237 | 238 | 239 | # 240 | -------------------------------------------------------------------------------- /ajaxterm.js: -------------------------------------------------------------------------------- 1 | ajaxterm={}; 2 | ajaxterm.Terminal_ctor=function(id,width,height) { 3 | var ie=0; 4 | if(window.ActiveXObject) 5 | ie=1; 6 | var sid=""+Math.round(Math.random()*1000000000); 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, more info'); 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 | } 276 | ajaxterm.Terminal=function(id,width,height) { 277 | return new this.Terminal_ctor(id,width,height); 278 | } 279 | 280 | -------------------------------------------------------------------------------- /ajaxterm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Ajaxterm """ 4 | 5 | import array,cgi,fcntl,glob,mimetypes,optparse,os,pty,random,re,signal,select,sys,threading,time,termios,struct,pwd 6 | 7 | os.chdir(os.path.normpath(os.path.dirname(__file__))) 8 | # Optional: Add QWeb in sys path 9 | sys.path[0:0]=glob.glob('../../python') 10 | 11 | import qweb 12 | 13 | class Terminal: 14 | def __init__(self,width=80,height=24): 15 | self.width=width 16 | self.height=height 17 | self.init() 18 | self.reset() 19 | def init(self): 20 | self.esc_seq={ 21 | "\x00": None, 22 | "\x05": self.esc_da, 23 | "\x07": None, 24 | "\x08": self.esc_0x08, 25 | "\x09": self.esc_0x09, 26 | "\x0a": self.esc_0x0a, 27 | "\x0b": self.esc_0x0a, 28 | "\x0c": self.esc_0x0a, 29 | "\x0d": self.esc_0x0d, 30 | "\x0e": None, 31 | "\x0f": None, 32 | "\x1b#8": None, 33 | "\x1b=": None, 34 | "\x1b>": None, 35 | "\x1b(0": None, 36 | "\x1b(A": None, 37 | "\x1b(B": None, 38 | "\x1b[c": self.esc_da, 39 | "\x1b[0c": self.esc_da, 40 | "\x1b]R": None, 41 | "\x1b7": self.esc_save, 42 | "\x1b8": self.esc_restore, 43 | "\x1bD": None, 44 | "\x1bE": None, 45 | "\x1bH": None, 46 | "\x1bM": self.esc_ri, 47 | "\x1bN": None, 48 | "\x1bO": None, 49 | "\x1bZ": self.esc_da, 50 | "\x1ba": None, 51 | "\x1bc": self.reset, 52 | "\x1bn": None, 53 | "\x1bo": None, 54 | } 55 | for k,v in self.esc_seq.items(): 56 | if v==None: 57 | self.esc_seq[k]=self.esc_ignore 58 | # regex 59 | d={ 60 | r'\[\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])' : self.csi_dispatch, 61 | r'\]([^\x07]+)\x07' : self.esc_ignore, 62 | } 63 | self.esc_re=[] 64 | for k,v in d.items(): 65 | self.esc_re.append((re.compile('\x1b'+k),v)) 66 | # define csi sequences 67 | self.csi_seq={ 68 | '@': (self.csi_at,[1]), 69 | '`': (self.csi_G,[1]), 70 | 'J': (self.csi_J,[0]), 71 | 'K': (self.csi_K,[0]), 72 | } 73 | for i in [i[4] for i in dir(self) if i.startswith('csi_') and len(i)==5]: 74 | if not self.csi_seq.has_key(i): 75 | self.csi_seq[i]=(getattr(self,'csi_'+i),[1]) 76 | # Init 0-256 to latin1 and html translation table 77 | self.trl1="" 78 | for i in range(256): 79 | if i<32: 80 | self.trl1+=" " 81 | elif i<127 or i>160: 82 | self.trl1+=chr(i) 83 | else: 84 | self.trl1+="?" 85 | self.trhtml="" 86 | for i in range(256): 87 | if i==0x0a or (i>32 and i<127) or i>160: 88 | self.trhtml+=chr(i) 89 | elif i<=32: 90 | self.trhtml+="\xa0" 91 | else: 92 | self.trhtml+="?" 93 | def reset(self,s=""): 94 | self.scr=array.array('i',[0x000700]*(self.width*self.height)) 95 | self.st=0 96 | self.sb=self.height-1 97 | self.cx_bak=self.cx=0 98 | self.cy_bak=self.cy=0 99 | self.cl=0 100 | self.sgr=0x000700 101 | self.buf="" 102 | self.outbuf="" 103 | self.last_html="" 104 | def peek(self,y1,x1,y2,x2): 105 | return self.scr[self.width*y1+x1:self.width*y2+x2] 106 | def poke(self,y,x,s): 107 | pos=self.width*y+x 108 | self.scr[pos:pos+len(s)]=s 109 | def zero(self,y1,x1,y2,x2): 110 | w=self.width*(y2-y1)+x2-x1+1 111 | z=array.array('i',[0x000700]*w) 112 | self.scr[self.width*y1+x1:self.width*y2+x2+1]=z 113 | def scroll_up(self,y1,y2): 114 | self.poke(y1,0,self.peek(y1+1,0,y2,self.width)) 115 | self.zero(y2,0,y2,self.width-1) 116 | def scroll_down(self,y1,y2): 117 | self.poke(y1+1,0,self.peek(y1,0,y2-1,self.width)) 118 | self.zero(y1,0,y1,self.width-1) 119 | def scroll_right(self,y,x): 120 | self.poke(y,x+1,self.peek(y,x,y,self.width)) 121 | self.zero(y,x,y,x) 122 | def cursor_down(self): 123 | if self.cy>=self.st and self.cy<=self.sb: 124 | self.cl=0 125 | q,r=divmod(self.cy+1,self.sb+1) 126 | if q: 127 | self.scroll_up(self.st,self.sb) 128 | self.cy=self.sb 129 | else: 130 | self.cy=r 131 | def cursor_right(self): 132 | q,r=divmod(self.cx+1,self.width) 133 | if q: 134 | self.cl=1 135 | else: 136 | self.cx=r 137 | def echo(self,c): 138 | if self.cl: 139 | self.cursor_down() 140 | self.cx=0 141 | self.scr[(self.cy*self.width)+self.cx]=self.sgr|ord(c) 142 | self.cursor_right() 143 | def esc_0x08(self,s): 144 | self.cx=max(0,self.cx-1) 145 | def esc_0x09(self,s): 146 | x=self.cx+8 147 | q,r=divmod(x,8) 148 | self.cx=(q*8)%self.width 149 | def esc_0x0a(self,s): 150 | self.cursor_down() 151 | def esc_0x0d(self,s): 152 | self.cl=0 153 | self.cx=0 154 | def esc_save(self,s): 155 | self.cx_bak=self.cx 156 | self.cy_bak=self.cy 157 | def esc_restore(self,s): 158 | self.cx=self.cx_bak 159 | self.cy=self.cy_bak 160 | self.cl=0 161 | def esc_da(self,s): 162 | self.outbuf="\x1b[?6c" 163 | def esc_ri(self,s): 164 | self.cy=max(self.st,self.cy-1) 165 | if self.cy==self.st: 166 | self.scroll_down(self.st,self.sb) 167 | def esc_ignore(self,*s): 168 | pass 169 | # print "term:ignore: %s"%repr(s) 170 | def csi_dispatch(self,seq,mo): 171 | # CSI sequences 172 | s=mo.group(1) 173 | c=mo.group(2) 174 | f=self.csi_seq.get(c,None) 175 | if f: 176 | try: 177 | l=[min(int(i),1024) for i in s.split(';') if len(i)<4] 178 | except ValueError: 179 | l=[] 180 | if len(l)==0: 181 | l=f[1] 182 | f[0](l) 183 | # else: 184 | # print 'csi ignore',c,l 185 | def csi_at(self,l): 186 | for i in range(l[0]): 187 | self.scroll_right(self.cy,self.cx) 188 | def csi_A(self,l): 189 | self.cy=max(self.st,self.cy-l[0]) 190 | def csi_B(self,l): 191 | self.cy=min(self.sb,self.cy+l[0]) 192 | def csi_C(self,l): 193 | self.cx=min(self.width-1,self.cx+l[0]) 194 | self.cl=0 195 | def csi_D(self,l): 196 | self.cx=max(0,self.cx-l[0]) 197 | self.cl=0 198 | def csi_E(self,l): 199 | self.csi_B(l) 200 | self.cx=0 201 | self.cl=0 202 | def csi_F(self,l): 203 | self.csi_A(l) 204 | self.cx=0 205 | self.cl=0 206 | def csi_G(self,l): 207 | self.cx=min(self.width,l[0])-1 208 | def csi_H(self,l): 209 | if len(l)<2: l=[1,1] 210 | self.cx=min(self.width,l[1])-1 211 | self.cy=min(self.height,l[0])-1 212 | self.cl=0 213 | def csi_J(self,l): 214 | if l[0]==0: 215 | self.zero(self.cy,self.cx,self.height-1,self.width-1) 216 | elif l[0]==1: 217 | self.zero(0,0,self.cy,self.cx) 218 | elif l[0]==2: 219 | self.zero(0,0,self.height-1,self.width-1) 220 | def csi_K(self,l): 221 | if l[0]==0: 222 | self.zero(self.cy,self.cx,self.cy,self.width-1) 223 | elif l[0]==1: 224 | self.zero(self.cy,0,self.cy,self.cx) 225 | elif l[0]==2: 226 | self.zero(self.cy,0,self.cy,self.width-1) 227 | def csi_L(self,l): 228 | for i in range(l[0]): 229 | if self.cy=self.st and self.cy<=self.sb: 233 | for i in range(l[0]): 234 | self.scroll_up(self.cy,self.sb) 235 | def csi_P(self,l): 236 | w,cx,cy=self.width,self.cx,self.cy 237 | end=self.peek(cy,cx,cy,w) 238 | self.csi_K([0]) 239 | self.poke(cy,cx,end[l[0]:]) 240 | def csi_X(self,l): 241 | self.zero(self.cy,self.cx,self.cy,self.cx+l[0]) 242 | def csi_a(self,l): 243 | self.csi_C(l) 244 | def csi_c(self,l): 245 | #'\x1b[?0c' 0-8 cursor size 246 | pass 247 | def csi_d(self,l): 248 | self.cy=min(self.height,l[0])-1 249 | def csi_e(self,l): 250 | self.csi_B(l) 251 | def csi_f(self,l): 252 | self.csi_H(l) 253 | def csi_h(self,l): 254 | if l[0]==4: 255 | pass 256 | # print "insert on" 257 | def csi_l(self,l): 258 | if l[0]==4: 259 | pass 260 | # print "insert off" 261 | def csi_m(self,l): 262 | for i in l: 263 | if i==0 or i==39 or i==49 or i==27: 264 | self.sgr=0x000700 265 | elif i==1: 266 | self.sgr=(self.sgr|0x000800) 267 | elif i==7: 268 | self.sgr=0x070000 269 | elif i>=30 and i<=37: 270 | c=i-30 271 | self.sgr=(self.sgr&0xff08ff)|(c<<8) 272 | elif i>=40 and i<=47: 273 | c=i-40 274 | self.sgr=(self.sgr&0x00ffff)|(c<<16) 275 | # else: 276 | # print "CSI sgr ignore",l,i 277 | # print 'sgr: %r %x'%(l,self.sgr) 278 | def csi_r(self,l): 279 | if len(l)<2: l=[0,self.height] 280 | self.st=min(self.height-1,l[0]-1) 281 | self.sb=min(self.height-1,l[1]-1) 282 | self.sb=max(self.st,self.sb) 283 | def csi_s(self,l): 284 | self.esc_save(0) 285 | def csi_u(self,l): 286 | self.esc_restore(0) 287 | def escape(self): 288 | e=self.buf 289 | if len(e)>32: 290 | # print "error %r"%e 291 | self.buf="" 292 | elif e in self.esc_seq: 293 | self.esc_seq[e](e) 294 | self.buf="" 295 | else: 296 | for r,f in self.esc_re: 297 | mo=r.match(e) 298 | if mo: 299 | f(e,mo) 300 | self.buf="" 301 | break 302 | # if self.buf=='': print "ESC %r\n"%e 303 | def write(self,s): 304 | for i in s: 305 | if len(self.buf) or (i in self.esc_seq): 306 | self.buf+=i 307 | self.escape() 308 | elif i == '\x1b': 309 | self.buf+=i 310 | else: 311 | self.echo(i) 312 | def read(self): 313 | b=self.outbuf 314 | self.outbuf="" 315 | return b 316 | def dump(self): 317 | r='' 318 | for i in self.scr: 319 | r+=chr(i&255) 320 | return r 321 | def dumplatin1(self): 322 | return self.dump().translate(self.trl1) 323 | def dumphtml(self,color=1): 324 | h=self.height 325 | w=self.width 326 | r="" 327 | span="" 328 | span_bg,span_fg=-1,-1 329 | for i in range(h*w): 330 | q,c=divmod(self.scr[i],256) 331 | if color: 332 | bg,fg=divmod(q,256) 333 | else: 334 | bg,fg=0,7 335 | if i==self.cy*w+self.cx: 336 | bg,fg=1,7 337 | if (bg!=span_bg or fg!=span_fg or i==h*w-1): 338 | if len(span): 339 | r+='%s'%(span_fg,span_bg,cgi.escape(span.translate(self.trhtml))) 340 | span="" 341 | span_bg,span_fg=bg,fg 342 | span+=chr(c) 343 | if i%w==w-1: 344 | span+='\n' 345 | r='
%s
'%r 346 | if self.last_html==r: 347 | return '' 348 | else: 349 | self.last_html=r 350 | # print self 351 | return r 352 | def __repr__(self): 353 | d=self.dumplatin1() 354 | r="" 355 | for i in range(self.height): 356 | r+="|%s|\n"%d[self.width*i:self.width*(i+1)] 357 | return r 358 | 359 | class SynchronizedMethod: 360 | def __init__(self,lock,orig): 361 | self.lock=lock 362 | self.orig=orig 363 | def __call__(self,*l): 364 | self.lock.acquire() 365 | r=self.orig(*l) 366 | self.lock.release() 367 | return r 368 | 369 | class Multiplex: 370 | def __init__(self,cmd=None): 371 | signal.signal(signal.SIGCHLD, signal.SIG_IGN) 372 | self.cmd=cmd 373 | self.proc={} 374 | self.lock=threading.RLock() 375 | self.thread=threading.Thread(target=self.loop) 376 | self.alive=1 377 | # synchronize methods 378 | for name in ['create','fds','proc_read','proc_write','dump','die','run']: 379 | orig=getattr(self,name) 380 | setattr(self,name,SynchronizedMethod(self.lock,orig)) 381 | self.thread.start() 382 | def create(self,w=80,h=25): 383 | pid,fd=pty.fork() 384 | if pid==0: 385 | try: 386 | fdl=[int(i) for i in os.listdir('/proc/self/fd')] 387 | except OSError: 388 | fdl=range(256) 389 | for i in [i for i in fdl if i>2]: 390 | try: 391 | os.close(i) 392 | except OSError: 393 | pass 394 | if self.cmd: 395 | cmd=['/bin/sh','-c',self.cmd] 396 | elif os.getuid()==0: 397 | cmd=['/bin/login'] 398 | else: 399 | sys.stdout.write("Login: ") 400 | login=sys.stdin.readline().strip() 401 | if re.match('^[0-9A-Za-z-_. ]+$',login): 402 | cmd=['ssh'] 403 | cmd+=['-oPreferredAuthentications=keyboard-interactive,password'] 404 | cmd+=['-oNoHostAuthenticationForLocalhost=yes'] 405 | cmd+=['-oLogLevel=FATAL'] 406 | cmd+=['-F/dev/null','-l',login,'localhost'] 407 | else: 408 | os._exit(0) 409 | env={} 410 | env["COLUMNS"]=str(w) 411 | env["LINES"]=str(h) 412 | env["TERM"]="linux" 413 | env["PATH"]=os.environ['PATH'] 414 | os.execvpe(cmd[0],cmd,env) 415 | else: 416 | fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) 417 | # python bug http://python.org/sf/1112949 on amd64 418 | fcntl.ioctl(fd, struct.unpack('i',struct.pack('I',termios.TIOCSWINSZ))[0], struct.pack("HHHH",h,w,0,0)) 419 | self.proc[fd]={'pid':pid,'term':Terminal(w,h),'buf':'','time':time.time()} 420 | return fd 421 | def die(self): 422 | self.alive=0 423 | def run(self): 424 | return self.alive 425 | def fds(self): 426 | return self.proc.keys() 427 | def proc_kill(self,fd): 428 | if fd in self.proc: 429 | self.proc[fd]['time']=0 430 | t=time.time() 431 | for i in self.proc.keys(): 432 | t0=self.proc[i]['time'] 433 | if (t-t0)>120: 434 | try: 435 | os.close(i) 436 | os.kill(self.proc[i]['pid'],signal.SIGTERM) 437 | except (IOError,OSError): 438 | pass 439 | del self.proc[i] 440 | def proc_read(self,fd): 441 | try: 442 | t=self.proc[fd]['term'] 443 | t.write(os.read(fd,65536)) 444 | reply=t.read() 445 | if reply: 446 | os.write(fd,reply) 447 | self.proc[fd]['time']=time.time() 448 | except (KeyError,IOError,OSError): 449 | self.proc_kill(fd) 450 | def proc_write(self,fd,s): 451 | try: 452 | os.write(fd,s) 453 | except (IOError,OSError): 454 | self.proc_kill(fd) 455 | def dump(self,fd,color=1): 456 | try: 457 | return self.proc[fd]['term'].dumphtml(color) 458 | except KeyError: 459 | return False 460 | def loop(self): 461 | while self.run(): 462 | fds=self.fds() 463 | i,o,e=select.select(fds, [], [], 1.0) 464 | for fd in i: 465 | self.proc_read(fd) 466 | if len(i): 467 | time.sleep(0.002) 468 | for i in self.proc.keys(): 469 | try: 470 | os.close(i) 471 | os.kill(self.proc[i]['pid'],signal.SIGTERM) 472 | except (IOError,OSError): 473 | pass 474 | 475 | class AjaxTerm: 476 | def __init__(self,cmd=None,index_file='ajaxterm.html'): 477 | self.files={} 478 | for i in ['css','html','js']: 479 | for j in glob.glob('*.%s'%i): 480 | self.files[j]=file(j).read() 481 | self.files['index']=file(index_file).read() 482 | self.mime = mimetypes.types_map.copy() 483 | self.mime['.html']= 'text/html; charset=UTF-8' 484 | self.multi = Multiplex(cmd) 485 | self.session = {} 486 | def __call__(self, environ, start_response): 487 | req = qweb.QWebRequest(environ, start_response,session=None) 488 | if req.PATH_INFO.endswith('/u'): 489 | s=req.REQUEST["s"] 490 | k=req.REQUEST["k"] 491 | c=req.REQUEST["c"] 492 | w=req.REQUEST.int("w") 493 | h=req.REQUEST.int("h") 494 | if s in self.session: 495 | term=self.session[s] 496 | else: 497 | if not (w>2 and w<256 and h>2 and h<100): 498 | w,h=80,25 499 | term=self.session[s]=self.multi.create(w,h) 500 | if k: 501 | self.multi.proc_write(term,k) 502 | time.sleep(0.002) 503 | dump=self.multi.dump(term,c) 504 | req.response_headers['Content-Type']='text/xml' 505 | if isinstance(dump,str): 506 | req.write(dump) 507 | req.response_gzencode=1 508 | else: 509 | del self.session[s] 510 | req.write('') 511 | # print "sessions %r"%self.session 512 | else: 513 | n=os.path.basename(req.PATH_INFO) 514 | if n in self.files: 515 | req.response_headers['Content-Type'] = self.mime.get(os.path.splitext(n)[1].lower(), 'application/octet-stream') 516 | req.write(self.files[n]) 517 | else: 518 | req.response_headers['Content-Type'] = 'text/html; charset=UTF-8' 519 | req.write(self.files['index']) 520 | return req 521 | 522 | def main(): 523 | parser = optparse.OptionParser() 524 | parser.add_option("-p", "--port", dest="port", default="8022", help="Set the TCP port (default: 8022)") 525 | parser.add_option("-c", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh localhost)") 526 | parser.add_option("-l", "--log", action="store_true", dest="log",default=0,help="log requests to stderr (default: quiet mode)") 527 | parser.add_option("-d", "--daemon", action="store_true", dest="daemon", default=0, help="run as daemon in the background") 528 | parser.add_option("-P", "--pidfile",dest="pidfile",default="/var/run/ajaxterm.pid",help="set the pidfile (default: /var/run/ajaxterm.pid)") 529 | parser.add_option("-i", "--index", dest="index_file", default="ajaxterm.html",help="default index file (default: ajaxterm.html)") 530 | parser.add_option("-u", "--uid", dest="uid", help="Set the daemon's user id") 531 | parser.add_option("-b", "--bind", dest="addr", help="IP or address to bind to") 532 | 533 | (o, a) = parser.parse_args() 534 | 535 | addr = 'localhost' 536 | if o.addr: 537 | addr = o.addr 538 | 539 | if o.daemon: 540 | pid=os.fork() 541 | if pid == 0: 542 | #os.setsid() ? 543 | os.setpgrp() 544 | nullin = file('/dev/null', 'r') 545 | nullout = file('/dev/null', 'w') 546 | os.dup2(nullin.fileno(), sys.stdin.fileno()) 547 | os.dup2(nullout.fileno(), sys.stdout.fileno()) 548 | os.dup2(nullout.fileno(), sys.stderr.fileno()) 549 | if os.getuid()==0 and o.uid: 550 | try: 551 | os.setuid(int(o.uid)) 552 | except: 553 | os.setuid(pwd.getpwnam(o.uid).pw_uid) 554 | else: 555 | try: 556 | file(o.pidfile,'w+').write(str(pid)+'\n') 557 | except: 558 | pass 559 | print 'AjaxTerm at http://localhost:%s/ pid: %d' % (o.port,pid) 560 | sys.exit(0) 561 | else: 562 | print 'AjaxTerm at http://localhost:%s/' % o.port 563 | at=AjaxTerm(o.cmd,o.index_file) 564 | # f=lambda:os.system('firefox http://localhost:%s/&'%o.port) 565 | # qweb.qweb_wsgi_autorun(at,ip='localhost',port=int(o.port),threaded=0,log=o.log,callback_ready=None) 566 | try: 567 | qweb.QWebWSGIServer(at,ip=addr,port=int(o.port),threaded=0,log=o.log).serve_forever() 568 | except KeyboardInterrupt,e: 569 | sys.excepthook(*sys.exc_info()) 570 | at.multi.die() 571 | 572 | if __name__ == '__main__': 573 | main() 574 | 575 | -------------------------------------------------------------------------------- /ajaxenterm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Ajaxterm """ 4 | 5 | import array,cgi,fcntl,glob,mimetypes,optparse,os,pty,random,re,signal,select,sys,threading,time,termios,struct,pwd 6 | 7 | os.chdir(os.path.normpath(os.path.dirname(__file__))) 8 | # Optional: Add QWeb in sys path 9 | sys.path[0:0]=glob.glob('../../python') 10 | sys.path.append('/usr/lib64/python') 11 | import qweb 12 | import xen.lowlevel.xs as xs 13 | 14 | def getDomIDFromName(DomName): 15 | found=-1 16 | sock=xs.xs() 17 | trans=sock.transaction_start() 18 | domains=sock.ls(trans,'/local/domain') 19 | for domid in domains: 20 | path='/local/domain/'+domid+'/name' 21 | name=sock.read(trans,path) 22 | if (name == DomName): 23 | found=domid 24 | sock.transaction_end(trans) 25 | return found 26 | 27 | def getTTYByDomID(DomID): 28 | sock=xs.xs() 29 | trans=sock.transaction_start() 30 | path=sock.get_domain_path(DomID) 31 | pty=sock.read(trans,path+'/console/tty') 32 | sock.transaction_end(trans) 33 | return pty 34 | 35 | class Terminal: 36 | def __init__(self,width=80,height=24): 37 | self.width=width 38 | self.height=height 39 | self.init() 40 | self.reset() 41 | def init(self): 42 | self.esc_seq={ 43 | "\x00": None, 44 | "\x05": self.esc_da, 45 | "\x07": None, 46 | "\x08": self.esc_0x08, 47 | "\x09": self.esc_0x09, 48 | "\x0a": self.esc_0x0a, 49 | "\x0b": self.esc_0x0a, 50 | "\x0c": self.esc_0x0a, 51 | "\x0d": self.esc_0x0d, 52 | "\x0e": None, 53 | "\x0f": None, 54 | "\x1b#8": None, 55 | "\x1b=": None, 56 | "\x1b>": None, 57 | "\x1b(0": None, 58 | "\x1b(A": None, 59 | "\x1b(B": None, 60 | "\x1b[c": self.esc_da, 61 | "\x1b[0c": self.esc_da, 62 | "\x1b]R": None, 63 | "\x1b7": self.esc_save, 64 | "\x1b8": self.esc_restore, 65 | "\x1bD": None, 66 | "\x1bE": None, 67 | "\x1bH": None, 68 | "\x1bM": self.esc_ri, 69 | "\x1bN": None, 70 | "\x1bO": None, 71 | "\x1bZ": self.esc_da, 72 | "\x1ba": None, 73 | "\x1bc": self.reset, 74 | "\x1bn": None, 75 | "\x1bo": None, 76 | } 77 | for k,v in self.esc_seq.items(): 78 | if v==None: 79 | self.esc_seq[k]=self.esc_ignore 80 | # regex 81 | d={ 82 | r'\[\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])' : self.csi_dispatch, 83 | r'\]([^\x07]+)\x07' : self.esc_ignore, 84 | } 85 | self.esc_re=[] 86 | for k,v in d.items(): 87 | self.esc_re.append((re.compile('\x1b'+k),v)) 88 | # define csi sequences 89 | self.csi_seq={ 90 | '@': (self.csi_at,[1]), 91 | '`': (self.csi_G,[1]), 92 | 'J': (self.csi_J,[0]), 93 | 'K': (self.csi_K,[0]), 94 | } 95 | for i in [i[4] for i in dir(self) if i.startswith('csi_') and len(i)==5]: 96 | if not self.csi_seq.has_key(i): 97 | self.csi_seq[i]=(getattr(self,'csi_'+i),[1]) 98 | # Init 0-256 to latin1 and html translation table 99 | self.trl1="" 100 | for i in range(256): 101 | if i<32: 102 | self.trl1+=" " 103 | elif i<127 or i>160: 104 | self.trl1+=chr(i) 105 | else: 106 | self.trl1+="?" 107 | self.trhtml="" 108 | for i in range(256): 109 | if i==0x0a or (i>32 and i<127) or i>160: 110 | self.trhtml+=chr(i) 111 | elif i<=32: 112 | self.trhtml+="\xa0" 113 | else: 114 | self.trhtml+="?" 115 | def reset(self,s=""): 116 | self.scr=array.array('i',[0x000700]*(self.width*self.height)) 117 | self.st=0 118 | self.sb=self.height-1 119 | self.cx_bak=self.cx=0 120 | self.cy_bak=self.cy=0 121 | self.cl=0 122 | self.sgr=0x000700 123 | self.buf="" 124 | self.outbuf="" 125 | self.last_html="" 126 | def peek(self,y1,x1,y2,x2): 127 | return self.scr[self.width*y1+x1:self.width*y2+x2] 128 | def poke(self,y,x,s): 129 | pos=self.width*y+x 130 | self.scr[pos:pos+len(s)]=s 131 | def zero(self,y1,x1,y2,x2): 132 | w=self.width*(y2-y1)+x2-x1+1 133 | z=array.array('i',[0x000700]*w) 134 | self.scr[self.width*y1+x1:self.width*y2+x2+1]=z 135 | def scroll_up(self,y1,y2): 136 | self.poke(y1,0,self.peek(y1+1,0,y2,self.width)) 137 | self.zero(y2,0,y2,self.width-1) 138 | def scroll_down(self,y1,y2): 139 | self.poke(y1+1,0,self.peek(y1,0,y2-1,self.width)) 140 | self.zero(y1,0,y1,self.width-1) 141 | def scroll_right(self,y,x): 142 | self.poke(y,x+1,self.peek(y,x,y,self.width)) 143 | self.zero(y,x,y,x) 144 | def cursor_down(self): 145 | if self.cy>=self.st and self.cy<=self.sb: 146 | self.cl=0 147 | q,r=divmod(self.cy+1,self.sb+1) 148 | if q: 149 | self.scroll_up(self.st,self.sb) 150 | self.cy=self.sb 151 | else: 152 | self.cy=r 153 | def cursor_right(self): 154 | q,r=divmod(self.cx+1,self.width) 155 | if q: 156 | self.cl=1 157 | else: 158 | self.cx=r 159 | def echo(self,c): 160 | if self.cl: 161 | self.cursor_down() 162 | self.cx=0 163 | self.scr[(self.cy*self.width)+self.cx]=self.sgr|ord(c) 164 | self.cursor_right() 165 | def esc_0x08(self,s): 166 | self.cx=max(0,self.cx-1) 167 | def esc_0x09(self,s): 168 | x=self.cx+8 169 | q,r=divmod(x,8) 170 | self.cx=(q*8)%self.width 171 | def esc_0x0a(self,s): 172 | self.cursor_down() 173 | def esc_0x0d(self,s): 174 | self.cl=0 175 | self.cx=0 176 | def esc_save(self,s): 177 | self.cx_bak=self.cx 178 | self.cy_bak=self.cy 179 | def esc_restore(self,s): 180 | self.cx=self.cx_bak 181 | self.cy=self.cy_bak 182 | self.cl=0 183 | def esc_da(self,s): 184 | self.outbuf="\x1b[?6c" 185 | def esc_ri(self,s): 186 | self.cy=max(self.st,self.cy-1) 187 | if self.cy==self.st: 188 | self.scroll_down(self.st,self.sb) 189 | def esc_ignore(self,*s): 190 | pass 191 | # print "term:ignore: %s"%repr(s) 192 | def csi_dispatch(self,seq,mo): 193 | # CSI sequences 194 | s=mo.group(1) 195 | c=mo.group(2) 196 | f=self.csi_seq.get(c,None) 197 | if f: 198 | try: 199 | l=[min(int(i),1024) for i in s.split(';') if len(i)<4] 200 | except ValueError: 201 | l=[] 202 | if len(l)==0: 203 | l=f[1] 204 | f[0](l) 205 | # else: 206 | # print 'csi ignore',c,l 207 | def csi_at(self,l): 208 | for i in range(l[0]): 209 | self.scroll_right(self.cy,self.cx) 210 | def csi_A(self,l): 211 | self.cy=max(self.st,self.cy-l[0]) 212 | def csi_B(self,l): 213 | self.cy=min(self.sb,self.cy+l[0]) 214 | def csi_C(self,l): 215 | self.cx=min(self.width-1,self.cx+l[0]) 216 | self.cl=0 217 | def csi_D(self,l): 218 | self.cx=max(0,self.cx-l[0]) 219 | self.cl=0 220 | def csi_E(self,l): 221 | self.csi_B(l) 222 | self.cx=0 223 | self.cl=0 224 | def csi_F(self,l): 225 | self.csi_A(l) 226 | self.cx=0 227 | self.cl=0 228 | def csi_G(self,l): 229 | self.cx=min(self.width,l[0])-1 230 | def csi_H(self,l): 231 | if len(l)<2: l=[1,1] 232 | self.cx=min(self.width,l[1])-1 233 | self.cy=min(self.height,l[0])-1 234 | self.cl=0 235 | def csi_J(self,l): 236 | if l[0]==0: 237 | self.zero(self.cy,self.cx,self.height-1,self.width-1) 238 | elif l[0]==1: 239 | self.zero(0,0,self.cy,self.cx) 240 | elif l[0]==2: 241 | self.zero(0,0,self.height-1,self.width-1) 242 | def csi_K(self,l): 243 | if l[0]==0: 244 | self.zero(self.cy,self.cx,self.cy,self.width-1) 245 | elif l[0]==1: 246 | self.zero(self.cy,0,self.cy,self.cx) 247 | elif l[0]==2: 248 | self.zero(self.cy,0,self.cy,self.width-1) 249 | def csi_L(self,l): 250 | for i in range(l[0]): 251 | if self.cy=self.st and self.cy<=self.sb: 255 | for i in range(l[0]): 256 | self.scroll_up(self.cy,self.sb) 257 | def csi_P(self,l): 258 | w,cx,cy=self.width,self.cx,self.cy 259 | end=self.peek(cy,cx,cy,w) 260 | self.csi_K([0]) 261 | self.poke(cy,cx,end[l[0]:]) 262 | def csi_X(self,l): 263 | self.zero(self.cy,self.cx,self.cy,self.cx+l[0]) 264 | def csi_a(self,l): 265 | self.csi_C(l) 266 | def csi_c(self,l): 267 | #'\x1b[?0c' 0-8 cursor size 268 | pass 269 | def csi_d(self,l): 270 | self.cy=min(self.height,l[0])-1 271 | def csi_e(self,l): 272 | self.csi_B(l) 273 | def csi_f(self,l): 274 | self.csi_H(l) 275 | def csi_h(self,l): 276 | if l[0]==4: 277 | pass 278 | # print "insert on" 279 | def csi_l(self,l): 280 | if l[0]==4: 281 | pass 282 | # print "insert off" 283 | def csi_m(self,l): 284 | for i in l: 285 | if i==0 or i==39 or i==49 or i==27: 286 | self.sgr=0x000700 287 | elif i==1: 288 | self.sgr=(self.sgr|0x000800) 289 | elif i==7: 290 | self.sgr=0x070000 291 | elif i>=30 and i<=37: 292 | c=i-30 293 | self.sgr=(self.sgr&0xff08ff)|(c<<8) 294 | elif i>=40 and i<=47: 295 | c=i-40 296 | self.sgr=(self.sgr&0x00ffff)|(c<<16) 297 | # else: 298 | # print "CSI sgr ignore",l,i 299 | # print 'sgr: %r %x'%(l,self.sgr) 300 | def csi_r(self,l): 301 | if len(l)<2: l=[0,self.height] 302 | self.st=min(self.height-1,l[0]-1) 303 | self.sb=min(self.height-1,l[1]-1) 304 | self.sb=max(self.st,self.sb) 305 | def csi_s(self,l): 306 | self.esc_save(0) 307 | def csi_u(self,l): 308 | self.esc_restore(0) 309 | def escape(self): 310 | e=self.buf 311 | if len(e)>32: 312 | # print "error %r"%e 313 | self.buf="" 314 | elif e in self.esc_seq: 315 | self.esc_seq[e](e) 316 | self.buf="" 317 | else: 318 | for r,f in self.esc_re: 319 | mo=r.match(e) 320 | if mo: 321 | f(e,mo) 322 | self.buf="" 323 | break 324 | # if self.buf=='': print "ESC %r\n"%e 325 | def write(self,s): 326 | for i in s: 327 | if len(self.buf) or (i in self.esc_seq): 328 | self.buf+=i 329 | self.escape() 330 | elif i == '\x1b': 331 | self.buf+=i 332 | else: 333 | self.echo(i) 334 | def read(self): 335 | b=self.outbuf 336 | self.outbuf="" 337 | return b 338 | def dump(self): 339 | r='' 340 | for i in self.scr: 341 | r+=chr(i&255) 342 | return r 343 | def dumplatin1(self): 344 | return self.dump().translate(self.trl1) 345 | def dumphtml(self,color=1): 346 | h=self.height 347 | w=self.width 348 | r="" 349 | span="" 350 | span_bg,span_fg=-1,-1 351 | for i in range(h*w): 352 | q,c=divmod(self.scr[i],256) 353 | if color: 354 | bg,fg=divmod(q,256) 355 | else: 356 | bg,fg=0,7 357 | if i==self.cy*w+self.cx: 358 | bg,fg=1,7 359 | if (bg!=span_bg or fg!=span_fg or i==h*w-1): 360 | if len(span): 361 | r+='%s'%(span_fg,span_bg,cgi.escape(span.translate(self.trhtml))) 362 | span="" 363 | span_bg,span_fg=bg,fg 364 | span+=chr(c) 365 | if i%w==w-1: 366 | span+='\n' 367 | r='
%s
'%r 368 | if self.last_html==r: 369 | return '' 370 | else: 371 | self.last_html=r 372 | # print self 373 | return r 374 | def __repr__(self): 375 | d=self.dumplatin1() 376 | r="" 377 | for i in range(self.height): 378 | r+="|%s|\n"%d[self.width*i:self.width*(i+1)] 379 | return r 380 | 381 | class SynchronizedMethod: 382 | def __init__(self,lock,orig): 383 | self.lock=lock 384 | self.orig=orig 385 | def __call__(self,*l): 386 | self.lock.acquire() 387 | r=self.orig(*l) 388 | self.lock.release() 389 | return r 390 | 391 | class Multiplex: 392 | def __init__(self,cmd=None,domname=None): 393 | signal.signal(signal.SIGCHLD, signal.SIG_IGN) 394 | self.cmd=cmd 395 | self.proc={} 396 | self.lock=threading.RLock() 397 | self.thread=threading.Thread(target=self.loop) 398 | self.alive=1 399 | self.domname=domname 400 | self.xenfd=-1 401 | # synchronize methods 402 | for name in ['create','fds','proc_read','proc_write','dump','die','run']: 403 | orig=getattr(self,name) 404 | setattr(self,name,SynchronizedMethod(self.lock,orig)) 405 | self.thread.start() 406 | def create(self,w=80,h=25): 407 | pid,fd=pty.fork() 408 | if pid==0: 409 | try: 410 | fdl=[int(i) for i in os.listdir('/proc/self/fd')] 411 | except OSError: 412 | fdl=range(256) 413 | for i in [i for i in fdl if i>2]: 414 | try: 415 | os.close(i) 416 | except OSError: 417 | pass 418 | if self.cmd: 419 | cmd=['/bin/sh','-c',self.cmd] 420 | elif os.getuid()==0: 421 | #cmd=['/bin/login'] 422 | #cmd=['/usr/bin/telnet', 'localhost', '9002'] 423 | cmd=['/bin/ls'] 424 | else: 425 | sys.stdout.write("Login: ") 426 | login=sys.stdin.readline().strip() 427 | if re.match('^[0-9A-Za-z-_.]+$',login): 428 | cmd=['ssh'] 429 | cmd+=['-oPreferredAuthentications=keyboard-interactive,password'] 430 | cmd+=['-oNoHostAuthenticationForLocalhost=yes'] 431 | cmd+=['-oLogLevel=FATAL'] 432 | cmd+=['-F/dev/null','-l',login,'localhost'] 433 | else: 434 | os._exit(0) 435 | env={} 436 | env["COLUMNS"]=str(w) 437 | env["LINES"]=str(h) 438 | env["TERM"]="linux" 439 | env["PATH"]=os.environ['PATH'] 440 | os.execvpe(cmd[0],cmd,env) 441 | else: 442 | # Get Xen pty 443 | if (self.domname): 444 | domid=int(getDomIDFromName(self.domname)) 445 | 446 | if (domid != -1): 447 | xenpty=getTTYByDomID(domid) 448 | if (self.xenfd != -1): 449 | print "Closing xen fd" 450 | os.close(self.xenfd) 451 | self.xenfd=-1 452 | if (self.xenfd == -1): 453 | print "Opening xen fd" 454 | fd=os.open(xenpty,os.O_RDWR+os.O_NONBLOCK) 455 | self.xenfd=fd 456 | 457 | 458 | fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) 459 | # python bug http://python.org/sf/1112949 on amd64 460 | fcntl.ioctl(fd, struct.unpack('i',struct.pack('I',termios.TIOCSWINSZ))[0], struct.pack("HHHH",h,w,0,0)) 461 | self.proc[fd]={'pid':pid,'term':Terminal(w,h),'buf':'','time':time.time()} 462 | return fd 463 | def die(self): 464 | self.alive=0 465 | def run(self): 466 | return self.alive 467 | def fds(self): 468 | return self.proc.keys() 469 | def proc_kill(self,fd): 470 | if fd in self.proc: 471 | self.proc[fd]['time']=0 472 | t=time.time() 473 | for i in self.proc.keys(): 474 | t0=self.proc[i]['time'] 475 | if (t-t0)>120: 476 | try: 477 | os.close(i) 478 | os.kill(self.proc[i]['pid'],signal.SIGTERM) 479 | except (IOError,OSError): 480 | pass 481 | del self.proc[i] 482 | def proc_read(self,fd): 483 | try: 484 | t=self.proc[fd]['term'] 485 | t.write(os.read(fd,65536)) 486 | reply=t.read() 487 | if reply: 488 | os.write(fd,reply) 489 | self.proc[fd]['time']=time.time() 490 | except (KeyError,IOError,OSError): 491 | self.proc_kill(fd) 492 | def proc_write(self,fd,s): 493 | try: 494 | os.write(fd,s) 495 | except (IOError,OSError): 496 | self.proc_kill(fd) 497 | def dump(self,fd,color=1): 498 | try: 499 | return self.proc[fd]['term'].dumphtml(color) 500 | except KeyError: 501 | return False 502 | def loop(self): 503 | while self.run(): 504 | fds=self.fds() 505 | i,o,e=select.select(fds, [], [], 1.0) 506 | for fd in i: 507 | self.proc_read(fd) 508 | if len(i): 509 | time.sleep(0.002) 510 | for i in self.proc.keys(): 511 | try: 512 | os.close(i) 513 | os.kill(self.proc[i]['pid'],signal.SIGTERM) 514 | except (IOError,OSError): 515 | pass 516 | 517 | class AjaxTerm: 518 | def __init__(self,cmd=None,index_file='ajaxterm.html',domname=None): 519 | self.files={} 520 | for i in ['css','html','js']: 521 | for j in glob.glob('*.%s'%i): 522 | self.files[j]=file(j).read() 523 | self.files['index']=file(index_file).read() 524 | self.mime = mimetypes.types_map.copy() 525 | self.mime['.html']= 'text/html; charset=UTF-8' 526 | self.multi = Multiplex(cmd,domname) 527 | self.session = {} 528 | def __call__(self, environ, start_response): 529 | req = qweb.QWebRequest(environ, start_response,session=None) 530 | if req.PATH_INFO.endswith('/u'): 531 | s=req.REQUEST["s"] 532 | k=req.REQUEST["k"] 533 | c=req.REQUEST["c"] 534 | w=req.REQUEST.int("w") 535 | h=req.REQUEST.int("h") 536 | if s in self.session: 537 | term=self.session[s] 538 | else: 539 | if not (w>2 and w<256 and h>2 and h<100): 540 | w,h=80,25 541 | term=self.session[s]=self.multi.create(w,h) 542 | if k: 543 | self.multi.proc_write(term,k) 544 | time.sleep(0.002) 545 | dump=self.multi.dump(term,c) 546 | req.response_headers['Content-Type']='text/xml' 547 | if isinstance(dump,str): 548 | req.write(dump) 549 | req.response_gzencode=1 550 | else: 551 | del self.session[s] 552 | req.write('') 553 | # print "sessions %r"%self.session 554 | else: 555 | n=os.path.basename(req.PATH_INFO) 556 | if n in self.files: 557 | req.response_headers['Content-Type'] = self.mime.get(os.path.splitext(n)[1].lower(), 'application/octet-stream') 558 | req.write(self.files[n]) 559 | else: 560 | req.response_headers['Content-Type'] = 'text/html; charset=UTF-8' 561 | req.write(self.files['index']) 562 | return req 563 | 564 | def main(): 565 | parser = optparse.OptionParser() 566 | parser.add_option("-p", "--port", dest="port", default="8022", help="Set the TCP port (default: 8022)") 567 | parser.add_option("-c", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh localhost)") 568 | parser.add_option("-l", "--log", action="store_true", dest="log",default=0,help="log requests to stderr (default: quiet mode)") 569 | parser.add_option("-d", "--daemon", action="store_true", dest="daemon", default=0, help="run as daemon in the background") 570 | parser.add_option("-P", "--pidfile",dest="pidfile",default="/var/run/ajaxterm.pid",help="set the pidfile (default: /var/run/ajaxterm.pid)") 571 | parser.add_option("-i", "--index", dest="index_file", default="ajaxterm.html",help="default index file (default: ajaxterm.html)") 572 | parser.add_option("-u", "--uid", dest="uid", help="Set the daemon's user id") 573 | parser.add_option("-x", "--xenid", dest="domid", help="ID of the Xen domain to connect to") 574 | parser.add_option("-n", "--domname", dest="domname", help="Name of Xen domain to connect to") 575 | parser.add_option("-b", "--bind", dest="addr", help="IP or address to bind to") 576 | parser.add_option("-a", "--all", action="store_true", dest="all", default=0, help="starts for all Xen domains") 577 | 578 | (o, a) = parser.parse_args() 579 | 580 | import socket 581 | if o.addr: 582 | myname=o.addr 583 | else: 584 | myname=socket.gethostname() 585 | 586 | if o.all: 587 | import subprocess, re 588 | 589 | output, error = subprocess.Popen(["xm", "list"], stdout = subprocess.PIPE, stderr= subprocess.PIPE).communicate() 590 | 591 | port = int(o.port) 592 | lines = output.split("\n") 593 | for line in lines[2:-1]: 594 | xen_domain = re.sub(' +', ' ', line).split(' ')[0] 595 | print xen_domain 596 | 597 | pid=os.fork() 598 | if pid == 0: 599 | #os.setsid() ? 600 | os.setpgrp() 601 | nullin = file('/dev/null', 'r') 602 | nullout = file('/dev/null', 'w') 603 | os.dup2(nullin.fileno(), sys.stdin.fileno()) 604 | os.dup2(nullout.fileno(), sys.stdout.fileno()) 605 | os.dup2(nullout.fileno(), sys.stderr.fileno()) 606 | if os.getuid()==0 and o.uid: 607 | try: 608 | os.setuid(int(o.uid)) 609 | except: 610 | os.setuid(pwd.getpwnam(o.uid).pw_uid) 611 | 612 | at=AjaxTerm(o.cmd,o.index_file,xen_domain) 613 | qweb.qweb_wsgi_autorun(at,ip=myname,port=port,threaded=0,log=o.log,callback_ready=None) 614 | at.multi.die() 615 | 616 | else: 617 | try: 618 | file(o.pidfile,'w+').write(str(pid)+'\n') 619 | except: 620 | pass 621 | print "%s,%s" % (xen_domain,str(port)) 622 | print 'AjaxTerm for %s at http://%s:%s/ pid: %d' % (xen_domain,myname,port,pid) 623 | 624 | port += 1 625 | 626 | else: 627 | if o.daemon: 628 | pid=os.fork() 629 | if pid == 0: 630 | #os.setsid() ? 631 | os.setpgrp() 632 | nullin = file('/dev/null', 'r') 633 | nullout = file('/dev/null', 'w') 634 | os.dup2(nullin.fileno(), sys.stdin.fileno()) 635 | os.dup2(nullout.fileno(), sys.stdout.fileno()) 636 | os.dup2(nullout.fileno(), sys.stderr.fileno()) 637 | if os.getuid()==0 and o.uid: 638 | try: 639 | os.setuid(int(o.uid)) 640 | except: 641 | os.setuid(pwd.getpwnam(o.uid).pw_uid) 642 | else: 643 | try: 644 | file(o.pidfile,'w+').write(str(pid)+'\n') 645 | except: 646 | pass 647 | print 'AjaxTerm at http://%s:%s/ pid: %d' % (myname,o.port,pid) 648 | sys.exit(0) 649 | else: 650 | print 'AjaxTerm at http://%s:%s/' % (myname,o.port) 651 | 652 | print "starting ajaxterm" 653 | at=AjaxTerm(o.cmd,o.index_file,o.domname) 654 | 655 | print "starting web server" 656 | qweb.qweb_wsgi_autorun(at,ip=myname,port=int(o.port),threaded=0,log=o.log,callback_ready=None) 657 | 658 | print "Successfully started AjaxTerm server" 659 | at.multi.die() 660 | 661 | if __name__ == '__main__': 662 | main() 663 | 664 | -------------------------------------------------------------------------------- /sarissa.js: -------------------------------------------------------------------------------- 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 | *

Sarissa is a utility class. Provides "static" methods for DOMDocument and 31 | * XMLHTTP objects, DOM Node serializatrion to XML strings and other goodies.

32 | * @constructor 33 | */ 34 | function Sarissa(){}; 35 | /** @private */ 36 | Sarissa.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 | */ 44 | Sarissa.IS_ENABLED_TRANSFORM_NODE = false; 45 | /** 46 | * tells you whether XMLHttpRequest (or equivalent) is available 47 | * @type boolean 48 | */ 49 | Sarissa.IS_ENABLED_XMLHTTP = false; 50 | /** 51 | * tells you whether selectNodes/selectSingleNode is available 52 | * @type boolean 53 | */ 54 | Sarissa.IS_ENABLED_SELECT_NODES = false; 55 | var _sarissa_iNsCounter = 0; 56 | var _SARISSA_IEPREFIX4XSLPARAM = ""; 57 | var _SARISSA_HAS_DOM_IMPLEMENTATION = document.implementation && true; 58 | var _SARISSA_HAS_DOM_CREATE_DOCUMENT = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.createDocument; 59 | var _SARISSA_HAS_DOM_FEATURE = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.hasFeature; 60 | var _SARISSA_IS_MOZ = _SARISSA_HAS_DOM_CREATE_DOCUMENT && _SARISSA_HAS_DOM_FEATURE; 61 | var _SARISSA_IS_SAFARI = (navigator.userAgent && navigator.vendor && (navigator.userAgent.toLowerCase().indexOf("applewebkit") != -1 || navigator.vendor.indexOf("Apple") != -1)); 62 | var _SARISSA_IS_IE = document.all && window.ActiveXObject && navigator.userAgent.toLowerCase().indexOf("msie") > -1 && navigator.userAgent.toLowerCase().indexOf("opera") == -1; 63 | if(!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 68 | if(_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"); 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 | } 219 | else{ /* end IE initialization, try to deal with real browsers now ;-) */ 220 | if(_SARISSA_HAS_DOM_CREATE_DOCUMENT){ 221 | /** 222 | *

Ensures the document was loaded correctly, otherwise sets the 223 | * parseError to -1 to indicate something went wrong. Internal use

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 | *

Attached by an event handler to the load event. Internal use.

233 | * @private 234 | */ 235 | _sarissa_XMLDocument_onload = function(){ 236 | Sarissa.__handleLoad__(this); 237 | }; 238 | /** 239 | *

Sets the readyState property of the given DOM Document object. 240 | * Internal use.

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(window.XMLDocument){ 257 | /** 258 | *

Emulate IE's onreadystatechange attribute

259 | */ 260 | XMLDocument.prototype.onreadystatechange = null; 261 | /** 262 | *

Emulates IE's readyState property, which always gives an integer from 0 to 4:

263 | *
  • 1 == LOADING,
  • 264 | *
  • 2 == LOADED,
  • 265 | *
  • 3 == INTERACTIVE,
  • 266 | *
  • 4 == COMPLETED
267 | */ 268 | XMLDocument.prototype.readyState = 0; 269 | /** 270 | *

Emulate IE's parseError attribute

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 | *

Keeps a handle to the original load() method. Internal use and only 283 | * if Mozilla version is lower than 1.4

284 | * @private 285 | */ 286 | XMLDocument.prototype._sarissa_load = XMLDocument.prototype.load; 287 | 288 | /** 289 | *

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)

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 | *

Factory method to obtain a new DOM Document object

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 | //========================================== 366 | if(!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 | 398 | if(window.XMLHttpRequest){ 399 | Sarissa.IS_ENABLED_XMLHTTP = true; 400 | } 401 | else 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 | 412 | if(!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 | }; 433 | if(!Sarissa.getParseErrorText){ 434 | /** 435 | *

Returns a human readable description of the parsing error. Usefull 436 | * for debugging. Tip: append the returned error string in a <pre> 437 | * element if you want to render it.

438 | *

Many thanks to Christian Stocker for the initial patch.

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 | }; 460 | Sarissa.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 | }; 477 | if(window.XMLSerializer){ 478 | /** 479 | *

Factory method to obtain the serialization of a DOM Node

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 | */ 517 | Sarissa.stripTags = function (s) { 518 | return s.replace(/<[^>]+>/g,""); 519 | }; 520 | /** 521 | *

Deletes all child nodes of the given node

522 | * @argument oNode the Node to empty 523 | */ 524 | Sarissa.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 | *

Copies the childNodes of nodeFrom to nodeTo

532 | *

Note: The second object's original content is deleted before 533 | * the copy operation, unless you supply a true third parameter

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 | */ 538 | Sarissa.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 | *

Moves the childNodes of nodeFrom to nodeTo

561 | *

Note: The second object's original content is deleted before 562 | * the move operation, unless you supply a true third parameter

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 | */ 567 | Sarissa.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 | *

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 array-item elements, 598 | * using their index/key as the value of the key attribute.

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 | */ 603 | Sarissa.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?"\n":"\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 | */ 627 | Sarissa.escape = function(sXml){ 628 | return sXml.replace(/&/g, "&") 629 | .replace(//g, ">") 631 | .replace(/"/g, """) 632 | .replace(/'/g, "'"); 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 | */ 640 | Sarissa.unescape = function(sXml){ 641 | return sXml.replace(/'/g,"'") 642 | .replace(/"/g,"\"") 643 | .replace(/>/g,">") 644 | .replace(/</g,"<") 645 | .replace(/&/g,"&"); 646 | }; 647 | // EOF 648 | -------------------------------------------------------------------------------- /qweb/fcgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (c) 2002, 2003, 2005 Allan Saddi 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 15 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 18 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | # SUCH DAMAGE. 25 | # 26 | # $Id$ 27 | 28 | """ 29 | fcgi - a FastCGI/WSGI gateway. 30 | 31 | For more information about FastCGI, see . 32 | 33 | For more information about the Web Server Gateway Interface, see 34 | . 35 | 36 | Example usage: 37 | 38 | #!/usr/bin/env python 39 | from myapplication import app # Assume app is your WSGI application object 40 | from fcgi import WSGIServer 41 | WSGIServer(app).run() 42 | 43 | See the documentation for WSGIServer/Server for more information. 44 | 45 | On most platforms, fcgi will fallback to regular CGI behavior if run in a 46 | non-FastCGI context. If you want to force CGI behavior, set the environment 47 | variable FCGI_FORCE_CGI to "Y" or "y". 48 | """ 49 | 50 | __author__ = 'Allan Saddi ' 51 | __version__ = '$Revision$' 52 | 53 | import sys 54 | import os 55 | import signal 56 | import struct 57 | import cStringIO as StringIO 58 | import select 59 | import socket 60 | import errno 61 | import traceback 62 | 63 | try: 64 | import thread 65 | import threading 66 | thread_available = True 67 | except ImportError: 68 | import dummy_thread as thread 69 | import dummy_threading as threading 70 | thread_available = False 71 | 72 | # Apparently 2.3 doesn't define SHUT_WR? Assume it is 1 in this case. 73 | if not hasattr(socket, 'SHUT_WR'): 74 | socket.SHUT_WR = 1 75 | 76 | __all__ = ['WSGIServer'] 77 | 78 | # Constants from the spec. 79 | FCGI_LISTENSOCK_FILENO = 0 80 | 81 | FCGI_HEADER_LEN = 8 82 | 83 | FCGI_VERSION_1 = 1 84 | 85 | FCGI_BEGIN_REQUEST = 1 86 | FCGI_ABORT_REQUEST = 2 87 | FCGI_END_REQUEST = 3 88 | FCGI_PARAMS = 4 89 | FCGI_STDIN = 5 90 | FCGI_STDOUT = 6 91 | FCGI_STDERR = 7 92 | FCGI_DATA = 8 93 | FCGI_GET_VALUES = 9 94 | FCGI_GET_VALUES_RESULT = 10 95 | FCGI_UNKNOWN_TYPE = 11 96 | FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE 97 | 98 | FCGI_NULL_REQUEST_ID = 0 99 | 100 | FCGI_KEEP_CONN = 1 101 | 102 | FCGI_RESPONDER = 1 103 | FCGI_AUTHORIZER = 2 104 | FCGI_FILTER = 3 105 | 106 | FCGI_REQUEST_COMPLETE = 0 107 | FCGI_CANT_MPX_CONN = 1 108 | FCGI_OVERLOADED = 2 109 | FCGI_UNKNOWN_ROLE = 3 110 | 111 | FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' 112 | FCGI_MAX_REQS = 'FCGI_MAX_REQS' 113 | FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' 114 | 115 | FCGI_Header = '!BBHHBx' 116 | FCGI_BeginRequestBody = '!HB5x' 117 | FCGI_EndRequestBody = '!LB3x' 118 | FCGI_UnknownTypeBody = '!B7x' 119 | 120 | FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody) 121 | FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody) 122 | 123 | if __debug__: 124 | import time 125 | 126 | # Set non-zero to write debug output to a file. 127 | DEBUG = 0 128 | DEBUGLOG = '/tmp/fcgi.log' 129 | 130 | def _debug(level, msg): 131 | if DEBUG < level: 132 | return 133 | 134 | try: 135 | f = open(DEBUGLOG, 'a') 136 | f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg)) 137 | f.close() 138 | except: 139 | pass 140 | 141 | class InputStream(object): 142 | """ 143 | File-like object representing FastCGI input streams (FCGI_STDIN and 144 | FCGI_DATA). Supports the minimum methods required by WSGI spec. 145 | """ 146 | def __init__(self, conn): 147 | self._conn = conn 148 | 149 | # See Server. 150 | self._shrinkThreshold = conn.server.inputStreamShrinkThreshold 151 | 152 | self._buf = '' 153 | self._bufList = [] 154 | self._pos = 0 # Current read position. 155 | self._avail = 0 # Number of bytes currently available. 156 | 157 | self._eof = False # True when server has sent EOF notification. 158 | 159 | def _shrinkBuffer(self): 160 | """Gets rid of already read data (since we can't rewind).""" 161 | if self._pos >= self._shrinkThreshold: 162 | self._buf = self._buf[self._pos:] 163 | self._avail -= self._pos 164 | self._pos = 0 165 | 166 | assert self._avail >= 0 167 | 168 | def _waitForData(self): 169 | """Waits for more data to become available.""" 170 | self._conn.process_input() 171 | 172 | def read(self, n=-1): 173 | if self._pos == self._avail and self._eof: 174 | return '' 175 | while True: 176 | if n < 0 or (self._avail - self._pos) < n: 177 | # Not enough data available. 178 | if self._eof: 179 | # And there's no more coming. 180 | newPos = self._avail 181 | break 182 | else: 183 | # Wait for more data. 184 | self._waitForData() 185 | continue 186 | else: 187 | newPos = self._pos + n 188 | break 189 | # Merge buffer list, if necessary. 190 | if self._bufList: 191 | self._buf += ''.join(self._bufList) 192 | self._bufList = [] 193 | r = self._buf[self._pos:newPos] 194 | self._pos = newPos 195 | self._shrinkBuffer() 196 | return r 197 | 198 | def readline(self, length=None): 199 | if self._pos == self._avail and self._eof: 200 | return '' 201 | while True: 202 | # Unfortunately, we need to merge the buffer list early. 203 | if self._bufList: 204 | self._buf += ''.join(self._bufList) 205 | self._bufList = [] 206 | # Find newline. 207 | i = self._buf.find('\n', self._pos) 208 | if i < 0: 209 | # Not found? 210 | if self._eof: 211 | # No more data coming. 212 | newPos = self._avail 213 | break 214 | else: 215 | # Wait for more to come. 216 | self._waitForData() 217 | continue 218 | else: 219 | newPos = i + 1 220 | break 221 | if length is not None: 222 | if self._pos + length < newPos: 223 | newPos = self._pos + length 224 | r = self._buf[self._pos:newPos] 225 | self._pos = newPos 226 | self._shrinkBuffer() 227 | return r 228 | 229 | def readlines(self, sizehint=0): 230 | total = 0 231 | lines = [] 232 | line = self.readline() 233 | while line: 234 | lines.append(line) 235 | total += len(line) 236 | if 0 < sizehint <= total: 237 | break 238 | line = self.readline() 239 | return lines 240 | 241 | def __iter__(self): 242 | return self 243 | 244 | def next(self): 245 | r = self.readline() 246 | if not r: 247 | raise StopIteration 248 | return r 249 | 250 | def add_data(self, data): 251 | if not data: 252 | self._eof = True 253 | else: 254 | self._bufList.append(data) 255 | self._avail += len(data) 256 | 257 | class MultiplexedInputStream(InputStream): 258 | """ 259 | A version of InputStream meant to be used with MultiplexedConnections. 260 | Assumes the MultiplexedConnection (the producer) and the Request 261 | (the consumer) are running in different threads. 262 | """ 263 | def __init__(self, conn): 264 | super(MultiplexedInputStream, self).__init__(conn) 265 | 266 | # Arbitrates access to this InputStream (it's used simultaneously 267 | # by a Request and its owning Connection object). 268 | lock = threading.RLock() 269 | 270 | # Notifies Request thread that there is new data available. 271 | self._lock = threading.Condition(lock) 272 | 273 | def _waitForData(self): 274 | # Wait for notification from add_data(). 275 | self._lock.wait() 276 | 277 | def read(self, n=-1): 278 | self._lock.acquire() 279 | try: 280 | return super(MultiplexedInputStream, self).read(n) 281 | finally: 282 | self._lock.release() 283 | 284 | def readline(self, length=None): 285 | self._lock.acquire() 286 | try: 287 | return super(MultiplexedInputStream, self).readline(length) 288 | finally: 289 | self._lock.release() 290 | 291 | def add_data(self, data): 292 | self._lock.acquire() 293 | try: 294 | super(MultiplexedInputStream, self).add_data(data) 295 | self._lock.notify() 296 | finally: 297 | self._lock.release() 298 | 299 | class OutputStream(object): 300 | """ 301 | FastCGI output stream (FCGI_STDOUT/FCGI_STDERR). By default, calls to 302 | write() or writelines() immediately result in Records being sent back 303 | to the server. Buffering should be done in a higher level! 304 | """ 305 | def __init__(self, conn, req, type, buffered=False): 306 | self._conn = conn 307 | self._req = req 308 | self._type = type 309 | self._buffered = buffered 310 | self._bufList = [] # Used if buffered is True 311 | self.dataWritten = False 312 | self.closed = False 313 | 314 | def _write(self, data): 315 | length = len(data) 316 | while length: 317 | toWrite = min(length, self._req.server.maxwrite - FCGI_HEADER_LEN) 318 | 319 | rec = Record(self._type, self._req.requestId) 320 | rec.contentLength = toWrite 321 | rec.contentData = data[:toWrite] 322 | self._conn.writeRecord(rec) 323 | 324 | data = data[toWrite:] 325 | length -= toWrite 326 | 327 | def write(self, data): 328 | assert not self.closed 329 | 330 | if not data: 331 | return 332 | 333 | self.dataWritten = True 334 | 335 | if self._buffered: 336 | self._bufList.append(data) 337 | else: 338 | self._write(data) 339 | 340 | def writelines(self, lines): 341 | assert not self.closed 342 | 343 | for line in lines: 344 | self.write(line) 345 | 346 | def flush(self): 347 | # Only need to flush if this OutputStream is actually buffered. 348 | if self._buffered: 349 | data = ''.join(self._bufList) 350 | self._bufList = [] 351 | self._write(data) 352 | 353 | # Though available, the following should NOT be called by WSGI apps. 354 | def close(self): 355 | """Sends end-of-stream notification, if necessary.""" 356 | if not self.closed and self.dataWritten: 357 | self.flush() 358 | rec = Record(self._type, self._req.requestId) 359 | self._conn.writeRecord(rec) 360 | self.closed = True 361 | 362 | class TeeOutputStream(object): 363 | """ 364 | Simple wrapper around two or more output file-like objects that copies 365 | written data to all streams. 366 | """ 367 | def __init__(self, streamList): 368 | self._streamList = streamList 369 | 370 | def write(self, data): 371 | for f in self._streamList: 372 | f.write(data) 373 | 374 | def writelines(self, lines): 375 | for line in lines: 376 | self.write(line) 377 | 378 | def flush(self): 379 | for f in self._streamList: 380 | f.flush() 381 | 382 | class StdoutWrapper(object): 383 | """ 384 | Wrapper for sys.stdout so we know if data has actually been written. 385 | """ 386 | def __init__(self, stdout): 387 | self._file = stdout 388 | self.dataWritten = False 389 | 390 | def write(self, data): 391 | if data: 392 | self.dataWritten = True 393 | self._file.write(data) 394 | 395 | def writelines(self, lines): 396 | for line in lines: 397 | self.write(line) 398 | 399 | def __getattr__(self, name): 400 | return getattr(self._file, name) 401 | 402 | def decode_pair(s, pos=0): 403 | """ 404 | Decodes a name/value pair. 405 | 406 | The number of bytes decoded as well as the name/value pair 407 | are returned. 408 | """ 409 | nameLength = ord(s[pos]) 410 | if nameLength & 128: 411 | nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff 412 | pos += 4 413 | else: 414 | pos += 1 415 | 416 | valueLength = ord(s[pos]) 417 | if valueLength & 128: 418 | valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff 419 | pos += 4 420 | else: 421 | pos += 1 422 | 423 | name = s[pos:pos+nameLength] 424 | pos += nameLength 425 | value = s[pos:pos+valueLength] 426 | pos += valueLength 427 | 428 | return (pos, (name, value)) 429 | 430 | def encode_pair(name, value): 431 | """ 432 | Encodes a name/value pair. 433 | 434 | The encoded string is returned. 435 | """ 436 | nameLength = len(name) 437 | if nameLength < 128: 438 | s = chr(nameLength) 439 | else: 440 | s = struct.pack('!L', nameLength | 0x80000000L) 441 | 442 | valueLength = len(value) 443 | if valueLength < 128: 444 | s += chr(valueLength) 445 | else: 446 | s += struct.pack('!L', valueLength | 0x80000000L) 447 | 448 | return s + name + value 449 | 450 | class Record(object): 451 | """ 452 | A FastCGI Record. 453 | 454 | Used for encoding/decoding records. 455 | """ 456 | def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID): 457 | self.version = FCGI_VERSION_1 458 | self.type = type 459 | self.requestId = requestId 460 | self.contentLength = 0 461 | self.paddingLength = 0 462 | self.contentData = '' 463 | 464 | def _recvall(sock, length): 465 | """ 466 | Attempts to receive length bytes from a socket, blocking if necessary. 467 | (Socket may be blocking or non-blocking.) 468 | """ 469 | dataList = [] 470 | recvLen = 0 471 | while length: 472 | try: 473 | data = sock.recv(length) 474 | except socket.error, e: 475 | if e[0] == errno.EAGAIN: 476 | select.select([sock], [], []) 477 | continue 478 | else: 479 | raise 480 | if not data: # EOF 481 | break 482 | dataList.append(data) 483 | dataLen = len(data) 484 | recvLen += dataLen 485 | length -= dataLen 486 | return ''.join(dataList), recvLen 487 | _recvall = staticmethod(_recvall) 488 | 489 | def read(self, sock): 490 | """Read and decode a Record from a socket.""" 491 | try: 492 | header, length = self._recvall(sock, FCGI_HEADER_LEN) 493 | except: 494 | raise EOFError 495 | 496 | if length < FCGI_HEADER_LEN: 497 | raise EOFError 498 | 499 | self.version, self.type, self.requestId, self.contentLength, \ 500 | self.paddingLength = struct.unpack(FCGI_Header, header) 501 | 502 | if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, ' 503 | 'contentLength = %d' % 504 | (sock.fileno(), self.type, self.requestId, 505 | self.contentLength)) 506 | 507 | if self.contentLength: 508 | try: 509 | self.contentData, length = self._recvall(sock, 510 | self.contentLength) 511 | except: 512 | raise EOFError 513 | 514 | if length < self.contentLength: 515 | raise EOFError 516 | 517 | if self.paddingLength: 518 | try: 519 | self._recvall(sock, self.paddingLength) 520 | except: 521 | raise EOFError 522 | 523 | def _sendall(sock, data): 524 | """ 525 | Writes data to a socket and does not return until all the data is sent. 526 | """ 527 | length = len(data) 528 | while length: 529 | try: 530 | sent = sock.send(data) 531 | except socket.error, e: 532 | if e[0] == errno.EPIPE: 533 | return # Don't bother raising an exception. Just ignore. 534 | elif e[0] == errno.EAGAIN: 535 | select.select([], [sock], []) 536 | continue 537 | else: 538 | raise 539 | data = data[sent:] 540 | length -= sent 541 | _sendall = staticmethod(_sendall) 542 | 543 | def write(self, sock): 544 | """Encode and write a Record to a socket.""" 545 | self.paddingLength = -self.contentLength & 7 546 | 547 | if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, ' 548 | 'contentLength = %d' % 549 | (sock.fileno(), self.type, self.requestId, 550 | self.contentLength)) 551 | 552 | header = struct.pack(FCGI_Header, self.version, self.type, 553 | self.requestId, self.contentLength, 554 | self.paddingLength) 555 | self._sendall(sock, header) 556 | if self.contentLength: 557 | self._sendall(sock, self.contentData) 558 | if self.paddingLength: 559 | self._sendall(sock, '\x00'*self.paddingLength) 560 | 561 | class Request(object): 562 | """ 563 | Represents a single FastCGI request. 564 | 565 | These objects are passed to your handler and is the main interface 566 | between your handler and the fcgi module. The methods should not 567 | be called by your handler. However, server, params, stdin, stdout, 568 | stderr, and data are free for your handler's use. 569 | """ 570 | def __init__(self, conn, inputStreamClass): 571 | self._conn = conn 572 | 573 | self.server = conn.server 574 | self.params = {} 575 | self.stdin = inputStreamClass(conn) 576 | self.stdout = OutputStream(conn, self, FCGI_STDOUT) 577 | self.stderr = OutputStream(conn, self, FCGI_STDERR, buffered=True) 578 | self.data = inputStreamClass(conn) 579 | 580 | def run(self): 581 | """Runs the handler, flushes the streams, and ends the request.""" 582 | try: 583 | protocolStatus, appStatus = self.server.handler(self) 584 | except: 585 | traceback.print_exc(file=self.stderr) 586 | self.stderr.flush() 587 | if not self.stdout.dataWritten: 588 | self.server.error(self) 589 | 590 | protocolStatus, appStatus = FCGI_REQUEST_COMPLETE, 0 591 | 592 | if __debug__: _debug(1, 'protocolStatus = %d, appStatus = %d' % 593 | (protocolStatus, appStatus)) 594 | 595 | self._flush() 596 | self._end(appStatus, protocolStatus) 597 | 598 | def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE): 599 | self._conn.end_request(self, appStatus, protocolStatus) 600 | 601 | def _flush(self): 602 | self.stdout.close() 603 | self.stderr.close() 604 | 605 | class CGIRequest(Request): 606 | """A normal CGI request disguised as a FastCGI request.""" 607 | def __init__(self, server): 608 | # These are normally filled in by Connection. 609 | self.requestId = 1 610 | self.role = FCGI_RESPONDER 611 | self.flags = 0 612 | self.aborted = False 613 | 614 | self.server = server 615 | self.params = dict(os.environ) 616 | self.stdin = sys.stdin 617 | self.stdout = StdoutWrapper(sys.stdout) # Oh, the humanity! 618 | self.stderr = sys.stderr 619 | self.data = StringIO.StringIO() 620 | 621 | def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE): 622 | sys.exit(appStatus) 623 | 624 | def _flush(self): 625 | # Not buffered, do nothing. 626 | pass 627 | 628 | class Connection(object): 629 | """ 630 | A Connection with the web server. 631 | 632 | Each Connection is associated with a single socket (which is 633 | connected to the web server) and is responsible for handling all 634 | the FastCGI message processing for that socket. 635 | """ 636 | _multiplexed = False 637 | _inputStreamClass = InputStream 638 | 639 | def __init__(self, sock, addr, server): 640 | self._sock = sock 641 | self._addr = addr 642 | self.server = server 643 | 644 | # Active Requests for this Connection, mapped by request ID. 645 | self._requests = {} 646 | 647 | def _cleanupSocket(self): 648 | """Close the Connection's socket.""" 649 | try: 650 | self._sock.shutdown(socket.SHUT_WR) 651 | except: 652 | return 653 | try: 654 | while True: 655 | r, w, e = select.select([self._sock], [], []) 656 | if not r or not self._sock.recv(1024): 657 | break 658 | except: 659 | pass 660 | self._sock.close() 661 | 662 | def run(self): 663 | """Begin processing data from the socket.""" 664 | self._keepGoing = True 665 | while self._keepGoing: 666 | try: 667 | self.process_input() 668 | except EOFError: 669 | break 670 | except (select.error, socket.error), e: 671 | if e[0] == errno.EBADF: # Socket was closed by Request. 672 | break 673 | raise 674 | 675 | self._cleanupSocket() 676 | 677 | def process_input(self): 678 | """Attempt to read a single Record from the socket and process it.""" 679 | # Currently, any children Request threads notify this Connection 680 | # that it is no longer needed by closing the Connection's socket. 681 | # We need to put a timeout on select, otherwise we might get 682 | # stuck in it indefinitely... (I don't like this solution.) 683 | while self._keepGoing: 684 | try: 685 | r, w, e = select.select([self._sock], [], [], 1.0) 686 | except ValueError: 687 | # Sigh. ValueError gets thrown sometimes when passing select 688 | # a closed socket. 689 | raise EOFError 690 | if r: break 691 | if not self._keepGoing: 692 | return 693 | rec = Record() 694 | rec.read(self._sock) 695 | 696 | if rec.type == FCGI_GET_VALUES: 697 | self._do_get_values(rec) 698 | elif rec.type == FCGI_BEGIN_REQUEST: 699 | self._do_begin_request(rec) 700 | elif rec.type == FCGI_ABORT_REQUEST: 701 | self._do_abort_request(rec) 702 | elif rec.type == FCGI_PARAMS: 703 | self._do_params(rec) 704 | elif rec.type == FCGI_STDIN: 705 | self._do_stdin(rec) 706 | elif rec.type == FCGI_DATA: 707 | self._do_data(rec) 708 | elif rec.requestId == FCGI_NULL_REQUEST_ID: 709 | self._do_unknown_type(rec) 710 | else: 711 | # Need to complain about this. 712 | pass 713 | 714 | def writeRecord(self, rec): 715 | """ 716 | Write a Record to the socket. 717 | """ 718 | rec.write(self._sock) 719 | 720 | def end_request(self, req, appStatus=0L, 721 | protocolStatus=FCGI_REQUEST_COMPLETE, remove=True): 722 | """ 723 | End a Request. 724 | 725 | Called by Request objects. An FCGI_END_REQUEST Record is 726 | sent to the web server. If the web server no longer requires 727 | the connection, the socket is closed, thereby ending this 728 | Connection (run() returns). 729 | """ 730 | rec = Record(FCGI_END_REQUEST, req.requestId) 731 | rec.contentData = struct.pack(FCGI_EndRequestBody, appStatus, 732 | protocolStatus) 733 | rec.contentLength = FCGI_EndRequestBody_LEN 734 | self.writeRecord(rec) 735 | 736 | if remove: 737 | del self._requests[req.requestId] 738 | 739 | if __debug__: _debug(2, 'end_request: flags = %d' % req.flags) 740 | 741 | if not (req.flags & FCGI_KEEP_CONN) and not self._requests: 742 | self._cleanupSocket() 743 | self._keepGoing = False 744 | 745 | def _do_get_values(self, inrec): 746 | """Handle an FCGI_GET_VALUES request from the web server.""" 747 | outrec = Record(FCGI_GET_VALUES_RESULT) 748 | 749 | pos = 0 750 | while pos < inrec.contentLength: 751 | pos, (name, value) = decode_pair(inrec.contentData, pos) 752 | cap = self.server.capability.get(name) 753 | if cap is not None: 754 | outrec.contentData += encode_pair(name, str(cap)) 755 | 756 | outrec.contentLength = len(outrec.contentData) 757 | self.writeRecord(outrec) 758 | 759 | def _do_begin_request(self, inrec): 760 | """Handle an FCGI_BEGIN_REQUEST from the web server.""" 761 | role, flags = struct.unpack(FCGI_BeginRequestBody, inrec.contentData) 762 | 763 | req = self.server.request_class(self, self._inputStreamClass) 764 | req.requestId, req.role, req.flags = inrec.requestId, role, flags 765 | req.aborted = False 766 | 767 | if not self._multiplexed and self._requests: 768 | # Can't multiplex requests. 769 | self.end_request(req, 0L, FCGI_CANT_MPX_CONN, remove=False) 770 | else: 771 | self._requests[inrec.requestId] = req 772 | 773 | def _do_abort_request(self, inrec): 774 | """ 775 | Handle an FCGI_ABORT_REQUEST from the web server. 776 | 777 | We just mark a flag in the associated Request. 778 | """ 779 | req = self._requests.get(inrec.requestId) 780 | if req is not None: 781 | req.aborted = True 782 | 783 | def _start_request(self, req): 784 | """Run the request.""" 785 | # Not multiplexed, so run it inline. 786 | req.run() 787 | 788 | def _do_params(self, inrec): 789 | """ 790 | Handle an FCGI_PARAMS Record. 791 | 792 | If the last FCGI_PARAMS Record is received, start the request. 793 | """ 794 | req = self._requests.get(inrec.requestId) 795 | if req is not None: 796 | if inrec.contentLength: 797 | pos = 0 798 | while pos < inrec.contentLength: 799 | pos, (name, value) = decode_pair(inrec.contentData, pos) 800 | req.params[name] = value 801 | else: 802 | self._start_request(req) 803 | 804 | def _do_stdin(self, inrec): 805 | """Handle the FCGI_STDIN stream.""" 806 | req = self._requests.get(inrec.requestId) 807 | if req is not None: 808 | req.stdin.add_data(inrec.contentData) 809 | 810 | def _do_data(self, inrec): 811 | """Handle the FCGI_DATA stream.""" 812 | req = self._requests.get(inrec.requestId) 813 | if req is not None: 814 | req.data.add_data(inrec.contentData) 815 | 816 | def _do_unknown_type(self, inrec): 817 | """Handle an unknown request type. Respond accordingly.""" 818 | outrec = Record(FCGI_UNKNOWN_TYPE) 819 | outrec.contentData = struct.pack(FCGI_UnknownTypeBody, inrec.type) 820 | outrec.contentLength = FCGI_UnknownTypeBody_LEN 821 | self.writeRecord(rec) 822 | 823 | class MultiplexedConnection(Connection): 824 | """ 825 | A version of Connection capable of handling multiple requests 826 | simultaneously. 827 | """ 828 | _multiplexed = True 829 | _inputStreamClass = MultiplexedInputStream 830 | 831 | def __init__(self, sock, addr, server): 832 | super(MultiplexedConnection, self).__init__(sock, addr, server) 833 | 834 | # Used to arbitrate access to self._requests. 835 | lock = threading.RLock() 836 | 837 | # Notification is posted everytime a request completes, allowing us 838 | # to quit cleanly. 839 | self._lock = threading.Condition(lock) 840 | 841 | def _cleanupSocket(self): 842 | # Wait for any outstanding requests before closing the socket. 843 | self._lock.acquire() 844 | while self._requests: 845 | self._lock.wait() 846 | self._lock.release() 847 | 848 | super(MultiplexedConnection, self)._cleanupSocket() 849 | 850 | def writeRecord(self, rec): 851 | # Must use locking to prevent intermingling of Records from different 852 | # threads. 853 | self._lock.acquire() 854 | try: 855 | # Probably faster than calling super. ;) 856 | rec.write(self._sock) 857 | finally: 858 | self._lock.release() 859 | 860 | def end_request(self, req, appStatus=0L, 861 | protocolStatus=FCGI_REQUEST_COMPLETE, remove=True): 862 | self._lock.acquire() 863 | try: 864 | super(MultiplexedConnection, self).end_request(req, appStatus, 865 | protocolStatus, 866 | remove) 867 | self._lock.notify() 868 | finally: 869 | self._lock.release() 870 | 871 | def _do_begin_request(self, inrec): 872 | self._lock.acquire() 873 | try: 874 | super(MultiplexedConnection, self)._do_begin_request(inrec) 875 | finally: 876 | self._lock.release() 877 | 878 | def _do_abort_request(self, inrec): 879 | self._lock.acquire() 880 | try: 881 | super(MultiplexedConnection, self)._do_abort_request(inrec) 882 | finally: 883 | self._lock.release() 884 | 885 | def _start_request(self, req): 886 | thread.start_new_thread(req.run, ()) 887 | 888 | def _do_params(self, inrec): 889 | self._lock.acquire() 890 | try: 891 | super(MultiplexedConnection, self)._do_params(inrec) 892 | finally: 893 | self._lock.release() 894 | 895 | def _do_stdin(self, inrec): 896 | self._lock.acquire() 897 | try: 898 | super(MultiplexedConnection, self)._do_stdin(inrec) 899 | finally: 900 | self._lock.release() 901 | 902 | def _do_data(self, inrec): 903 | self._lock.acquire() 904 | try: 905 | super(MultiplexedConnection, self)._do_data(inrec) 906 | finally: 907 | self._lock.release() 908 | 909 | class Server(object): 910 | """ 911 | The FastCGI server. 912 | 913 | Waits for connections from the web server, processing each 914 | request. 915 | 916 | If run in a normal CGI context, it will instead instantiate a 917 | CGIRequest and run the handler through there. 918 | """ 919 | request_class = Request 920 | cgirequest_class = CGIRequest 921 | 922 | # Limits the size of the InputStream's string buffer to this size + the 923 | # server's maximum Record size. Since the InputStream is not seekable, 924 | # we throw away already-read data once this certain amount has been read. 925 | inputStreamShrinkThreshold = 102400 - 8192 926 | 927 | def __init__(self, handler=None, maxwrite=8192, bindAddress=None, 928 | multiplexed=False): 929 | """ 930 | handler, if present, must reference a function or method that 931 | takes one argument: a Request object. If handler is not 932 | specified at creation time, Server *must* be subclassed. 933 | (The handler method below is abstract.) 934 | 935 | maxwrite is the maximum number of bytes (per Record) to write 936 | to the server. I've noticed mod_fastcgi has a relatively small 937 | receive buffer (8K or so). 938 | 939 | bindAddress, if present, must either be a string or a 2-tuple. If 940 | present, run() will open its own listening socket. You would use 941 | this if you wanted to run your application as an 'external' FastCGI 942 | app. (i.e. the webserver would no longer be responsible for starting 943 | your app) If a string, it will be interpreted as a filename and a UNIX 944 | socket will be opened. If a tuple, the first element, a string, 945 | is the interface name/IP to bind to, and the second element (an int) 946 | is the port number. 947 | 948 | Set multiplexed to True if you want to handle multiple requests 949 | per connection. Some FastCGI backends (namely mod_fastcgi) don't 950 | multiplex requests at all, so by default this is off (which saves 951 | on thread creation/locking overhead). If threads aren't available, 952 | this keyword is ignored; it's not possible to multiplex requests 953 | at all. 954 | """ 955 | if handler is not None: 956 | self.handler = handler 957 | self.maxwrite = maxwrite 958 | if thread_available: 959 | try: 960 | import resource 961 | # Attempt to glean the maximum number of connections 962 | # from the OS. 963 | maxConns = resource.getrlimit(resource.RLIMIT_NOFILE)[0] 964 | except ImportError: 965 | maxConns = 100 # Just some made up number. 966 | maxReqs = maxConns 967 | if multiplexed: 968 | self._connectionClass = MultiplexedConnection 969 | maxReqs *= 5 # Another made up number. 970 | else: 971 | self._connectionClass = Connection 972 | self.capability = { 973 | FCGI_MAX_CONNS: maxConns, 974 | FCGI_MAX_REQS: maxReqs, 975 | FCGI_MPXS_CONNS: multiplexed and 1 or 0 976 | } 977 | else: 978 | self._connectionClass = Connection 979 | self.capability = { 980 | # If threads aren't available, these are pretty much correct. 981 | FCGI_MAX_CONNS: 1, 982 | FCGI_MAX_REQS: 1, 983 | FCGI_MPXS_CONNS: 0 984 | } 985 | self._bindAddress = bindAddress 986 | 987 | def _setupSocket(self): 988 | if self._bindAddress is None: # Run as a normal FastCGI? 989 | isFCGI = True 990 | 991 | sock = socket.fromfd(FCGI_LISTENSOCK_FILENO, socket.AF_INET, 992 | socket.SOCK_STREAM) 993 | try: 994 | sock.getpeername() 995 | except socket.error, e: 996 | if e[0] == errno.ENOTSOCK: 997 | # Not a socket, assume CGI context. 998 | isFCGI = False 999 | elif e[0] != errno.ENOTCONN: 1000 | raise 1001 | 1002 | # FastCGI/CGI discrimination is broken on Mac OS X. 1003 | # Set the environment variable FCGI_FORCE_CGI to "Y" or "y" 1004 | # if you want to run your app as a simple CGI. (You can do 1005 | # this with Apache's mod_env [not loaded by default in OS X 1006 | # client, ha ha] and the SetEnv directive.) 1007 | if not isFCGI or \ 1008 | os.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'): 1009 | req = self.cgirequest_class(self) 1010 | req.run() 1011 | sys.exit(0) 1012 | else: 1013 | # Run as a server 1014 | if type(self._bindAddress) is str: 1015 | # Unix socket 1016 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 1017 | try: 1018 | os.unlink(self._bindAddress) 1019 | except OSError: 1020 | pass 1021 | else: 1022 | # INET socket 1023 | assert type(self._bindAddress) is tuple 1024 | assert len(self._bindAddress) == 2 1025 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1026 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 1027 | 1028 | sock.bind(self._bindAddress) 1029 | sock.listen(socket.SOMAXCONN) 1030 | 1031 | return sock 1032 | 1033 | def _cleanupSocket(self, sock): 1034 | """Closes the main socket.""" 1035 | sock.close() 1036 | 1037 | def _installSignalHandlers(self): 1038 | self._oldSIGs = [(x,signal.getsignal(x)) for x in 1039 | (signal.SIGHUP, signal.SIGINT, signal.SIGTERM)] 1040 | signal.signal(signal.SIGHUP, self._hupHandler) 1041 | signal.signal(signal.SIGINT, self._intHandler) 1042 | signal.signal(signal.SIGTERM, self._intHandler) 1043 | 1044 | def _restoreSignalHandlers(self): 1045 | for signum,handler in self._oldSIGs: 1046 | signal.signal(signum, handler) 1047 | 1048 | def _hupHandler(self, signum, frame): 1049 | self._hupReceived = True 1050 | self._keepGoing = False 1051 | 1052 | def _intHandler(self, signum, frame): 1053 | self._keepGoing = False 1054 | 1055 | def run(self, timeout=1.0): 1056 | """ 1057 | The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if 1058 | SIGHUP was received, False otherwise. 1059 | """ 1060 | web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS') 1061 | if web_server_addrs is not None: 1062 | web_server_addrs = map(lambda x: x.strip(), 1063 | web_server_addrs.split(',')) 1064 | 1065 | sock = self._setupSocket() 1066 | 1067 | self._keepGoing = True 1068 | self._hupReceived = False 1069 | 1070 | # Install signal handlers. 1071 | self._installSignalHandlers() 1072 | 1073 | while self._keepGoing: 1074 | try: 1075 | r, w, e = select.select([sock], [], [], timeout) 1076 | except select.error, e: 1077 | if e[0] == errno.EINTR: 1078 | continue 1079 | raise 1080 | 1081 | if r: 1082 | try: 1083 | clientSock, addr = sock.accept() 1084 | except socket.error, e: 1085 | if e[0] in (errno.EINTR, errno.EAGAIN): 1086 | continue 1087 | raise 1088 | 1089 | if web_server_addrs and \ 1090 | (len(addr) != 2 or addr[0] not in web_server_addrs): 1091 | clientSock.close() 1092 | continue 1093 | 1094 | # Instantiate a new Connection and begin processing FastCGI 1095 | # messages (either in a new thread or this thread). 1096 | conn = self._connectionClass(clientSock, addr, self) 1097 | thread.start_new_thread(conn.run, ()) 1098 | 1099 | self._mainloopPeriodic() 1100 | 1101 | # Restore signal handlers. 1102 | self._restoreSignalHandlers() 1103 | 1104 | self._cleanupSocket(sock) 1105 | 1106 | return self._hupReceived 1107 | 1108 | def _mainloopPeriodic(self): 1109 | """ 1110 | Called with just about each iteration of the main loop. Meant to 1111 | be overridden. 1112 | """ 1113 | pass 1114 | 1115 | def _exit(self, reload=False): 1116 | """ 1117 | Protected convenience method for subclasses to force an exit. Not 1118 | really thread-safe, which is why it isn't public. 1119 | """ 1120 | if self._keepGoing: 1121 | self._keepGoing = False 1122 | self._hupReceived = reload 1123 | 1124 | def handler(self, req): 1125 | """ 1126 | Default handler, which just raises an exception. Unless a handler 1127 | is passed at initialization time, this must be implemented by 1128 | a subclass. 1129 | """ 1130 | raise NotImplementedError, self.__class__.__name__ + '.handler' 1131 | 1132 | def error(self, req): 1133 | """ 1134 | Called by Request if an exception occurs within the handler. May and 1135 | should be overridden. 1136 | """ 1137 | import cgitb 1138 | req.stdout.write('Content-Type: text/html\r\n\r\n' + 1139 | cgitb.html(sys.exc_info())) 1140 | 1141 | class WSGIServer(Server): 1142 | """ 1143 | FastCGI server that supports the Web Server Gateway Interface. See 1144 | . 1145 | """ 1146 | def __init__(self, application, environ=None, multithreaded=True, **kw): 1147 | """ 1148 | environ, if present, must be a dictionary-like object. Its 1149 | contents will be copied into application's environ. Useful 1150 | for passing application-specific variables. 1151 | 1152 | Set multithreaded to False if your application is not MT-safe. 1153 | """ 1154 | if kw.has_key('handler'): 1155 | del kw['handler'] # Doesn't make sense to let this through 1156 | super(WSGIServer, self).__init__(**kw) 1157 | 1158 | if environ is None: 1159 | environ = {} 1160 | 1161 | self.application = application 1162 | self.environ = environ 1163 | self.multithreaded = multithreaded 1164 | 1165 | # Used to force single-threadedness 1166 | self._app_lock = thread.allocate_lock() 1167 | 1168 | def handler(self, req): 1169 | """Special handler for WSGI.""" 1170 | if req.role != FCGI_RESPONDER: 1171 | return FCGI_UNKNOWN_ROLE, 0 1172 | 1173 | # Mostly taken from example CGI gateway. 1174 | environ = req.params 1175 | environ.update(self.environ) 1176 | 1177 | environ['wsgi.version'] = (1,0) 1178 | environ['wsgi.input'] = req.stdin 1179 | if self._bindAddress is None: 1180 | stderr = req.stderr 1181 | else: 1182 | stderr = TeeOutputStream((sys.stderr, req.stderr)) 1183 | environ['wsgi.errors'] = stderr 1184 | environ['wsgi.multithread'] = not isinstance(req, CGIRequest) and \ 1185 | thread_available and self.multithreaded 1186 | # Rationale for the following: If started by the web server 1187 | # (self._bindAddress is None) in either FastCGI or CGI mode, the 1188 | # possibility of being spawned multiple times simultaneously is quite 1189 | # real. And, if started as an external server, multiple copies may be 1190 | # spawned for load-balancing/redundancy. (Though I don't think 1191 | # mod_fastcgi supports this?) 1192 | environ['wsgi.multiprocess'] = True 1193 | environ['wsgi.run_once'] = isinstance(req, CGIRequest) 1194 | 1195 | if environ.get('HTTPS', 'off') in ('on', '1'): 1196 | environ['wsgi.url_scheme'] = 'https' 1197 | else: 1198 | environ['wsgi.url_scheme'] = 'http' 1199 | 1200 | self._sanitizeEnv(environ) 1201 | 1202 | headers_set = [] 1203 | headers_sent = [] 1204 | result = None 1205 | 1206 | def write(data): 1207 | assert type(data) is str, 'write() argument must be string' 1208 | assert headers_set, 'write() before start_response()' 1209 | 1210 | if not headers_sent: 1211 | status, responseHeaders = headers_sent[:] = headers_set 1212 | found = False 1213 | for header,value in responseHeaders: 1214 | if header.lower() == 'content-length': 1215 | found = True 1216 | break 1217 | if not found and result is not None: 1218 | try: 1219 | if len(result) == 1: 1220 | responseHeaders.append(('Content-Length', 1221 | str(len(data)))) 1222 | except: 1223 | pass 1224 | s = 'Status: %s\r\n' % status 1225 | for header in responseHeaders: 1226 | s += '%s: %s\r\n' % header 1227 | s += '\r\n' 1228 | req.stdout.write(s) 1229 | 1230 | req.stdout.write(data) 1231 | req.stdout.flush() 1232 | 1233 | def start_response(status, response_headers, exc_info=None): 1234 | if exc_info: 1235 | try: 1236 | if headers_sent: 1237 | # Re-raise if too late 1238 | raise exc_info[0], exc_info[1], exc_info[2] 1239 | finally: 1240 | exc_info = None # avoid dangling circular ref 1241 | else: 1242 | assert not headers_set, 'Headers already set!' 1243 | 1244 | assert type(status) is str, 'Status must be a string' 1245 | assert len(status) >= 4, 'Status must be at least 4 characters' 1246 | assert int(status[:3]), 'Status must begin with 3-digit code' 1247 | assert status[3] == ' ', 'Status must have a space after code' 1248 | assert type(response_headers) is list, 'Headers must be a list' 1249 | if __debug__: 1250 | for name,val in response_headers: 1251 | assert type(name) is str, 'Header names must be strings' 1252 | assert type(val) is str, 'Header values must be strings' 1253 | 1254 | headers_set[:] = [status, response_headers] 1255 | return write 1256 | 1257 | if not self.multithreaded: 1258 | self._app_lock.acquire() 1259 | try: 1260 | result = self.application(environ, start_response) 1261 | try: 1262 | for data in result: 1263 | if data: 1264 | write(data) 1265 | if not headers_sent: 1266 | write('') # in case body was empty 1267 | finally: 1268 | if hasattr(result, 'close'): 1269 | result.close() 1270 | finally: 1271 | if not self.multithreaded: 1272 | self._app_lock.release() 1273 | 1274 | return FCGI_REQUEST_COMPLETE, 0 1275 | 1276 | def _sanitizeEnv(self, environ): 1277 | """Ensure certain values are present, if required by WSGI.""" 1278 | if not environ.has_key('SCRIPT_NAME'): 1279 | environ['SCRIPT_NAME'] = '' 1280 | if not environ.has_key('PATH_INFO'): 1281 | environ['PATH_INFO'] = '' 1282 | 1283 | # If any of these are missing, it probably signifies a broken 1284 | # server... 1285 | for name,default in [('REQUEST_METHOD', 'GET'), 1286 | ('SERVER_NAME', 'localhost'), 1287 | ('SERVER_PORT', '80'), 1288 | ('SERVER_PROTOCOL', 'HTTP/1.0')]: 1289 | if not environ.has_key(name): 1290 | environ['wsgi.errors'].write('%s: missing FastCGI param %s ' 1291 | 'required by WSGI!\n' % 1292 | (self.__class__.__name__, name)) 1293 | environ[name] = default 1294 | 1295 | if __name__ == '__main__': 1296 | def test_app(environ, start_response): 1297 | """Probably not the most efficient example.""" 1298 | import cgi 1299 | start_response('200 OK', [('Content-Type', 'text/html')]) 1300 | yield 'Hello World!\n' \ 1301 | '\n' \ 1302 | '

Hello World!

\n' \ 1303 | '' 1304 | names = environ.keys() 1305 | names.sort() 1306 | for name in names: 1307 | yield '\n' % ( 1308 | name, cgi.escape(`environ[name]`)) 1309 | 1310 | form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 1311 | keep_blank_values=1) 1312 | if form.list: 1313 | yield '' 1314 | 1315 | for field in form.list: 1316 | yield '\n' % ( 1317 | field.name, field.value) 1318 | 1319 | yield '
%s%s
Form data
%s%s
\n' \ 1320 | '\n' 1321 | 1322 | WSGIServer(test_app,multithreaded=False).run() 1323 | -------------------------------------------------------------------------------- /qweb/qweb.py: -------------------------------------------------------------------------------- 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 | 13 | QWeb is a python based [http://www.python.org/doc/peps/pep-0333/ WSGI] 14 | compatible web framework, it provides an infratructure to quickly build web 15 | applications 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 | 25 | QWeb applications are runnable in standalone mode (from commandline), via 26 | FastCGI, Regular CGI or by any python WSGI compliant server. 27 | 28 | QWeb doesn't provide any database access but it integrates nicely with ORMs 29 | such as SQLObject, SQLAlchemy or plain DB-API. 30 | 31 | Written by Antony Lesuisse (email al AT udev.org) 32 | 33 | Homepage: http://antony.lesuisse.org/qweb/trac/ 34 | 35 | Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum] 36 | 37 | == Quick Start (for Linux, MacOS X and cygwin) == 38 | 39 | Make 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 | 48 | And point your browser to http://localhost:8080/ 49 | 50 | You 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 | 71 | QWeb Components: 72 | ---------------- 73 | 74 | QWeb also feature a simple components api, that enables developers to easily 75 | produces reusable components. 76 | 77 | Default 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 | 86 | License 87 | ------- 88 | qweb/fcgi.py wich is BSD-like from saddi.com. 89 | Everything else is put in the public domain. 90 | 91 | 92 | TODO 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 | 108 | import BaseHTTPServer,SocketServer,Cookie 109 | import cgi,datetime,email,email.Message,errno,gzip,os,random,re,socket,sys,tempfile,time,types,urllib,urlparse,xml.dom 110 | try: 111 | import cPickle as pickle 112 | except ImportError: 113 | import pickle 114 | try: 115 | import cStringIO as StringIO 116 | except ImportError: 117 | import StringIO 118 | 119 | #---------------------------------------------------------- 120 | # Qweb Xml t-raw t-esc t-if t-foreach t-set t-call t-trim 121 | #---------------------------------------------------------- 122 | class 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 160 | class 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("%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 | #---------------------------------------------------------- 357 | class 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([''%(k,cgi.escape(str(v),1)) for k,v in tmp.items()]) 417 | return (p,r) 418 | class 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) 451 | class 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 555 | class 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 564 | class 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: 583 | # 584 | # 585 | # Simple forms: 586 | # 587 | # 588 | # 589 | # 590 | #