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 |
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 |
'%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
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
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)
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.
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":"" + objectName + ">\n");
621 | };
622 |
623 | /**
624 | * Escape the given string chacters that correspond to the five predefined XML entities
625 | * @param sXml the string to escape
626 | */
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 '