├── flup ├── __init__.py ├── client │ ├── __init__.py │ ├── scgi_app.py │ └── fcgi_app.py └── server │ ├── __init__.py │ ├── cgi.py │ ├── threadpool.py │ ├── singleserver.py │ ├── fcgi.py │ ├── fcgi_single.py │ ├── threadedserver.py │ ├── fcgi_fork.py │ ├── paste_factory.py │ ├── ajp_fork.py │ ├── scgi_fork.py │ ├── ajp.py │ ├── scgi.py │ ├── preforkserver.py │ ├── scgi_base.py │ └── ajp_base.py ├── MANIFEST.in ├── .hgignore ├── setup.cfg ├── docs ├── source │ ├── modules │ │ ├── flup.server.cgi.rst │ │ ├── flup.server.threadpool.rst │ │ ├── flup.server.paste_factory.rst │ │ ├── flup.server.singleserver.rst │ │ ├── flup.server.preforkserver.rst │ │ ├── flup.server.threadedserver.rst │ │ ├── flup.server.ajp_base.rst │ │ ├── flup.server.scgi.rst │ │ ├── flup.server.scgi_base.rst │ │ ├── flup.server.ajp.rst │ │ ├── flup.server.fcgi.rst │ │ ├── flup.server.fcgi_base.rst │ │ ├── flup.server.ajp_fork.rst │ │ ├── flup.server.scgi_fork.rst │ │ ├── flup.server.fcgi_fork.rst │ │ └── flup.server.fcgi_single.rst │ ├── index.rst │ └── conf.py └── Makefile ├── setup.py ├── README.md └── ChangeLog /flup/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /flup/client/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include docs/Makefile 2 | recursive-include docs *.py *.rst 3 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *~ 3 | __pycache__ 4 | build 5 | dist 6 | flup.egg-info 7 | -------------------------------------------------------------------------------- /flup/server/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | class NoDefault(object): 3 | pass 4 | 5 | __all__ = [ 'NoDefault', ] 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = .dev 3 | tag_date = true 4 | 5 | [build_sphinx] 6 | source-dir = docs/source 7 | build-dir = docs/build 8 | all-files = true 9 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.cgi.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.cgi` 2 | ====================== 3 | 4 | .. automodule:: flup.server.cgi 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.threadpool.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.threadpool` 2 | ============================= 3 | 4 | .. automodule:: flup.server.threadpool 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | 9 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.paste_factory.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.paste_factory` 2 | ================================ 3 | 4 | .. automodule:: flup.server.paste_factory 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | 9 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.singleserver.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.singleserver` 2 | =============================== 3 | 4 | .. automodule:: flup.server.singleserver 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.preforkserver.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.preforkserver` 2 | ================================ 3 | 4 | .. automodule:: flup.server.preforkserver 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.threadedserver.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.threadedserver` 2 | ================================= 3 | 4 | .. automodule:: flup.server.threadedserver 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.ajp_base.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.ajp_base` - ajp - an AJP 1.3/WSGI gateway 2 | =========================================================== 3 | 4 | .. automodule:: flup.server.ajp_base 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.scgi.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.scgi` - scgi - an SCGI/WSGI gateway (threaded) 2 | ================================================================ 3 | 4 | .. automodule:: flup.server.scgi 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.scgi_base.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.scgi_base` - scgi - an SCGI/WSGI gateway 2 | ========================================================== 3 | 4 | .. automodule:: flup.server.scgi_base 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.ajp.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.ajp` - ajp - an AJP 1.3/WSGI gateway (threaded) 2 | ================================================================= 3 | 4 | .. automodule:: flup.server.ajp 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | 9 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.fcgi.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.fcgi` - fcgi - a FastCGI/WSGI gateway (threaded) 2 | ================================================================== 3 | 4 | .. automodule:: flup.server.fcgi 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.fcgi_base.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.fcgi_base` - fcgi - a FastCGI/WSGI gateway 2 | ============================================================ 3 | 4 | .. automodule:: flup.server.fcgi_base 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.ajp_fork.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.ajp_fork` - ajp - an AJP 1.3/WSGI gateway (forking) 2 | ===================================================================== 3 | 4 | .. automodule:: flup.server.ajp_fork 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.scgi_fork.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.scgi_fork` - scgi - an SCGI/WSGI gateway (forking) 2 | ==================================================================== 3 | 4 | .. automodule:: flup.server.scgi_fork 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.fcgi_fork.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.fcgi_fork` - fcgi - a FastCGI/WSGI gateway (forking) 2 | ====================================================================== 3 | 4 | .. automodule:: flup.server.fcgi_fork 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/modules/flup.server.fcgi_single.rst: -------------------------------------------------------------------------------- 1 | :mod:`flup.server.fcgi_single` - fcgi - a FastCGI/WSGI gateway (single-threaded) 2 | ================================================================================ 3 | 4 | .. automodule:: flup.server.fcgi_single 5 | :members: 6 | :undoc-members: 7 | :inherited-members: 8 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to flup's documentation! 2 | ================================ 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | modules/flup.server.ajp_base 10 | modules/flup.server.ajp 11 | modules/flup.server.ajp_fork 12 | modules/flup.server.cgi 13 | modules/flup.server.fcgi_base 14 | modules/flup.server.fcgi 15 | modules/flup.server.fcgi_fork 16 | modules/flup.server.fcgi_single 17 | modules/flup.server.scgi_base 18 | modules/flup.server.scgi 19 | modules/flup.server.scgi_fork 20 | modules/flup.server.paste_factory 21 | modules/flup.server.threadedserver 22 | modules/flup.server.threadpool 23 | modules/flup.server.preforkserver 24 | modules/flup.server.singleserver 25 | 26 | 27 | Modules 28 | ======= 29 | 30 | 31 | Indices and tables 32 | ================== 33 | 34 | * :ref:`genindex` 35 | * :ref:`modindex` 36 | * :ref:`search` 37 | 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Bootstrap setuptools 2 | 3 | from setuptools import setup, find_packages 4 | setup( 5 | name = 'flup', 6 | version = '1.0.3', 7 | packages = find_packages(), 8 | zip_safe = True, 9 | 10 | entry_points = """ 11 | [paste.server_runner] 12 | ajp = flup.server.paste_factory:run_ajp_thread 13 | fcgi = flup.server.paste_factory:run_fcgi_thread 14 | scgi = flup.server.paste_factory:run_scgi_thread 15 | ajp_thread = flup.server.paste_factory:run_ajp_thread 16 | fcgi_thread = flup.server.paste_factory:run_fcgi_thread 17 | scgi_thread = flup.server.paste_factory:run_scgi_thread 18 | ajp_fork = flup.server.paste_factory:run_ajp_fork 19 | fcgi_fork = flup.server.paste_factory:run_fcgi_fork 20 | scgi_fork = flup.server.paste_factory:run_scgi_fork 21 | """, 22 | 23 | author = 'Allan Saddi', 24 | author_email = 'allan@saddi.com', 25 | description = 'Random assortment of WSGI servers', 26 | license = 'BSD', 27 | url='http://www.saddi.com/software/flup/', 28 | classifiers = [ 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Environment :: Web Environment', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python', 35 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 36 | 'Topic :: Software Development :: Libraries :: Python Modules', 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flup-py3 for Python 3.4+ 2 | 3 | [flup-py3](https://hg.saddi.com/flup-py3.0/) fork and updated for Python 3.4+. 4 | 5 | Please note that [WSGI](https://www.python.org/dev/peps/pep-0333/) is the preferable way for 6 | Python based web applications. 7 | 8 | ## Installation 9 | 10 | You may install this package by using [pip] for Python 3. Optionally with the help 11 | of [venv]. 12 | 13 | The actual command is different among operation systems. For example, 14 | [Debian / Ubuntu package][python3-pip3] name it as **pip3**. 15 | 16 | Add this line to your `requirements.txt`: 17 | 18 | ```text 19 | flup-py3 20 | ``` 21 | 22 | Then run this command (assuming **pip3**): 23 | 24 | ```text 25 | pip3 install -r requirements.txt 26 | ``` 27 | 28 | [pip]: https://pypi.python.org/pypi/pip 29 | [venv]: https://docs.python.org/3/library/venv.html 30 | [python3-pip3]: https://packages.debian.org/jessie/python/python3-pip 31 | 32 | ## Usage 33 | 34 | A simple hello world app (reference: [Python 3.4 Documentations][webserver]) 35 | 36 | ```python 37 | import sys, os, logging 38 | from html import escape 39 | from flup.server.fcgi import WSGIServer 40 | 41 | def app(environ, start_response): 42 | start_response('200 OK', [('Content-Type', 'text/html')]) 43 | yield "hello world" 44 | 45 | def main(): 46 | try: 47 | WSGIServer(app, bindAddress='./hello-world.sock', umask=0000).run() 48 | except (KeyboardInterrupt, SystemExit, SystemError): 49 | logging.info("Shutdown requested...exiting") 50 | except Exception: 51 | traceback.print_exc(file=sys.stdout) 52 | ``` 53 | 54 | [webserver]: https://docs.python.org/3.4/howto/webservers.html 55 | 56 | ## Maintenance 57 | 58 | Github Repository: [github.com/pquentin/flup-py3][github] 59 | 60 | [github]: github.com/pquentin/flup-py3 61 | -------------------------------------------------------------------------------- /flup/server/cgi.py: -------------------------------------------------------------------------------- 1 | # Taken from 2 | # which was placed in the public domain. 3 | 4 | import os, sys 5 | 6 | 7 | __all__ = ['WSGIServer'] 8 | 9 | 10 | class WSGIServer(object): 11 | 12 | def __init__(self, application): 13 | self.application = application 14 | 15 | def run(self): 16 | 17 | environ = dict(list(os.environ.items())) 18 | environ['wsgi.input'] = sys.stdin 19 | environ['wsgi.errors'] = sys.stderr 20 | environ['wsgi.version'] = (1,0) 21 | environ['wsgi.multithread'] = False 22 | environ['wsgi.multiprocess'] = True 23 | environ['wsgi.run_once'] = True 24 | 25 | if environ.get('HTTPS','off') in ('on','1'): 26 | environ['wsgi.url_scheme'] = 'https' 27 | else: 28 | environ['wsgi.url_scheme'] = 'http' 29 | 30 | headers_set = [] 31 | headers_sent = [] 32 | 33 | def write(data): 34 | if not headers_set: 35 | raise AssertionError("write() before start_response()") 36 | 37 | elif not headers_sent: 38 | # Before the first output, send the stored headers 39 | status, response_headers = headers_sent[:] = headers_set 40 | sys.stdout.write('Status: %s\r\n' % status) 41 | for header in response_headers: 42 | sys.stdout.write('%s: %s\r\n' % header) 43 | sys.stdout.write('\r\n') 44 | 45 | sys.stdout.write(data) 46 | sys.stdout.flush() 47 | 48 | def start_response(status,response_headers,exc_info=None): 49 | if exc_info: 50 | try: 51 | if headers_sent: 52 | # Re-raise original exception if headers sent 53 | raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) 54 | finally: 55 | exc_info = None # avoid dangling circular ref 56 | elif headers_set: 57 | raise AssertionError("Headers already set!") 58 | 59 | headers_set[:] = [status,response_headers] 60 | return write 61 | 62 | result = self.application(environ, start_response) 63 | try: 64 | for data in result: 65 | if data: # don't send headers until body appears 66 | write(data) 67 | if not headers_sent: 68 | write('') # send headers now if body was empty 69 | finally: 70 | if hasattr(result,'close'): 71 | result.close() 72 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 13 | 14 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " dirhtml to make HTML files named index.html in directories" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " qthelp to make HTML files and a qthelp project" 24 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 25 | @echo " changes to make an overview of all changed/added/deprecated items" 26 | @echo " linkcheck to check all external links for integrity" 27 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 28 | 29 | clean: 30 | -rm -rf build/* 31 | 32 | html: 33 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html 34 | @echo 35 | @echo "Build finished. The HTML pages are in build/html." 36 | 37 | dirhtml: 38 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml 39 | @echo 40 | @echo "Build finished. The HTML pages are in build/dirhtml." 41 | 42 | pickle: 43 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle 44 | @echo 45 | @echo "Build finished; now you can process the pickle files." 46 | 47 | json: 48 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json 49 | @echo 50 | @echo "Build finished; now you can process the JSON files." 51 | 52 | htmlhelp: 53 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp 54 | @echo 55 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 56 | ".hhp project file in build/htmlhelp." 57 | 58 | qthelp: 59 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp 60 | @echo 61 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 62 | ".qhcp project file in build/qthelp, like this:" 63 | @echo "# qcollectiongenerator build/qthelp/flup.qhcp" 64 | @echo "To view the help file:" 65 | @echo "# assistant -collectionFile build/qthelp/flup.qhc" 66 | 67 | latex: 68 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex 69 | @echo 70 | @echo "Build finished; the LaTeX files are in build/latex." 71 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 72 | "run these through (pdf)latex." 73 | 74 | changes: 75 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes 76 | @echo 77 | @echo "The overview file is in build/changes." 78 | 79 | linkcheck: 80 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck 81 | @echo 82 | @echo "Link check complete; look for any errors in the above output " \ 83 | "or in build/linkcheck/output.txt." 84 | 85 | doctest: 86 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest 87 | @echo "Testing of doctests in the sources finished, look at the " \ 88 | "results in build/doctest/output.txt." 89 | -------------------------------------------------------------------------------- /flup/server/threadpool.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | __author__ = 'Allan Saddi ' 28 | __version__ = '$Revision$' 29 | 30 | import sys 31 | try: 32 | import thread 33 | except ImportError: 34 | import _thread as thread 35 | import threading 36 | 37 | class ThreadPool(object): 38 | """ 39 | Thread pool that maintains the number of idle threads between 40 | minSpare and maxSpare inclusive. By default, there is no limit on 41 | the number of threads that can be started, but this can be controlled 42 | by maxThreads. 43 | """ 44 | def __init__(self, minSpare=1, maxSpare=5, maxThreads=sys.maxsize): 45 | self._minSpare = minSpare 46 | self._maxSpare = maxSpare 47 | self._maxThreads = max(minSpare, maxThreads) 48 | 49 | self._lock = threading.Condition() 50 | self._workQueue = [] 51 | self._idleCount = self._workerCount = maxSpare 52 | 53 | self._threads = [] 54 | self._stop = False 55 | 56 | # Start the minimum number of worker threads. 57 | for i in range(maxSpare): 58 | self._start_new_thread() 59 | 60 | def _start_new_thread(self): 61 | t = threading.Thread(target=self._worker) 62 | self._threads.append(t) 63 | t.setDaemon(True) 64 | t.start() 65 | return t 66 | 67 | def shutdown(self): 68 | """shutdown all workers.""" 69 | self._lock.acquire() 70 | self._stop = True 71 | self._lock.notifyAll() 72 | self._lock.release() 73 | 74 | # wait for all threads to finish 75 | for t in self._threads[:]: 76 | t.join() 77 | 78 | def addJob(self, job, allowQueuing=True): 79 | """ 80 | Adds a job to the work queue. The job object should have a run() 81 | method. If allowQueuing is True (the default), the job will be 82 | added to the work queue regardless if there are any idle threads 83 | ready. (The only way for there to be no idle threads is if maxThreads 84 | is some reasonable, finite limit.) 85 | 86 | Otherwise, if allowQueuing is False, and there are no more idle 87 | threads, the job will not be queued. 88 | 89 | Returns True if the job was queued, False otherwise. 90 | """ 91 | self._lock.acquire() 92 | try: 93 | # Maintain minimum number of spares. 94 | while self._idleCount < self._minSpare and \ 95 | self._workerCount < self._maxThreads: 96 | try: 97 | self._start_new_thread() 98 | except thread.error: 99 | return False 100 | self._workerCount += 1 101 | self._idleCount += 1 102 | 103 | # Hand off the job. 104 | if self._idleCount or allowQueuing: 105 | self._workQueue.append(job) 106 | self._lock.notify() 107 | return True 108 | else: 109 | return False 110 | finally: 111 | self._lock.release() 112 | 113 | def _worker(self): 114 | """ 115 | Worker thread routine. Waits for a job, executes it, repeat. 116 | """ 117 | self._lock.acquire() 118 | try: 119 | while True: 120 | while not self._workQueue and not self._stop: 121 | self._lock.wait() 122 | 123 | if self._stop: 124 | return 125 | 126 | # We have a job to do... 127 | job = self._workQueue.pop(0) 128 | 129 | assert self._idleCount > 0 130 | self._idleCount -= 1 131 | 132 | self._lock.release() 133 | 134 | try: 135 | job.run() 136 | except: 137 | # FIXME: This should really be reported somewhere. 138 | # But we can't simply report it to stderr because of fcgi 139 | pass 140 | 141 | self._lock.acquire() 142 | 143 | if self._idleCount == self._maxSpare: 144 | break # NB: lock still held 145 | self._idleCount += 1 146 | assert self._idleCount <= self._maxSpare 147 | 148 | # Die off... 149 | assert self._workerCount > self._maxSpare 150 | self._threads.remove(threading.currentThread()) 151 | self._workerCount -= 1 152 | finally: 153 | self._lock.release() 154 | -------------------------------------------------------------------------------- /flup/server/singleserver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | __author__ = 'Allan Saddi ' 28 | __version__ = '$Revision$' 29 | 30 | import sys 31 | import socket 32 | import select 33 | import signal 34 | import errno 35 | 36 | try: 37 | import fcntl 38 | except ImportError: 39 | def setCloseOnExec(sock): 40 | pass 41 | else: 42 | def setCloseOnExec(sock): 43 | fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC) 44 | 45 | __all__ = ['SingleServer'] 46 | 47 | class SingleServer(object): 48 | def __init__(self, jobClass=None, jobArgs=(), **kw): 49 | self._jobClass = jobClass 50 | self._jobArgs = jobArgs 51 | 52 | def run(self, sock, timeout=1.0): 53 | """ 54 | The main loop. Pass a socket that is ready to accept() client 55 | connections. Return value will be True or False indiciating whether 56 | or not the loop was exited due to SIGHUP. 57 | """ 58 | # Set up signal handlers. 59 | self._keepGoing = True 60 | self._hupReceived = False 61 | 62 | # Might need to revisit this? 63 | if not sys.platform.startswith('win'): 64 | self._installSignalHandlers() 65 | 66 | # Set close-on-exec 67 | setCloseOnExec(sock) 68 | 69 | # Main loop. 70 | while self._keepGoing: 71 | try: 72 | r, w, e = select.select([sock], [], [], timeout) 73 | except select.error as e: 74 | if e.args[0] == errno.EINTR: 75 | continue 76 | raise 77 | 78 | if r: 79 | try: 80 | clientSock, addr = sock.accept() 81 | except socket.error as e: 82 | if e.args[0] in (errno.EINTR, errno.EAGAIN): 83 | continue 84 | raise 85 | 86 | setCloseOnExec(clientSock) 87 | 88 | if not self._isClientAllowed(addr): 89 | clientSock.close() 90 | continue 91 | 92 | # Hand off to Connection. 93 | conn = self._jobClass(clientSock, addr, *self._jobArgs) 94 | conn.run() 95 | 96 | self._mainloopPeriodic() 97 | 98 | # Restore signal handlers. 99 | self._restoreSignalHandlers() 100 | 101 | # Return bool based on whether or not SIGHUP was received. 102 | return self._hupReceived 103 | 104 | def _mainloopPeriodic(self): 105 | """ 106 | Called with just about each iteration of the main loop. Meant to 107 | be overridden. 108 | """ 109 | pass 110 | 111 | def _exit(self, reload=False): 112 | """ 113 | Protected convenience method for subclasses to force an exit. Not 114 | really thread-safe, which is why it isn't public. 115 | """ 116 | if self._keepGoing: 117 | self._keepGoing = False 118 | self._hupReceived = reload 119 | 120 | def _isClientAllowed(self, addr): 121 | """Override to provide access control.""" 122 | return True 123 | 124 | # Signal handlers 125 | 126 | def _hupHandler(self, signum, frame): 127 | self._hupReceived = True 128 | self._keepGoing = False 129 | 130 | def _intHandler(self, signum, frame): 131 | self._keepGoing = False 132 | 133 | def _installSignalHandlers(self): 134 | supportedSignals = [signal.SIGINT, signal.SIGTERM] 135 | if hasattr(signal, 'SIGHUP'): 136 | supportedSignals.append(signal.SIGHUP) 137 | 138 | self._oldSIGs = [(x,signal.getsignal(x)) for x in supportedSignals] 139 | 140 | for sig in supportedSignals: 141 | if hasattr(signal, 'SIGHUP') and sig == signal.SIGHUP: 142 | signal.signal(sig, self._hupHandler) 143 | else: 144 | signal.signal(sig, self._intHandler) 145 | 146 | def _restoreSignalHandlers(self): 147 | for signum,handler in self._oldSIGs: 148 | signal.signal(signum, handler) 149 | 150 | if __name__ == '__main__': 151 | class TestJob(object): 152 | def __init__(self, sock, addr): 153 | self._sock = sock 154 | self._addr = addr 155 | def run(self): 156 | print("Client connection opened from %s:%d" % self._addr) 157 | self._sock.send('Hello World!\n') 158 | self._sock.setblocking(1) 159 | self._sock.recv(1) 160 | self._sock.close() 161 | print("Client connection closed from %s:%d" % self._addr) 162 | sock = socket.socket() 163 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 164 | sock.bind(('', 8080)) 165 | sock.listen(socket.SOMAXCONN) 166 | SingleServer(jobClass=TestJob).run(sock) 167 | -------------------------------------------------------------------------------- /flup/server/fcgi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005, 2006 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | """ 28 | fcgi - a FastCGI/WSGI gateway. 29 | 30 | For more information about FastCGI, see . 31 | 32 | For more information about the Web Server Gateway Interface, see 33 | . 34 | 35 | Example usage: 36 | 37 | #!/usr/bin/env python 38 | from myapplication import app # Assume app is your WSGI application object 39 | from fcgi import WSGIServer 40 | WSGIServer(app).run() 41 | 42 | See the documentation for WSGIServer for more information. 43 | 44 | On most platforms, fcgi will fallback to regular CGI behavior if run in a 45 | non-FastCGI context. If you want to force CGI behavior, set the environment 46 | variable FCGI_FORCE_CGI to "Y" or "y". 47 | """ 48 | 49 | __author__ = 'Allan Saddi ' 50 | __version__ = '$Revision$' 51 | 52 | import os 53 | 54 | from .fcgi_base import BaseFCGIServer, FCGI_RESPONDER 55 | from .threadedserver import ThreadedServer 56 | 57 | __all__ = ['WSGIServer'] 58 | 59 | class WSGIServer(BaseFCGIServer, ThreadedServer): 60 | """ 61 | FastCGI server that supports the Web Server Gateway Interface. See 62 | . 63 | """ 64 | def __init__(self, application, environ=None, 65 | multithreaded=True, multiprocess=False, 66 | bindAddress=None, umask=None, multiplexed=False, 67 | debug=False, roles=(FCGI_RESPONDER,), forceCGI=False, **kw): 68 | """ 69 | environ, if present, must be a dictionary-like object. Its 70 | contents will be copied into application's environ. Useful 71 | for passing application-specific variables. 72 | 73 | bindAddress, if present, must either be a string or a 2-tuple. If 74 | present, run() will open its own listening socket. You would use 75 | this if you wanted to run your application as an 'external' FastCGI 76 | app. (i.e. the webserver would no longer be responsible for starting 77 | your app) If a string, it will be interpreted as a filename and a UNIX 78 | socket will be opened. If a tuple, the first element, a string, 79 | is the interface name/IP to bind to, and the second element (an int) 80 | is the port number. 81 | """ 82 | BaseFCGIServer.__init__(self, application, 83 | environ=environ, 84 | multithreaded=multithreaded, 85 | multiprocess=multiprocess, 86 | bindAddress=bindAddress, 87 | umask=umask, 88 | multiplexed=multiplexed, 89 | debug=debug, 90 | roles=roles, 91 | forceCGI=forceCGI) 92 | for key in ('jobClass', 'jobArgs'): 93 | if key in kw: 94 | del kw[key] 95 | ThreadedServer.__init__(self, jobClass=self._connectionClass, 96 | jobArgs=(self, None), **kw) 97 | 98 | def _isClientAllowed(self, addr): 99 | return self._web_server_addrs is None or \ 100 | (len(addr) == 2 and addr[0] in self._web_server_addrs) 101 | 102 | def run(self): 103 | """ 104 | The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if 105 | SIGHUP was received, False otherwise. 106 | """ 107 | self._web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS') 108 | if self._web_server_addrs is not None: 109 | self._web_server_addrs = [x.strip() for x in self._web_server_addrs.split(',')] 110 | 111 | sock = self._setupSocket() 112 | 113 | ret = ThreadedServer.run(self, sock) 114 | 115 | self._cleanupSocket(sock) 116 | self.shutdown() 117 | 118 | return ret 119 | 120 | if __name__ == '__main__': 121 | def test_app(environ, start_response): 122 | """Probably not the most efficient example.""" 123 | import cgi 124 | start_response('200 OK', [('Content-Type', 'text/html')]) 125 | yield 'Hello World!\n' \ 126 | '\n' \ 127 | '

Hello World!

\n' \ 128 | '' 129 | names = list(environ.keys()) 130 | names.sort() 131 | for name in names: 132 | yield '\n' % ( 133 | name, cgi.escape(repr(environ[name]))) 134 | 135 | form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 136 | keep_blank_values=1) 137 | if form.list: 138 | yield '' 139 | 140 | for field in form.list: 141 | yield '\n' % ( 142 | field.name, field.value) 143 | 144 | yield '
%s%s
Form data
%s%s
\n' \ 145 | '\n' 146 | 147 | from wsgiref import validate 148 | test_app = validate.validator(test_app) 149 | WSGIServer(test_app).run() 150 | -------------------------------------------------------------------------------- /flup/server/fcgi_single.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005, 2006 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | """ 28 | fcgi - a FastCGI/WSGI gateway. 29 | 30 | For more information about FastCGI, see . 31 | 32 | For more information about the Web Server Gateway Interface, see 33 | . 34 | 35 | Example usage: 36 | 37 | #!/usr/bin/env python 38 | from myapplication import app # Assume app is your WSGI application object 39 | from fcgi import WSGIServer 40 | WSGIServer(app).run() 41 | 42 | See the documentation for WSGIServer for more information. 43 | 44 | On most platforms, fcgi will fallback to regular CGI behavior if run in a 45 | non-FastCGI context. If you want to force CGI behavior, set the environment 46 | variable FCGI_FORCE_CGI to "Y" or "y". 47 | """ 48 | 49 | __author__ = 'Allan Saddi ' 50 | __version__ = '$Revision$' 51 | 52 | import os 53 | 54 | from .fcgi_base import BaseFCGIServer, FCGI_RESPONDER, \ 55 | FCGI_MAX_CONNS, FCGI_MAX_REQS, FCGI_MPXS_CONNS 56 | from .singleserver import SingleServer 57 | 58 | __all__ = ['WSGIServer'] 59 | 60 | class WSGIServer(BaseFCGIServer, SingleServer): 61 | """ 62 | FastCGI server that supports the Web Server Gateway Interface. See 63 | . 64 | """ 65 | def __init__(self, application, environ=None, 66 | bindAddress=None, umask=None, multiplexed=False, 67 | debug=False, roles=(FCGI_RESPONDER,), forceCGI=False, **kw): 68 | """ 69 | environ, if present, must be a dictionary-like object. Its 70 | contents will be copied into application's environ. Useful 71 | for passing application-specific variables. 72 | 73 | bindAddress, if present, must either be a string or a 2-tuple. If 74 | present, run() will open its own listening socket. You would use 75 | this if you wanted to run your application as an 'external' FastCGI 76 | app. (i.e. the webserver would no longer be responsible for starting 77 | your app) If a string, it will be interpreted as a filename and a UNIX 78 | socket will be opened. If a tuple, the first element, a string, 79 | is the interface name/IP to bind to, and the second element (an int) 80 | is the port number. 81 | """ 82 | BaseFCGIServer.__init__(self, application, 83 | environ=environ, 84 | multithreaded=False, 85 | multiprocess=False, 86 | bindAddress=bindAddress, 87 | umask=umask, 88 | multiplexed=multiplexed, 89 | debug=debug, 90 | roles=roles, 91 | forceCGI=forceCGI) 92 | for key in ('jobClass', 'jobArgs'): 93 | if key in kw: 94 | del kw[key] 95 | SingleServer.__init__(self, jobClass=self._connectionClass, 96 | jobArgs=(self, None), **kw) 97 | self.capability = { 98 | FCGI_MAX_CONNS: 1, 99 | FCGI_MAX_REQS: 1, 100 | FCGI_MPXS_CONNS: 0 101 | } 102 | 103 | def _isClientAllowed(self, addr): 104 | return self._web_server_addrs is None or \ 105 | (len(addr) == 2 and addr[0] in self._web_server_addrs) 106 | 107 | def run(self): 108 | """ 109 | The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if 110 | SIGHUP was received, False otherwise. 111 | """ 112 | self._web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS') 113 | if self._web_server_addrs is not None: 114 | self._web_server_addrs = [x.strip() for x in self._web_server_addrs.split(',')] 115 | 116 | sock = self._setupSocket() 117 | 118 | ret = SingleServer.run(self, sock) 119 | 120 | self._cleanupSocket(sock) 121 | 122 | return ret 123 | 124 | if __name__ == '__main__': 125 | def test_app(environ, start_response): 126 | """Probably not the most efficient example.""" 127 | from . import cgi 128 | start_response('200 OK', [('Content-Type', 'text/html')]) 129 | yield 'Hello World!\n' \ 130 | '\n' \ 131 | '

Hello World!

\n' \ 132 | '' 133 | names = list(environ.keys()) 134 | names.sort() 135 | for name in names: 136 | yield '\n' % ( 137 | name, cgi.escape(repr(environ[name]))) 138 | 139 | form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 140 | keep_blank_values=1) 141 | if form.list: 142 | yield '' 143 | 144 | for field in form.list: 145 | yield '\n' % ( 146 | field.name, field.value) 147 | 148 | yield '
%s%s
Form data
%s%s
\n' \ 149 | '\n' 150 | 151 | from wsgiref import validate 152 | test_app = validate.validator(test_app) 153 | WSGIServer(test_app).run() 154 | -------------------------------------------------------------------------------- /flup/client/scgi_app.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2006 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | __author__ = 'Allan Saddi ' 28 | __version__ = '$Revision$' 29 | 30 | import select 31 | import struct 32 | import socket 33 | import errno 34 | import types 35 | 36 | __all__ = ['SCGIApp'] 37 | 38 | def encodeNetstring(s): 39 | return b''.join([str(len(s)).encode('latin-1'), b':', s, b',']) 40 | 41 | class SCGIApp(object): 42 | def __init__(self, connect=None, host=None, port=None, 43 | filterEnviron=True): 44 | if host is not None: 45 | assert port is not None 46 | connect=(host, port) 47 | 48 | assert connect is not None 49 | self._connect = connect 50 | 51 | self._filterEnviron = filterEnviron 52 | 53 | def __call__(self, environ, start_response): 54 | sock = self._getConnection() 55 | 56 | outfile = sock.makefile('wb') 57 | infile = sock.makefile('rb') 58 | 59 | sock.close() 60 | 61 | # Filter WSGI environ and send as request headers 62 | if self._filterEnviron: 63 | headers = self._defaultFilterEnviron(environ) 64 | else: 65 | headers = self._lightFilterEnviron(environ) 66 | # TODO: Anything not from environ that needs to be sent also? 67 | 68 | content_length = int(environ.get('CONTENT_LENGTH') or 0) 69 | if 'CONTENT_LENGTH' in headers: 70 | del headers['CONTENT_LENGTH'] 71 | 72 | headers_out = [b'CONTENT_LENGTH', str(content_length).encode('latin-1'), b'SCGI', b'1'] 73 | for k,v in list(headers.items()): 74 | headers_out.append(k.encode('latin-1')) 75 | headers_out.append(v.encode('latin-1')) 76 | headers_out.append(b'') # For trailing NUL 77 | outfile.write(encodeNetstring(b'\x00'.join(headers_out))) 78 | 79 | # Transfer wsgi.input to outfile 80 | while True: 81 | chunk_size = min(content_length, 4096) 82 | s = environ['wsgi.input'].read(chunk_size) 83 | content_length -= len(s) 84 | outfile.write(s) 85 | 86 | if not s: break 87 | 88 | outfile.close() 89 | 90 | # Read result from SCGI server 91 | result = [] 92 | while True: 93 | buf = infile.read(4096) 94 | if not buf: break 95 | 96 | result.append(buf) 97 | 98 | infile.close() 99 | 100 | result = b''.join(result) 101 | 102 | # Parse response headers 103 | status = b'200 OK' 104 | headers = [] 105 | pos = 0 106 | while True: 107 | eolpos = result.find(b'\n', pos) 108 | if eolpos < 0: break 109 | line = result[pos:eolpos-1] 110 | pos = eolpos + 1 111 | 112 | # strip in case of CR. NB: This will also strip other 113 | # whitespace... 114 | line = line.strip() 115 | 116 | # Empty line signifies end of headers 117 | if not line: break 118 | 119 | # TODO: Better error handling 120 | header, value = line.split(b':', 1) 121 | header = header.strip().lower() 122 | value = value.strip() 123 | 124 | if header == b'status': 125 | # Special handling of Status header 126 | status = value 127 | if status.find(b' ') < 0: 128 | # Append a dummy reason phrase if one was not provided 129 | status += b' SCGIApp' 130 | else: 131 | headers.append((header, value)) 132 | 133 | result = result[pos:] 134 | 135 | # Set WSGI status, headers, and return result. 136 | start_response(status, headers) 137 | return [result] 138 | 139 | def _getConnection(self): 140 | if isinstance(self._connect, types.StringTypes): 141 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 142 | sock.connect(self._connect) 143 | elif hasattr(socket, 'create_connection'): 144 | sock = socket.create_connection(self._connect) 145 | else: 146 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 147 | sock.connect(self._connect) 148 | return sock 149 | 150 | _environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_', 151 | 'CONTENT_'] 152 | _environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE'] 153 | _environRenames = {} 154 | 155 | def _defaultFilterEnviron(self, environ): 156 | result = {} 157 | for n in list(environ.keys()): 158 | for p in self._environPrefixes: 159 | if n.startswith(p): 160 | result[n] = environ[n] 161 | if n in self._environCopies: 162 | result[n] = environ[n] 163 | if n in self._environRenames: 164 | result[self._environRenames[n]] = environ[n] 165 | 166 | return result 167 | 168 | def _lightFilterEnviron(self, environ): 169 | result = {} 170 | for n in list(environ.keys()): 171 | if n.upper() == n: 172 | result[n] = environ[n] 173 | return result 174 | 175 | if __name__ == '__main__': 176 | from flup.server.ajp import WSGIServer 177 | app = SCGIApp(connect=('localhost', 4000)) 178 | #import paste.lint 179 | #app = paste.lint.middleware(app) 180 | WSGIServer(app).run() 181 | -------------------------------------------------------------------------------- /flup/server/threadedserver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | __author__ = 'Allan Saddi ' 28 | __version__ = '$Revision$' 29 | 30 | import sys 31 | import socket 32 | import select 33 | import signal 34 | import errno 35 | 36 | try: 37 | import fcntl 38 | except ImportError: 39 | def setCloseOnExec(sock): 40 | pass 41 | else: 42 | def setCloseOnExec(sock): 43 | fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC) 44 | 45 | from .threadpool import ThreadPool 46 | 47 | __all__ = ['ThreadedServer'] 48 | 49 | class ThreadedServer(object): 50 | def __init__(self, jobClass=None, jobArgs=(), **kw): 51 | self._jobClass = jobClass 52 | self._jobArgs = jobArgs 53 | 54 | self._threadPool = ThreadPool(**kw) 55 | 56 | def run(self, sock, timeout=1.0): 57 | """ 58 | The main loop. Pass a socket that is ready to accept() client 59 | connections. Return value will be True or False indiciating whether 60 | or not the loop was exited due to SIGHUP. 61 | """ 62 | # Set up signal handlers. 63 | self._keepGoing = True 64 | self._hupReceived = False 65 | 66 | # Might need to revisit this? 67 | if not sys.platform.startswith('win'): 68 | self._installSignalHandlers() 69 | 70 | # Set close-on-exec 71 | setCloseOnExec(sock) 72 | 73 | # Main loop. 74 | while self._keepGoing: 75 | try: 76 | r, w, e = select.select([sock], [], [], timeout) 77 | except select.error as e: 78 | if e.args[0] == errno.EINTR: 79 | continue 80 | raise 81 | 82 | if r: 83 | try: 84 | clientSock, addr = sock.accept() 85 | except socket.error as e: 86 | if e.args[0] in (errno.EINTR, errno.EAGAIN): 87 | continue 88 | raise 89 | 90 | setCloseOnExec(clientSock) 91 | 92 | if not self._isClientAllowed(addr): 93 | clientSock.close() 94 | continue 95 | 96 | # Hand off to Connection. 97 | conn = self._jobClass(clientSock, addr, *self._jobArgs) 98 | if not self._threadPool.addJob(conn, allowQueuing=False): 99 | # No thread left, immediately close the socket to hopefully 100 | # indicate to the web server that we're at our limit... 101 | # and to prevent having too many opened (and useless) 102 | # files. 103 | clientSock.close() 104 | 105 | self._mainloopPeriodic() 106 | 107 | # Restore signal handlers. 108 | if not sys.platform.startswith('win'): 109 | self._restoreSignalHandlers() 110 | 111 | # Return bool based on whether or not SIGHUP was received. 112 | return self._hupReceived 113 | 114 | def shutdown(self): 115 | """Wait for running threads to finish.""" 116 | self._threadPool.shutdown() 117 | 118 | def _mainloopPeriodic(self): 119 | """ 120 | Called with just about each iteration of the main loop. Meant to 121 | be overridden. 122 | """ 123 | pass 124 | 125 | def _exit(self, reload=False): 126 | """ 127 | Protected convenience method for subclasses to force an exit. Not 128 | really thread-safe, which is why it isn't public. 129 | """ 130 | if self._keepGoing: 131 | self._keepGoing = False 132 | self._hupReceived = reload 133 | 134 | def _isClientAllowed(self, addr): 135 | """Override to provide access control.""" 136 | return True 137 | 138 | # Signal handlers 139 | 140 | def _hupHandler(self, signum, frame): 141 | self._hupReceived = True 142 | self._keepGoing = False 143 | 144 | def _intHandler(self, signum, frame): 145 | self._keepGoing = False 146 | 147 | def _installSignalHandlers(self): 148 | supportedSignals = [signal.SIGINT, signal.SIGTERM] 149 | if hasattr(signal, 'SIGHUP'): 150 | supportedSignals.append(signal.SIGHUP) 151 | 152 | self._oldSIGs = [(x,signal.getsignal(x)) for x in supportedSignals] 153 | 154 | for sig in supportedSignals: 155 | if hasattr(signal, 'SIGHUP') and sig == signal.SIGHUP: 156 | signal.signal(sig, self._hupHandler) 157 | else: 158 | signal.signal(sig, self._intHandler) 159 | 160 | def _restoreSignalHandlers(self): 161 | for signum,handler in self._oldSIGs: 162 | signal.signal(signum, handler) 163 | 164 | if __name__ == '__main__': 165 | class TestJob(object): 166 | def __init__(self, sock, addr): 167 | self._sock = sock 168 | self._addr = addr 169 | def run(self): 170 | print("Client connection opened from %s:%d" % self._addr) 171 | self._sock.send('Hello World!\n') 172 | self._sock.setblocking(1) 173 | self._sock.recv(1) 174 | self._sock.close() 175 | print("Client connection closed from %s:%d" % self._addr) 176 | sock = socket.socket() 177 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 178 | sock.bind(('', 8080)) 179 | sock.listen(socket.SOMAXCONN) 180 | ThreadedServer(maxThreads=10, jobClass=TestJob).run(sock) 181 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # flup documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Jul 6 10:53:43 2009. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.append(os.path.abspath('.')) 20 | sys.path.insert(0, (os.path.abspath('../..'))) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = ['sphinx.ext.autodoc'] 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ['_templates'] 30 | 31 | # The suffix of source filenames. 32 | source_suffix = '.rst' 33 | 34 | # The encoding of source files. 35 | #source_encoding = 'utf-8' 36 | 37 | # The master toctree document. 38 | master_doc = 'index' 39 | 40 | # General information about the project. 41 | project = u'flup' 42 | copyright = u'2009, Allan Saddi' 43 | 44 | # The version info for the project you're documenting, acts as replacement for 45 | # |version| and |release|, also used in various other places throughout the 46 | # built documents. 47 | # 48 | # The short X.Y version. 49 | version = '1.0.2' 50 | # The full version, including alpha/beta/rc tags. 51 | release = '1.0.2' 52 | 53 | # The language for content autogenerated by Sphinx. Refer to documentation 54 | # for a list of supported languages. 55 | #language = None 56 | 57 | # There are two options for replacing |today|: either, you set today to some 58 | # non-false value, then it is used: 59 | #today = '' 60 | # Else, today_fmt is used as the format for a strftime call. 61 | #today_fmt = '%B %d, %Y' 62 | 63 | # List of documents that shouldn't be included in the build. 64 | #unused_docs = [] 65 | 66 | # List of directories, relative to source directory, that shouldn't be searched 67 | # for source files. 68 | exclude_trees = [] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. Major themes that come with 94 | # Sphinx are currently 'default' and 'sphinxdoc'. 95 | html_theme = 'default' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_use_modindex = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, an OpenSearch description file will be output, and all pages will 154 | # contain a tag referring to it. The value of this option must be the 155 | # base URL from which the finished HTML is served. 156 | #html_use_opensearch = '' 157 | 158 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 159 | #html_file_suffix = '' 160 | 161 | # Output file base name for HTML help builder. 162 | htmlhelp_basename = 'flupdoc' 163 | 164 | 165 | # -- Options for LaTeX output -------------------------------------------------- 166 | 167 | # The paper size ('letter' or 'a4'). 168 | #latex_paper_size = 'letter' 169 | 170 | # The font size ('10pt', '11pt' or '12pt'). 171 | #latex_font_size = '10pt' 172 | 173 | # Grouping the document tree into LaTeX files. List of tuples 174 | # (source start file, target name, title, author, documentclass [howto/manual]). 175 | latex_documents = [ 176 | ('index', 'flup.tex', u'flup Documentation', 177 | u'Allan Saddi', 'manual'), 178 | ] 179 | 180 | # The name of an image file (relative to this directory) to place at the top of 181 | # the title page. 182 | #latex_logo = None 183 | 184 | # For "manual" documents, if this is true, then toplevel headings are parts, 185 | # not chapters. 186 | #latex_use_parts = False 187 | 188 | # Additional stuff for the LaTeX preamble. 189 | #latex_preamble = '' 190 | 191 | # Documents to append as an appendix to all manuals. 192 | #latex_appendices = [] 193 | 194 | # If false, no module index is generated. 195 | #latex_use_modindex = True 196 | -------------------------------------------------------------------------------- /flup/server/fcgi_fork.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. highlight:: python 3 | :linenothreshold: 5 4 | 5 | .. highlight:: bash 6 | :linenothreshold: 5 7 | 8 | fcgi - a FastCGI/WSGI gateway. 9 | 10 | :copyright: Copyright (c) 2005, 2006 Allan Saddi 11 | All rights reserved. 12 | :license: 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions 16 | are met: 17 | 18 | 1. Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | 2. Redistributions in binary form must reproduce the above copyright 21 | notice, this list of conditions and the following disclaimer in the 22 | documentation and/or other materials provided with the distribution. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS **AS IS** AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 30 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 33 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 34 | SUCH DAMAGE. 35 | 36 | For more information about FastCGI, see http://www.fastcgi.com/. 37 | 38 | For more information about the Web Server Gateway Interface, see 39 | http://www.python.org/peps/pep-0333.html. 40 | 41 | Example usage:: 42 | 43 | #!/usr/bin/env python 44 | from myapplication import app # Assume app is your WSGI application object 45 | from fcgi import WSGIServer 46 | WSGIServer(app).run() 47 | 48 | See the documentation for WSGIServer for more information. 49 | 50 | On most platforms, fcgi will fallback to regular CGI behavior if run in a 51 | non-FastCGI context. If you want to force CGI behavior, set the environment 52 | variable FCGI_FORCE_CGI to "Y" or "y". 53 | """ 54 | 55 | __author__ = 'Allan Saddi ' 56 | __version__ = '$Revision$' 57 | 58 | import os 59 | 60 | from .fcgi_base import BaseFCGIServer, FCGI_RESPONDER, \ 61 | FCGI_MAX_CONNS, FCGI_MAX_REQS, FCGI_MPXS_CONNS 62 | from .preforkserver import PreforkServer 63 | 64 | __all__ = ['WSGIServer'] 65 | 66 | class WSGIServer(BaseFCGIServer, PreforkServer): 67 | """ 68 | FastCGI server that supports the Web Server Gateway Interface. See 69 | http://www.python.org/peps/pep-0333.html. 70 | """ 71 | def __init__(self, application, environ=None, 72 | bindAddress=None, umask=None, multiplexed=False, 73 | debug=False, roles=(FCGI_RESPONDER,), forceCGI=False, 74 | timeout=None, **kw): 75 | """ 76 | environ, if present, must be a dictionary-like object. Its 77 | contents will be copied into application's environ. Useful 78 | for passing application-specific variables. 79 | 80 | bindAddress, if present, must either be a string or a 2-tuple. If 81 | present, run() will open its own listening socket. You would use 82 | this if you wanted to run your application as an 'external' FastCGI 83 | app. (i.e. the webserver would no longer be responsible for starting 84 | your app) If a string, it will be interpreted as a filename and a UNIX 85 | socket will be opened. If a tuple, the first element, a string, 86 | is the interface name/IP to bind to, and the second element (an int) 87 | is the port number. 88 | """ 89 | BaseFCGIServer.__init__(self, application, 90 | environ=environ, 91 | multithreaded=False, 92 | multiprocess=True, 93 | bindAddress=bindAddress, 94 | umask=umask, 95 | multiplexed=multiplexed, 96 | debug=debug, 97 | roles=roles, 98 | forceCGI=forceCGI) 99 | for key in ('multithreaded', 'multiprocess', 'jobClass', 'jobArgs'): 100 | if key in kw: 101 | del kw[key] 102 | PreforkServer.__init__(self, jobClass=self._connectionClass, 103 | jobArgs=(self, timeout), **kw) 104 | 105 | try: 106 | import resource 107 | # Attempt to glean the maximum number of connections 108 | # from the OS. 109 | try: 110 | maxProcs = resource.getrlimit(resource.RLIMIT_NPROC)[0] 111 | maxConns = resource.getrlimit(resource.RLIMIT_NOFILE)[0] 112 | maxConns = min(maxConns, maxProcs) 113 | except AttributeError: 114 | maxConns = resource.getrlimit(resource.RLIMIT_NOFILE)[0] 115 | except ImportError: 116 | maxConns = 100 # Just some made up number. 117 | maxReqs = maxConns 118 | self.capability = { 119 | FCGI_MAX_CONNS: maxConns, 120 | FCGI_MAX_REQS: maxReqs, 121 | FCGI_MPXS_CONNS: 0 122 | } 123 | 124 | def _isClientAllowed(self, addr): 125 | return self._web_server_addrs is None or \ 126 | (len(addr) == 2 and addr[0] in self._web_server_addrs) 127 | 128 | def run(self): 129 | """ 130 | The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if 131 | SIGHUP was received, False otherwise. 132 | """ 133 | self._web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS') 134 | if self._web_server_addrs is not None: 135 | self._web_server_addrs = [x.strip() for x in self._web_server_addrs.split(',')] 136 | 137 | sock = self._setupSocket() 138 | 139 | ret = PreforkServer.run(self, sock) 140 | 141 | self._cleanupSocket(sock) 142 | 143 | return ret 144 | 145 | if __name__ == '__main__': 146 | def test_app(environ, start_response): 147 | """Probably not the most efficient example.""" 148 | from . import cgi 149 | start_response('200 OK', [('Content-Type', 'text/html')]) 150 | yield 'Hello World!\n' \ 151 | '\n' \ 152 | '

Hello World!

\n' \ 153 | '' 154 | names = list(environ.keys()) 155 | names.sort() 156 | for name in names: 157 | yield '\n' % ( 158 | name, cgi.escape(repr(environ[name]))) 159 | 160 | form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 161 | keep_blank_values=1) 162 | if form.list: 163 | yield '' 164 | 165 | for field in form.list: 166 | yield '\n' % ( 167 | field.name, field.value) 168 | 169 | yield '
%s%s
Form data
%s%s
\n' \ 170 | '\n' 171 | 172 | from wsgiref import validate 173 | test_app = validate.validator(test_app) 174 | WSGIServer(test_app).run() 175 | -------------------------------------------------------------------------------- /flup/server/paste_factory.py: -------------------------------------------------------------------------------- 1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 | 4 | from flup.server import NoDefault 5 | 6 | def asbool(obj): 7 | if isinstance(obj, (str, unicode)): 8 | obj = obj.strip().lower() 9 | if obj in ['true', 'yes', 'on', 'y', 't', '1']: 10 | return True 11 | elif obj in ['false', 'no', 'off', 'n', 'f', '0']: 12 | return False 13 | else: 14 | raise ValueError( 15 | "String is not true/false: %r" % obj) 16 | return bool(obj) 17 | 18 | def aslist(obj, sep=None, strip=True): 19 | if isinstance(obj, (str, unicode)): 20 | lst = obj.split(sep) 21 | if strip: 22 | lst = [v.strip() for v in lst] 23 | return lst 24 | elif isinstance(obj, (list, tuple)): 25 | return obj 26 | elif obj is None: 27 | return [] 28 | else: 29 | return [obj] 30 | 31 | def run_ajp_thread(wsgi_app, global_conf, 32 | scriptName='', host='localhost', port='8009', 33 | allowedServers='127.0.0.1', debug=NoDefault, 34 | minSpare=None, maxSpare=None, maxThreads=None): 35 | import flup.server.ajp 36 | addr = (host, int(port)) 37 | if debug is NoDefault: 38 | debug = global_conf.get('debug', False) 39 | debug = asbool(debug) 40 | threadpool_args = {} 41 | if minSpare is not None: 42 | threadpool_args['minSpare'] = int(minSpare) 43 | if maxSpare is not None: 44 | threadpool_args['maxSpare'] = int(maxSpare) 45 | if maxThreads is not None: 46 | threadpool_args['maxThreads'] = int(maxThreads) 47 | s = flup.server.ajp.WSGIServer( 48 | wsgi_app, 49 | scriptName=scriptName, 50 | bindAddress=addr, 51 | allowedServers=aslist(allowedServers), 52 | debug=debug, **threadpool_args 53 | ) 54 | s.run() 55 | 56 | def run_ajp_fork(wsgi_app, global_conf, 57 | scriptName='', host='localhost', port='8009', 58 | allowedServers='127.0.0.1', debug=NoDefault, 59 | minSpare=None, maxSpare=None, 60 | maxChildren=None, maxRequests=None): 61 | import flup.server.ajp_fork 62 | addr = (host, int(port)) 63 | if debug is NoDefault: 64 | debug = global_conf.get('debug', False) 65 | debug = asbool(debug) 66 | prefork_args = {} 67 | if minSpare is not None: 68 | prefork_args['minSpare'] = int(minSpare) 69 | if maxSpare is not None: 70 | prefork_args['maxSpare'] = int(maxSpare) 71 | if maxChildren is not None: 72 | prefork_args['maxChildren'] = int(maxChildren) 73 | if maxRequests is not None: 74 | prefork_args['maxRequests'] = int(maxRequests) 75 | s = flup.server.ajp_fork.WSGIServer( 76 | wsgi_app, 77 | scriptName=scriptName, 78 | bindAddress=addr, 79 | allowedServers=aslist(allowedServers), 80 | debug=debug, **prefork_args 81 | ) 82 | s.run() 83 | 84 | def run_fcgi_thread(wsgi_app, global_conf, 85 | host=None, port=None, 86 | socket=None, umask=None, 87 | multiplexed=False, debug=NoDefault, 88 | minSpare=None, maxSpare=None, maxThreads=None): 89 | import flup.server.fcgi 90 | if socket: 91 | assert host is None and port is None 92 | sock = socket 93 | elif host: 94 | assert host is not None and port is not None 95 | sock = (host, int(port)) 96 | else: 97 | sock = None 98 | if umask is not None: 99 | umask = int(umask) 100 | if debug is NoDefault: 101 | debug = global_conf.get('debug', False) 102 | debug = asbool(debug) 103 | threadpool_args = {} 104 | if minSpare is not None: 105 | threadpool_args['minSpare'] = int(minSpare) 106 | if maxSpare is not None: 107 | threadpool_args['maxSpare'] = int(maxSpare) 108 | if maxThreads is not None: 109 | threadpool_args['maxThreads'] = int(maxThreads) 110 | s = flup.server.fcgi.WSGIServer( 111 | wsgi_app, 112 | bindAddress=sock, umask=umask, 113 | multiplexed=asbool(multiplexed), 114 | debug=debug, **threadpool_args 115 | ) 116 | s.run() 117 | 118 | def run_fcgi_fork(wsgi_app, global_conf, 119 | host=None, port=None, 120 | socket=None, umask=None, 121 | multiplexed=False, 122 | debug=NoDefault, 123 | minSpare=None, maxSpare=None, 124 | maxChildren=None, maxRequests=None): 125 | import flup.server.fcgi_fork 126 | if socket: 127 | assert host is None and port is None 128 | sock = socket 129 | elif host: 130 | assert host is not None and port is not None 131 | sock = (host, int(port)) 132 | else: 133 | sock = None 134 | if umask is not None: 135 | umask = int(umask) 136 | if debug is NoDefault: 137 | debug = global_conf.get('debug', False) 138 | debug = asbool(debug) 139 | prefork_args = {} 140 | if minSpare is not None: 141 | prefork_args['minSpare'] = int(minSpare) 142 | if maxSpare is not None: 143 | prefork_args['maxSpare'] = int(maxSpare) 144 | if maxChildren is not None: 145 | prefork_args['maxChildren'] = int(maxChildren) 146 | if maxRequests is not None: 147 | prefork_args['maxRequests'] = int(maxRequests) 148 | s = flup.server.fcgi_fork.WSGIServer( 149 | wsgi_app, 150 | bindAddress=sock, umask=umask, 151 | multiplexed=asbool(multiplexed), 152 | debug=debug, **prefork_args 153 | ) 154 | s.run() 155 | 156 | def run_scgi_thread(wsgi_app, global_conf, 157 | scriptName=NoDefault, host='localhost', port='4000', 158 | allowedServers='127.0.0.1', 159 | debug=NoDefault, 160 | minSpare=None, maxSpare=None, maxThreads=None): 161 | import flup.server.scgi 162 | addr = (host, int(port)) 163 | if debug is NoDefault: 164 | debug = global_conf.get('debug', False) 165 | debug = asbool(debug) 166 | threadpool_args = {} 167 | if minSpare is not None: 168 | threadpool_args['minSpare'] = int(minSpare) 169 | if maxSpare is not None: 170 | threadpool_args['maxSpare'] = int(maxSpare) 171 | if maxThreads is not None: 172 | threadpool_args['maxThreads'] = int(maxThreads) 173 | s = flup.server.scgi.WSGIServer( 174 | wsgi_app, 175 | scriptName=scriptName, 176 | bindAddress=addr, 177 | allowedServers=aslist(allowedServers), 178 | debug=debug, **threadpool_args 179 | ) 180 | s.run() 181 | 182 | def run_scgi_fork(wsgi_app, global_conf, 183 | scriptName=NoDefault, host='localhost', port='4000', 184 | allowedServers='127.0.0.1', 185 | debug=NoDefault, 186 | minSpare=None, maxSpare=None, 187 | maxChildren=None, maxRequests=None): 188 | import flup.server.scgi_fork 189 | addr = (host, int(port)) 190 | if debug is NoDefault: 191 | debug = global_conf.get('debug', False) 192 | debug = asbool(debug) 193 | prefork_args = {} 194 | if minSpare is not None: 195 | prefork_args['minSpare'] = int(minSpare) 196 | if maxSpare is not None: 197 | prefork_args['maxSpare'] = int(maxSpare) 198 | if maxChildren is not None: 199 | prefork_args['maxChildren'] = int(maxChildren) 200 | if maxRequests is not None: 201 | prefork_args['maxRequests'] = int(maxRequests) 202 | s = flup.server.scgi_fork.WSGIServer( 203 | wsgi_app, 204 | scriptName=scriptName, 205 | bindAddress=addr, 206 | allowedServers=aslist(allowedServers), 207 | debug=debug, **prefork_args 208 | ) 209 | s.run() 210 | 211 | -------------------------------------------------------------------------------- /flup/server/ajp_fork.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. highlight:: python 3 | :linenothreshold: 5 4 | 5 | .. highlight:: bash 6 | :linenothreshold: 5 7 | 8 | ajp - an AJP 1.3/WSGI gateway. 9 | 10 | :copyright: Copyright (c) 2005, 2006 Allan Saddi 11 | All rights reserved. 12 | :license: 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions 16 | are met: 17 | 18 | 1. Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | 2. Redistributions in binary form must reproduce the above copyright 21 | notice, this list of conditions and the following disclaimer in the 22 | documentation and/or other materials provided with the distribution. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS **AS IS** AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 30 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 33 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 34 | SUCH DAMAGE. 35 | 36 | For more information about AJP and AJP connectors for your web server, see 37 | http://jakarta.apache.org/tomcat/connectors-doc/. 38 | 39 | For more information about the Web Server Gateway Interface, see 40 | http://www.python.org/peps/pep-0333.html. 41 | 42 | Example usage: 43 | 44 | #!/usr/bin/env python 45 | import sys 46 | from myapplication import app # Assume app is your WSGI application object 47 | from ajp import WSGIServer 48 | ret = WSGIServer(app).run() 49 | sys.exit(ret and 42 or 0) 50 | 51 | See the documentation for WSGIServer for more information. 52 | 53 | About the bit of logic at the end: 54 | Upon receiving SIGHUP, the python script will exit with status code 42. This 55 | can be used by a wrapper script to determine if the python script should be 56 | re-run. When a SIGINT or SIGTERM is received, the script exits with status 57 | code 0, possibly indicating a normal exit. 58 | 59 | Example wrapper script:: 60 | 61 | #!/bin/sh 62 | STATUS=42 63 | while test $STATUS -eq 42; do 64 | python "$@" that_script_above.py 65 | STATUS=$? 66 | done 67 | 68 | Example workers.properties (for mod_jk):: 69 | 70 | worker.list=foo 71 | worker.foo.port=8009 72 | worker.foo.host=localhost 73 | worker.foo.type=ajp13 74 | 75 | Example httpd.conf (for mod_jk):: 76 | 77 | JkWorkersFile /path/to/workers.properties 78 | JkMount /* foo 79 | 80 | Note that if you mount your ajp application anywhere but the root ("/"), you 81 | SHOULD specifiy scriptName to the WSGIServer constructor. This will ensure 82 | that SCRIPT_NAME/PATH_INFO are correctly deduced. 83 | """ 84 | 85 | __author__ = 'Allan Saddi ' 86 | __version__ = '$Revision$' 87 | 88 | import socket 89 | import logging 90 | 91 | from .ajp_base import BaseAJPServer, Connection 92 | from .preforkserver import PreforkServer 93 | 94 | __all__ = ['WSGIServer'] 95 | 96 | class WSGIServer(BaseAJPServer, PreforkServer): 97 | """ 98 | AJP1.3/WSGI server. Runs your WSGI application as a persistant program 99 | that understands AJP1.3. Opens up a TCP socket, binds it, and then 100 | waits for forwarded requests from your webserver. 101 | 102 | Why AJP? Two good reasons are that AJP provides load-balancing and 103 | fail-over support. Personally, I just wanted something new to 104 | implement. :) 105 | 106 | Of course you will need an AJP1.3 connector for your webserver (e.g. 107 | mod_jk) - see http://jakarta.apache.org/tomcat/connectors-doc/. 108 | """ 109 | def __init__(self, application, scriptName='', environ=None, 110 | bindAddress=('localhost', 8009), allowedServers=None, 111 | loggingLevel=logging.INFO, debug=False, timeout=None, **kw): 112 | """ 113 | scriptName is the initial portion of the URL path that "belongs" 114 | to your application. It is used to determine PATH_INFO (which doesn't 115 | seem to be passed in). An empty scriptName means your application 116 | is mounted at the root of your virtual host. 117 | 118 | environ, which must be a dictionary, can contain any additional 119 | environment variables you want to pass to your application. 120 | 121 | bindAddress is the address to bind to, which must be a tuple of 122 | length 2. The first element is a string, which is the host name 123 | or IPv4 address of a local interface. The 2nd element is the port 124 | number. 125 | 126 | allowedServers must be None or a list of strings representing the 127 | IPv4 addresses of servers allowed to connect. None means accept 128 | connections from anywhere. 129 | 130 | loggingLevel sets the logging level of the module-level logger. 131 | """ 132 | BaseAJPServer.__init__(self, application, 133 | scriptName=scriptName, 134 | environ=environ, 135 | multithreaded=False, 136 | multiprocess=True, 137 | bindAddress=bindAddress, 138 | allowedServers=allowedServers, 139 | loggingLevel=loggingLevel, 140 | debug=debug) 141 | for key in ('multithreaded', 'multiprocess', 'jobClass', 'jobArgs'): 142 | if key in kw: 143 | del kw[key] 144 | PreforkServer.__init__(self, jobClass=Connection, 145 | jobArgs=(self, timeout), **kw) 146 | 147 | def run(self): 148 | """ 149 | Main loop. Call this after instantiating WSGIServer. SIGHUP, SIGINT, 150 | SIGQUIT, SIGTERM cause it to cleanup and return. (If a SIGHUP 151 | is caught, this method returns True. Returns False otherwise.) 152 | """ 153 | self.logger.info('%s starting up', self.__class__.__name__) 154 | 155 | try: 156 | sock = self._setupSocket() 157 | except socket.error as e: 158 | self.logger.error('Failed to bind socket (%s), exiting', e[1]) 159 | return False 160 | 161 | ret = PreforkServer.run(self, sock) 162 | 163 | self._cleanupSocket(sock) 164 | 165 | self.logger.info('%s shutting down%s', self.__class__.__name__, 166 | self._hupReceived and ' (reload requested)' or '') 167 | 168 | return ret 169 | 170 | if __name__ == '__main__': 171 | def test_app(environ, start_response): 172 | """Probably not the most efficient example.""" 173 | from . import cgi 174 | start_response('200 OK', [('Content-Type', 'text/html')]) 175 | yield 'Hello World!\n' \ 176 | '\n' \ 177 | '

Hello World!

\n' \ 178 | '' 179 | names = list(environ.keys()) 180 | names.sort() 181 | for name in names: 182 | yield '\n' % ( 183 | name, cgi.escape(repr(environ[name]))) 184 | 185 | form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 186 | keep_blank_values=1) 187 | if form.list: 188 | yield '' 189 | 190 | for field in form.list: 191 | yield '\n' % ( 192 | field.name, field.value) 193 | 194 | yield '
%s%s
Form data
%s%s
\n' \ 195 | '\n' 196 | 197 | from wsgiref import validate 198 | test_app = validate.validator(test_app) 199 | # Explicitly set bindAddress to *:8009 for testing. 200 | WSGIServer(test_app, 201 | bindAddress=('', 8009), allowedServers=None, 202 | loggingLevel=logging.DEBUG).run() 203 | -------------------------------------------------------------------------------- /flup/server/scgi_fork.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. highlight:: python 3 | :linenothreshold: 5 4 | 5 | .. highlight:: bash 6 | :linenothreshold: 5 7 | 8 | scgi - an SCGI/WSGI gateway. 9 | 10 | :copyright: Copyright (c) 2005, 2006 Allan Saddi 11 | All rights reserved. 12 | :license: 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions 16 | are met: 17 | 18 | 1. Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | 2. Redistributions in binary form must reproduce the above copyright 21 | notice, this list of conditions and the following disclaimer in the 22 | documentation and/or other materials provided with the distribution. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS **AS IS** AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 30 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 33 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 34 | SUCH DAMAGE. 35 | 36 | For more information about SCGI and mod_scgi for Apache1/Apache2, see 37 | http://www.mems-exchange.org/software/scgi/. 38 | 39 | For more information about the Web Server Gateway Interface, see 40 | http://www.python.org/peps/pep-0333.html. 41 | 42 | Example usage:: 43 | 44 | #!/usr/bin/env python 45 | import sys 46 | from myapplication import app # Assume app is your WSGI application object 47 | from scgi import WSGIServer 48 | ret = WSGIServer(app).run() 49 | sys.exit(ret and 42 or 0) 50 | 51 | See the documentation for WSGIServer for more information. 52 | 53 | About the bit of logic at the end: 54 | Upon receiving SIGHUP, the python script will exit with status code 42. This 55 | can be used by a wrapper script to determine if the python script should be 56 | re-run. When a SIGINT or SIGTERM is received, the script exits with status 57 | code 0, possibly indicating a normal exit. 58 | 59 | Example wrapper script:: 60 | 61 | #!/bin/sh 62 | STATUS=42 63 | while test $STATUS -eq 42; do 64 | python "$@" that_script_above.py 65 | STATUS=$? 66 | done 67 | """ 68 | 69 | __author__ = 'Allan Saddi ' 70 | __version__ = '$Revision$' 71 | 72 | import logging 73 | import socket 74 | 75 | from flup.server import NoDefault 76 | from flup.server.scgi_base import BaseSCGIServer, Connection 77 | from flup.server.preforkserver import PreforkServer 78 | 79 | __all__ = ['WSGIServer'] 80 | 81 | class WSGIServer(BaseSCGIServer, PreforkServer): 82 | """ 83 | SCGI/WSGI server. For information about SCGI (Simple Common Gateway 84 | Interface), see http://www.mems-exchange.org/software/scgi/. 85 | 86 | This server is similar to SWAP http://www.idyll.org/~t/www-tools/wsgi/, 87 | another SCGI/WSGI server. 88 | 89 | It differs from SWAP in that it isn't based on scgi.scgi_server and 90 | therefore, it allows me to implement concurrency using threads. (Also, 91 | this server was written from scratch and really has no other depedencies.) 92 | Which server to use really boils down to whether you want multithreading 93 | or forking. (But as an aside, I've found scgi.scgi_server's implementation 94 | of preforking to be quite superior. So if your application really doesn't 95 | mind running in multiple processes, go use SWAP. ;) 96 | """ 97 | def __init__(self, application, scriptName=NoDefault, environ=None, 98 | bindAddress=('localhost', 4000), umask=None, 99 | allowedServers=None, 100 | loggingLevel=logging.INFO, debug=False, timeout=None, **kw): 101 | """ 102 | scriptName is the initial portion of the URL path that "belongs" 103 | to your application. It is used to determine PATH_INFO (which doesn't 104 | seem to be passed in). An empty scriptName means your application 105 | is mounted at the root of your virtual host. 106 | 107 | environ, which must be a dictionary, can contain any additional 108 | environment variables you want to pass to your application. 109 | 110 | bindAddress is the address to bind to, which must be a string or 111 | a tuple of length 2. If a tuple, the first element must be a string, 112 | which is the host name or IPv4 address of a local interface. The 113 | 2nd element of the tuple is the port number. If a string, it will 114 | be interpreted as a filename and a UNIX socket will be opened. 115 | 116 | If binding to a UNIX socket, umask may be set to specify what 117 | the umask is to be changed to before the socket is created in the 118 | filesystem. After the socket is created, the previous umask is 119 | restored. 120 | 121 | allowedServers must be None or a list of strings representing the 122 | IPv4 addresses of servers allowed to connect. None means accept 123 | connections from anywhere. 124 | 125 | loggingLevel sets the logging level of the module-level logger. 126 | """ 127 | BaseSCGIServer.__init__(self, application, 128 | scriptName=scriptName, 129 | environ=environ, 130 | multithreaded=False, 131 | multiprocess=True, 132 | bindAddress=bindAddress, 133 | umask=umask, 134 | allowedServers=allowedServers, 135 | loggingLevel=loggingLevel, 136 | debug=debug) 137 | for key in ('multithreaded', 'multiprocess', 'jobClass', 'jobArgs'): 138 | if key in kw: 139 | del kw[key] 140 | 141 | PreforkServer.__init__(self, jobClass=Connection, 142 | jobArgs=(self, timeout), **kw) 143 | 144 | def run(self): 145 | """ 146 | Main loop. Call this after instantiating WSGIServer. SIGHUP, SIGINT, 147 | SIGQUIT, SIGTERM cause it to cleanup and return. (If a SIGHUP 148 | is caught, this method returns True. Returns False otherwise.) 149 | """ 150 | self.logger.info('%s starting up', self.__class__.__name__) 151 | 152 | try: 153 | sock = self._setupSocket() 154 | except socket.error as e: 155 | self.logger.error('Failed to bind socket (%s), exiting', e[1]) 156 | return False 157 | 158 | ret = PreforkServer.run(self, sock) 159 | 160 | self._cleanupSocket(sock) 161 | 162 | self.logger.info('%s shutting down%s', self.__class__.__name__, 163 | self._hupReceived and ' (reload requested)' or '') 164 | 165 | return ret 166 | 167 | if __name__ == '__main__': 168 | def test_app(environ, start_response): 169 | """Probably not the most efficient example.""" 170 | from . import cgi 171 | start_response('200 OK', [('Content-Type', 'text/html')]) 172 | yield 'Hello World!\n' \ 173 | '\n' \ 174 | '

Hello World!

\n' \ 175 | '' 176 | names = list(environ.keys()) 177 | names.sort() 178 | for name in names: 179 | yield '\n' % ( 180 | name, cgi.escape(repr(environ[name]))) 181 | 182 | form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 183 | keep_blank_values=1) 184 | if form.list: 185 | yield '' 186 | 187 | for field in form.list: 188 | yield '\n' % ( 189 | field.name, field.value) 190 | 191 | yield '
%s%s
Form data
%s%s
\n' \ 192 | '\n' 193 | 194 | from wsgiref import validate 195 | test_app = validate.validator(test_app) 196 | WSGIServer(test_app, 197 | loggingLevel=logging.DEBUG).run() 198 | -------------------------------------------------------------------------------- /flup/server/ajp.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. highlight:: python 3 | :linenothreshold: 5 4 | 5 | .. highlight:: bash 6 | :linenothreshold: 5 7 | 8 | ajp - an AJP 1.3/WSGI gateway. 9 | 10 | :copyright: Copyright (c) 2005, 2006 Allan Saddi 11 | All rights reserved. 12 | :license: 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions 16 | are met: 17 | 18 | 1. Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | 2. Redistributions in binary form must reproduce the above copyright 21 | notice, this list of conditions and the following disclaimer in the 22 | documentation and/or other materials provided with the distribution. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS **AS IS** AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 30 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 33 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 34 | SUCH DAMAGE. 35 | 36 | For more information about AJP and AJP connectors for your web server, see 37 | http://jakarta.apache.org/tomcat/connectors-doc/. 38 | 39 | For more information about the Web Server Gateway Interface, see 40 | http://www.python.org/peps/pep-0333.html. 41 | 42 | Example usage:: 43 | 44 | #!/usr/bin/env python 45 | import sys 46 | from myapplication import app # Assume app is your WSGI application object 47 | from ajp import WSGIServer 48 | ret = WSGIServer(app).run() 49 | sys.exit(ret and 42 or 0) 50 | 51 | See the documentation for WSGIServer for more information. 52 | 53 | About the bit of logic at the end: 54 | Upon receiving SIGHUP, the python script will exit with status code 42. This 55 | can be used by a wrapper script to determine if the python script should be 56 | re-run. When a SIGINT or SIGTERM is received, the script exits with status 57 | code 0, possibly indicating a normal exit. 58 | 59 | Example wrapper script:: 60 | 61 | #!/bin/sh 62 | STATUS=42 63 | while test $STATUS -eq 42; do 64 | python "$@" that_script_above.py 65 | STATUS=$? 66 | done 67 | 68 | Example workers.properties (for mod_jk):: 69 | 70 | worker.list=foo 71 | worker.foo.port=8009 72 | worker.foo.host=localhost 73 | worker.foo.type=ajp13 74 | 75 | Example httpd.conf (for mod_jk):: 76 | 77 | JkWorkersFile /path/to/workers.properties 78 | JkMount /* foo 79 | 80 | Note that if you mount your ajp application anywhere but the root ("/"), you 81 | SHOULD specifiy scriptName to the WSGIServer constructor. This will ensure 82 | that SCRIPT_NAME/PATH_INFO are correctly deduced. 83 | """ 84 | 85 | __author__ = 'Allan Saddi ' 86 | __version__ = '$Revision$' 87 | 88 | import socket 89 | import logging 90 | 91 | from .ajp_base import BaseAJPServer, Connection 92 | from .threadedserver import ThreadedServer 93 | 94 | __all__ = ['WSGIServer'] 95 | 96 | class WSGIServer(BaseAJPServer, ThreadedServer): 97 | """ 98 | AJP1.3/WSGI server. Runs your WSGI application as a persistant program 99 | that understands AJP1.3. Opens up a TCP socket, binds it, and then 100 | waits for forwarded requests from your webserver. 101 | 102 | Why AJP? Two good reasons are that AJP provides load-balancing and 103 | fail-over support. Personally, I just wanted something new to 104 | implement. :) 105 | 106 | Of course you will need an AJP1.3 connector for your webserver (e.g. 107 | mod_jk) - see http://jakarta.apache.org/tomcat/connectors-doc/. 108 | """ 109 | def __init__(self, application, scriptName='', environ=None, 110 | multithreaded=True, multiprocess=False, 111 | bindAddress=('localhost', 8009), allowedServers=None, 112 | loggingLevel=logging.INFO, debug=False, **kw): 113 | """ 114 | scriptName is the initial portion of the URL path that "belongs" 115 | to your application. It is used to determine PATH_INFO (which doesn't 116 | seem to be passed in). An empty scriptName means your application 117 | is mounted at the root of your virtual host. 118 | 119 | environ, which must be a dictionary, can contain any additional 120 | environment variables you want to pass to your application. 121 | 122 | bindAddress is the address to bind to, which must be a tuple of 123 | length 2. The first element is a string, which is the host name 124 | or IPv4 address of a local interface. The 2nd element is the port 125 | number. 126 | 127 | allowedServers must be None or a list of strings representing the 128 | IPv4 addresses of servers allowed to connect. None means accept 129 | connections from anywhere. 130 | 131 | loggingLevel sets the logging level of the module-level logger. 132 | """ 133 | BaseAJPServer.__init__(self, application, 134 | scriptName=scriptName, 135 | environ=environ, 136 | multithreaded=multithreaded, 137 | multiprocess=multiprocess, 138 | bindAddress=bindAddress, 139 | allowedServers=allowedServers, 140 | loggingLevel=loggingLevel, 141 | debug=debug) 142 | for key in ('jobClass', 'jobArgs'): 143 | if key in kw: 144 | del kw[key] 145 | ThreadedServer.__init__(self, jobClass=Connection, 146 | jobArgs=(self, None), **kw) 147 | 148 | def run(self): 149 | """ 150 | Main loop. Call this after instantiating WSGIServer. SIGHUP, SIGINT, 151 | SIGQUIT, SIGTERM cause it to cleanup and return. (If a SIGHUP 152 | is caught, this method returns True. Returns False otherwise.) 153 | """ 154 | self.logger.info('%s starting up', self.__class__.__name__) 155 | 156 | try: 157 | sock = self._setupSocket() 158 | except socket.error as e: 159 | self.logger.error('Failed to bind socket (%s), exiting', e[1]) 160 | return False 161 | 162 | ret = ThreadedServer.run(self, sock) 163 | 164 | self._cleanupSocket(sock) 165 | # AJP connections are more or less persistent. .shutdown() will 166 | # not return until the web server lets go. So don't bother calling 167 | # it... 168 | #self.shutdown() 169 | 170 | self.logger.info('%s shutting down%s', self.__class__.__name__, 171 | self._hupReceived and ' (reload requested)' or '') 172 | 173 | return ret 174 | 175 | if __name__ == '__main__': 176 | def test_app(environ, start_response): 177 | """Probably not the most efficient example.""" 178 | import cgi 179 | start_response('200 OK', [('Content-Type', 'text/html')]) 180 | yield 'Hello World!\n' \ 181 | '\n' \ 182 | '

Hello World!

\n' \ 183 | '' 184 | names = list(environ.keys()) 185 | names.sort() 186 | for name in names: 187 | yield '\n' % ( 188 | name, cgi.escape(repr(environ[name]))) 189 | 190 | form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 191 | keep_blank_values=1) 192 | if form.list: 193 | yield '' 194 | 195 | for field in form.list: 196 | yield '\n' % ( 197 | field.name, field.value) 198 | 199 | yield '
%s%s
Form data
%s%s
\n' \ 200 | '\n' 201 | 202 | from wsgiref import validate 203 | test_app = validate.validator(test_app) 204 | # Explicitly set bindAddress to *:8009 for testing. 205 | WSGIServer(test_app, 206 | bindAddress=('', 8009), allowedServers=None, 207 | loggingLevel=logging.DEBUG).run() 208 | -------------------------------------------------------------------------------- /flup/server/scgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. highlight:: python 3 | :linenothreshold: 5 4 | 5 | .. highlight:: bash 6 | :linenothreshold: 5 7 | 8 | scgi - an SCGI/WSGI gateway. 9 | 10 | :copyright: Copyright (c) 2005, 2006 Allan Saddi 11 | All rights reserved. 12 | :license: 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions 16 | are met: 17 | 18 | 1. Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | 2. Redistributions in binary form must reproduce the above copyright 21 | notice, this list of conditions and the following disclaimer in the 22 | documentation and/or other materials provided with the distribution. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS **AS IS** AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 30 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 33 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 34 | SUCH DAMAGE. 35 | 36 | For more information about SCGI and mod_scgi for Apache1/Apache2, see 37 | http://www.mems-exchange.org/software/scgi/. 38 | 39 | For more information about the Web Server Gateway Interface, see 40 | http://www.python.org/peps/pep-0333.html. 41 | 42 | Example usage:: 43 | 44 | #!/usr/bin/env python 45 | import sys 46 | from myapplication import app # Assume app is your WSGI application object 47 | from scgi import WSGIServer 48 | ret = WSGIServer(app).run() 49 | sys.exit(ret and 42 or 0) 50 | 51 | See the documentation for WSGIServer for more information. 52 | 53 | About the bit of logic at the end: 54 | Upon receiving SIGHUP, the python script will exit with status code 42. This 55 | can be used by a wrapper script to determine if the python script should be 56 | re-run. When a SIGINT or SIGTERM is received, the script exits with status 57 | code 0, possibly indicating a normal exit. 58 | 59 | Example wrapper script:: 60 | 61 | #!/bin/sh 62 | STATUS=42 63 | while test $STATUS -eq 42; do 64 | python "$@" that_script_above.py 65 | STATUS=$? 66 | done 67 | """ 68 | 69 | __author__ = 'Allan Saddi ' 70 | __version__ = '$Revision$' 71 | 72 | import logging 73 | import socket 74 | 75 | from flup.server import NoDefault 76 | from flup.server.scgi_base import BaseSCGIServer, Connection 77 | from flup.server.threadedserver import ThreadedServer 78 | 79 | __all__ = ['WSGIServer'] 80 | 81 | class WSGIServer(BaseSCGIServer, ThreadedServer): 82 | """ 83 | SCGI/WSGI server. For information about SCGI (Simple Common Gateway 84 | Interface), see http://www.mems-exchange.org/software/scgi/. 85 | 86 | This server is similar to SWAP http://www.idyll.org/~t/www-tools/wsgi/, 87 | another SCGI/WSGI server. 88 | 89 | It differs from SWAP in that it isn't based on scgi.scgi_server and 90 | therefore, it allows me to implement concurrency using threads. (Also, 91 | this server was written from scratch and really has no other depedencies.) 92 | Which server to use really boils down to whether you want multithreading 93 | or forking. (But as an aside, I've found scgi.scgi_server's implementation 94 | of preforking to be quite superior. So if your application really doesn't 95 | mind running in multiple processes, go use SWAP. ;) 96 | """ 97 | def __init__(self, application, scriptName=NoDefault, environ=None, 98 | multithreaded=True, multiprocess=False, 99 | bindAddress=('localhost', 4000), umask=None, 100 | allowedServers=None, 101 | loggingLevel=logging.INFO, debug=False, **kw): 102 | """ 103 | scriptName is the initial portion of the URL path that "belongs" 104 | to your application. It is used to determine PATH_INFO (which doesn't 105 | seem to be passed in). An empty scriptName means your application 106 | is mounted at the root of your virtual host. 107 | 108 | environ, which must be a dictionary, can contain any additional 109 | environment variables you want to pass to your application. 110 | 111 | bindAddress is the address to bind to, which must be a string or 112 | a tuple of length 2. If a tuple, the first element must be a string, 113 | which is the host name or IPv4 address of a local interface. The 114 | 2nd element of the tuple is the port number. If a string, it will 115 | be interpreted as a filename and a UNIX socket will be opened. 116 | 117 | If binding to a UNIX socket, umask may be set to specify what 118 | the umask is to be changed to before the socket is created in the 119 | filesystem. After the socket is created, the previous umask is 120 | restored. 121 | 122 | allowedServers must be None or a list of strings representing the 123 | IPv4 addresses of servers allowed to connect. None means accept 124 | connections from anywhere. 125 | 126 | loggingLevel sets the logging level of the module-level logger. 127 | """ 128 | BaseSCGIServer.__init__(self, application, 129 | scriptName=scriptName, 130 | environ=environ, 131 | multithreaded=multithreaded, 132 | multiprocess=multiprocess, 133 | bindAddress=bindAddress, 134 | umask=umask, 135 | allowedServers=allowedServers, 136 | loggingLevel=loggingLevel, 137 | debug=debug) 138 | for key in ('jobClass', 'jobArgs'): 139 | if key in kw: 140 | del kw[key] 141 | ThreadedServer.__init__(self, jobClass=Connection, 142 | jobArgs=(self, None), **kw) 143 | 144 | def run(self): 145 | """ 146 | Main loop. Call this after instantiating WSGIServer. SIGHUP, SIGINT, 147 | SIGQUIT, SIGTERM cause it to cleanup and return. (If a SIGHUP 148 | is caught, this method returns True. Returns False otherwise.) 149 | """ 150 | self.logger.info('%s starting up', self.__class__.__name__) 151 | 152 | try: 153 | sock = self._setupSocket() 154 | except socket.error as e: 155 | self.logger.error('Failed to bind socket (%s), exiting', e[1]) 156 | return False 157 | 158 | ret = ThreadedServer.run(self, sock) 159 | 160 | self._cleanupSocket(sock) 161 | self.shutdown() 162 | 163 | self.logger.info('%s shutting down%s', self.__class__.__name__, 164 | self._hupReceived and ' (reload requested)' or '') 165 | 166 | return ret 167 | 168 | if __name__ == '__main__': 169 | def test_app(environ, start_response): 170 | """Probably not the most efficient example.""" 171 | import cgi 172 | start_response('200 OK', [('Content-Type', 'text/html')]) 173 | yield 'Hello World!\n' \ 174 | '\n' \ 175 | '

Hello World!

\n' \ 176 | '' 177 | names = list(environ.keys()) 178 | names.sort() 179 | for name in names: 180 | yield '\n' % ( 181 | name, cgi.escape(repr(environ[name]))) 182 | 183 | form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 184 | keep_blank_values=1) 185 | if form.list: 186 | yield '' 187 | 188 | for field in form.list: 189 | yield '\n' % ( 190 | field.name, field.value) 191 | 192 | yield '
%s%s
Form data
%s%s
\n' \ 193 | '\n' 194 | 195 | from wsgiref import validate 196 | test_app = validate.validator(test_app) 197 | WSGIServer(test_app, 198 | loggingLevel=logging.DEBUG).run() 199 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2011-04-22 Allan Saddi 2 | 3 | * Fix fcgi_single. 4 | 5 | 2011-04-04 Allan Saddi 6 | 7 | * Add threadpool options to Paste factories. 8 | 9 | 2011-02-19 Allan Saddi 10 | 11 | * When deriving PATH_INFO from REQUEST_URI, take SCRIPT_NAME into account. 12 | 13 | 2011-01-11 Allan Saddi 14 | 15 | * Use HTTP status code 500 for error pages. Thanks to 16 | Yohann Gabory for pointing out this issue and providing a patch. 17 | 18 | 2010-11-03 Allan Saddi 19 | 20 | * Some fixes to get forking fcgi working with Python 3.1. Thanks to 21 | Velko Ivanov for the patch! 22 | 23 | 2010-10-14 Allan Saddi 24 | 25 | * Don't try to restore signal handlers if they weren't installed in 26 | the first place. 27 | 28 | 2010-10-05 Allan Saddi 29 | 30 | * Improvements to *_app._getConnection methods suggested by 31 | Andrej A Antonov. Thanks! 32 | 33 | 2009-10-27 Allan Saddi 34 | 35 | * Exit gracefully if a thread cannot be started when adding a new 36 | job. 37 | 38 | 2009-10-21 Allan Saddi 39 | 40 | * Add configurable timeout (default: no timeout) to be used when the 41 | WSGI application is called. Only applies to forked servers! 42 | 43 | 2009-06-05 Allan Saddi 44 | 45 | * Fix bug in scgi servers that occurs when SCRIPT_NAME is missing. 46 | Thanks to Jon Nelson for finding the problem! 47 | 48 | 49 | 2009-05-29 Allan Saddi 50 | 51 | * Let all the active requests to finish before quitting. Thanks 52 | to Anand Chitipothu for the patch! 53 | 54 | 2009-05-26 Allan Saddi 55 | 56 | * Release 1.0.2 57 | 58 | 2009-05-18 Allan Saddi 59 | 60 | * Import Paste factories (and dependencies...) from PasteScript 61 | 62 | 2009-05-04 Allan Saddi 63 | 64 | * Be tolerant of EAGAIN when sending messages to parent process. 65 | 66 | 2009-02-02 Allan Saddi 67 | 68 | * Add forceCGI keyword argument to FastCGI servers to 69 | programmatically force CGI behavior. 70 | 71 | * Merge Tommi Virtanen's "single server" (sequential server) 72 | patch. 73 | 74 | 2008-12-03 Allan Saddi 75 | 76 | * Update ez_setup.py. 77 | 78 | 2008-09-26 Allan Saddi 79 | 80 | * Re-seed random module after each fork. 81 | 82 | 2008-09-11 Allan Saddi 83 | 84 | * Add an indication as to which header fails assertion when 85 | passing in non-string header names and/or values. 86 | 87 | 2008-08-20 Allan Saddi 88 | 89 | * Add support for setting umask for UNIX domain sockets from 90 | paste.server_factory implementations. Thanks to Michal Suszko 91 | for the patch. 92 | 93 | 2008-07-23 Allan Saddi 94 | 95 | * Add support for configuring UNIX domain sockets (for servers that 96 | support them) in the paste.server_factory implementations. Thanks 97 | to Dan Roberts for the code. 98 | 99 | 2008-07-22 Allan Saddi 100 | 101 | * Release 1.0.1 102 | 103 | * Attempt to deduce missing PATH_INFO and/or QUERY_STRING from 104 | REQUEST_URI, if present. Patch provided by Richard Davies. 105 | 106 | 2007-09-10 Allan Saddi 107 | 108 | * Fix readline implementations so size argument is checked 109 | earlier. 110 | 111 | 2007-07-14 Allan Saddi 112 | 113 | * Prevent ThreadPool inconsistences if an exception is 114 | actually raised. Thanks to Tim Chen for the patch. 115 | 116 | 2007-06-05 Allan Saddi 117 | 118 | * Remove publisher and middleware packages. 119 | * Add cgi server for completeness. 120 | 121 | 2007-05-17 Allan Saddi 122 | 123 | * Fix fcgi_fork so it can run on Solaris. Thanks to 124 | Basil Crow for the patch. 125 | 126 | 2007-01-22 Allan Saddi 127 | 128 | * Fix eunuchs import issue. 129 | 130 | 2007-01-10 Allan Saddi 131 | 132 | * Support gzip compression of XHTML pages using the 133 | correct MIME type. 134 | 135 | 2006-12-29 Allan Saddi 136 | 137 | * Deprecate WSGI_SCRIPT_NAME and scriptName in scgi_base. 138 | Modern versions of mod_scgi correctly set SCRIPT_NAME & 139 | PATH_INFO. 140 | 141 | 2006-12-13 Allan Saddi 142 | 143 | * Fix problem in session.py seen when optimization is on. 144 | 145 | 2006-12-05 Allan Saddi 146 | 147 | * Update servers to default to an empty QUERY_STRING if 148 | not present in the environ. 149 | * Update gzip.py: compresslevel -> compress_level 150 | * Update gzip.py by updating docstrings and renaming 151 | classes/methods/functions to better follow Python naming 152 | conventions. NB: mimeTypes keyword parameter is now 153 | mime_types. 154 | 155 | 2006-12-02 Allan Saddi 156 | 157 | * Change intra-package imports into absolute imports. 158 | 159 | 2006-12-02 Allan Saddi 160 | 161 | * Add forceCookieOutput attribute to SessionService to 162 | force Set-Cookie output for the current request. 163 | 164 | 2006-12-01 Allan Saddi 165 | 166 | * Update setup script. 167 | 168 | 2006-11-26 Allan Saddi 169 | 170 | * Don't attempt to install signal handlers under Windows 171 | to improve compatibility. 172 | 173 | 2006-11-24 Allan Saddi 174 | 175 | * Add *_thread egg entry-point aliases. 176 | * Add UNIX domain socket support to scgi, scgi_fork, 177 | scgi_app. 178 | * Add flup.client package which contains various 179 | WSGI -> connector client implentations. (So far: FastCGI, 180 | and SCGI.) 181 | 182 | 2006-11-19 Allan Saddi 183 | 184 | * Change mime-type matching algorithm in GzipMiddleware. 185 | Strip parameters (e.g. "encoding") and accept a list of 186 | regexps. By default, compress 'text/.*' mime-types. 187 | 188 | 2006-11-10 Allan Saddi 189 | 190 | * Add cookieAttributes to SessionService to make it easier 191 | to customize the generated cookie's attributes. 192 | 193 | 2006-08-28 Allan Saddi 194 | 195 | * Add support for FastCGI roles other than FCGI_RESPONDER. 196 | Patch provided by Seairth Jacobs. 197 | 198 | 2006-08-02 Allan Saddi 199 | 200 | * Add cookieExpiration keyword to SessionService / 201 | SessionMiddleware to adjust the session cookie's expiration. 202 | Thanks to Blaise Laflamme for the suggestion. 203 | 204 | 2006-06-27 Allan Saddi 205 | 206 | * Set close-on-exec flag on all server sockets. Thanks to 207 | Ralf Schmitt for reporting the problem. 208 | 209 | 2006-06-18 Allan Saddi 210 | 211 | * Stop ignoring EPIPE exceptions, as this is probably the 212 | wrong thing to do. (Application is unaware of disconnected 213 | clients and the CPU spins when sending large files to a 214 | disconnected client.) Thanks to Ivan Sagalaev for bringing 215 | this to my attention. 216 | 217 | NB: Existing applications that use the flup servers may begin 218 | seeing socket.error exceptions... 219 | 220 | 2006-05-18 Allan Saddi 221 | 222 | * Added umask keyword parameter to fcgi and fcgi_fork, 223 | for use when binding to a UNIX socket. 224 | 225 | 2006-05-03 Allan Saddi 226 | 227 | * Fix illusive problem with AJP implementation. Thanks to 228 | Moshe Van der Sterre for explaining the problem and 229 | providing a fix. 230 | 231 | 2006-04-06 Allan Saddi 232 | 233 | * Catch a strange FieldStorage case. Seen in production. 234 | Not quite sure what causes it. 235 | 236 | 2006-03-21 Allan Saddi 237 | 238 | * Add maxRequests option to PreforkServer. Patch provided by 239 | Wojtek Sobczuk. 240 | 241 | 2006-02-23 Allan Saddi 242 | 243 | * Add paste.server_factory-compliant factories and respective 244 | egg entry points. Thanks to Luis Bruno for the code. 245 | 246 | Add debug option to servers, which is True by default. 247 | Currently, only server-level error handling is affected. 248 | 249 | 2006-01-15 Allan Saddi 250 | 251 | * Change the behavior of ImportingModuleResolver when dealing 252 | with ImportErrors. Previously, it would act as if the module 253 | did not exist. Now, it propagates the exception to another 254 | level (outer middleware or WSGI). Reported by Scot Doyle. 255 | 256 | 2006-01-05 Allan Saddi 257 | 258 | * Improve Windows compatibility by conditionally installing 259 | SIGHUP handler. Thanks to Brad Miller for pointing out the 260 | problem and providing a fix. 261 | 262 | 2005-12-19 Allan Saddi 263 | 264 | * Fix socket leak in eunuchs socketpair() wrapper. Thanks to 265 | Georg Bauer for pointing this out. 266 | 267 | 2005-12-16 Allan Saddi 268 | 269 | * Switch to setuptools for egg support. 270 | * Add higher-level 404 error page support. Thanks to Scot Doyle 271 | for suggesting the idea and providing code. If you previously 272 | subclassed Publisher to provide a custom 404 error page, this 273 | is now broken. It will have to be massaged to fit the new 274 | calling convention. 275 | 276 | 2005-11-28 Allan Saddi 277 | 278 | * Fix issue with FCGI_GET_VALUES handling. Thanks to 279 | Timothy Wright for pointing this out. 280 | 281 | 2005-11-18 Allan Saddi 282 | 283 | * When running under Python < 2.4, attempt to use socketpair() 284 | from eunuchs module. 285 | 286 | 2005-09-07 Allan Saddi 287 | 288 | * Python 2.3 doesn't define socket.SHUT_WR, which affected 289 | the closing of the FastCGI socket with the server. This would 290 | cause output to hang. Thanks to Eugene Lazutkin for bringing 291 | the problem to my attention and going out of his way to help 292 | me debug it! 293 | 294 | 2005-07-03 Allan Saddi 295 | 296 | * Ensure session identifiers only contain ASCII characters when 297 | using a non-ASCII locale. Thanks to Ksenia Marasanova for the 298 | the fix. 299 | 300 | 2005-06-12 Allan Saddi 301 | 302 | * Cleanly close connection socket to avoid sending a TCP RST to 303 | the web server. (fcgi_base) Fix suggested by Dima Barsky. 304 | 305 | 2005-05-31 Allan Saddi 306 | 307 | * Take scriptName from the WSGI_SCRIPT_NAME environment variable 308 | passed from the web server, if present. 309 | * Check if scriptName is None, and if so, don't modify SCRIPT_NAME 310 | & PATH_INFO. For better compatibility with cgi2scgi. (scgi_base) 311 | 312 | 2005-05-18 Allan Saddi 313 | 314 | * Change default allowedServers for ajp and scgi to ['127.0.0.1']. 315 | * Accept PATH_INFO from environment for scgi servers, in case 316 | cgi2scgi is being used. Submitted by Ian Bicking. 317 | * Change threaded servers so wsgi.multiprocess is False by default. 318 | Allow it to be changed by keyword argument. 319 | * Fix wsgi.multiprocess for scgi_fork. (Set to True.) 320 | 321 | 2005-05-15 Allan Saddi 322 | 323 | * Prevent possible deadlock related to DiskSessionStore locking. 324 | * Add logic to SessionStore so that it will block if attempting to 325 | check out a Session that's already been checked out. 326 | 327 | 2005-05-14 Allan Saddi 328 | 329 | * Convert the use of decorators in session.py to something 330 | compatible with Python <2.4. 331 | 332 | 2005-04-23 Allan Saddi 333 | 334 | * Ensure that SessionStore.checkOutSession() never returns an 335 | invalidated Session. Reported by Rene Dudfield. 336 | -------------------------------------------------------------------------------- /flup/client/fcgi_app.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2006 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | __author__ = 'Allan Saddi ' 28 | __version__ = '$Revision$' 29 | 30 | import select 31 | import struct 32 | import socket 33 | import errno 34 | import types 35 | 36 | __all__ = ['FCGIApp'] 37 | 38 | # Constants from the spec. 39 | FCGI_LISTENSOCK_FILENO = 0 40 | 41 | FCGI_HEADER_LEN = 8 42 | 43 | FCGI_VERSION_1 = 1 44 | 45 | FCGI_BEGIN_REQUEST = 1 46 | FCGI_ABORT_REQUEST = 2 47 | FCGI_END_REQUEST = 3 48 | FCGI_PARAMS = 4 49 | FCGI_STDIN = 5 50 | FCGI_STDOUT = 6 51 | FCGI_STDERR = 7 52 | FCGI_DATA = 8 53 | FCGI_GET_VALUES = 9 54 | FCGI_GET_VALUES_RESULT = 10 55 | FCGI_UNKNOWN_TYPE = 11 56 | FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE 57 | 58 | FCGI_NULL_REQUEST_ID = 0 59 | 60 | FCGI_KEEP_CONN = 1 61 | 62 | FCGI_RESPONDER = 1 63 | FCGI_AUTHORIZER = 2 64 | FCGI_FILTER = 3 65 | 66 | FCGI_REQUEST_COMPLETE = 0 67 | FCGI_CANT_MPX_CONN = 1 68 | FCGI_OVERLOADED = 2 69 | FCGI_UNKNOWN_ROLE = 3 70 | 71 | FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' 72 | FCGI_MAX_REQS = 'FCGI_MAX_REQS' 73 | FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' 74 | 75 | FCGI_Header = '!BBHHBx' 76 | FCGI_BeginRequestBody = '!HB5x' 77 | FCGI_EndRequestBody = '!LB3x' 78 | FCGI_UnknownTypeBody = '!B7x' 79 | 80 | FCGI_BeginRequestBody_LEN = struct.calcsize(FCGI_BeginRequestBody) 81 | FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody) 82 | FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody) 83 | 84 | if __debug__: 85 | import time 86 | 87 | # Set non-zero to write debug output to a file. 88 | DEBUG = 0 89 | DEBUGLOG = '/tmp/fcgi_app.log' 90 | 91 | def _debug(level, msg): 92 | if DEBUG < level: 93 | return 94 | 95 | try: 96 | f = open(DEBUGLOG, 'a') 97 | f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg)) 98 | f.close() 99 | except: 100 | pass 101 | 102 | def decode_pair(s, pos=0): 103 | """ 104 | Decodes a name/value pair. 105 | 106 | The number of bytes decoded as well as the name/value pair 107 | are returned. 108 | """ 109 | nameLength = s[pos] 110 | if nameLength & 128: 111 | nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff 112 | pos += 4 113 | else: 114 | pos += 1 115 | 116 | valueLength = s[pos] 117 | if valueLength & 128: 118 | valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff 119 | pos += 4 120 | else: 121 | pos += 1 122 | 123 | name = s[pos:pos+nameLength] 124 | pos += nameLength 125 | value = s[pos:pos+valueLength] 126 | pos += valueLength 127 | 128 | return (pos, (name, value)) 129 | 130 | def encode_pair(name, value): 131 | """ 132 | Encodes a name/value pair. 133 | 134 | The encoded string is returned. 135 | """ 136 | nameLength = len(name) 137 | if nameLength < 128: 138 | s = bytes([nameLength]) 139 | else: 140 | s = struct.pack('!L', nameLength | 0x80000000) 141 | 142 | valueLength = len(value) 143 | if valueLength < 128: 144 | s += bytes([valueLength]) 145 | else: 146 | s += struct.pack('!L', valueLength | 0x80000000) 147 | 148 | return s + name + value 149 | 150 | class Record(object): 151 | """ 152 | A FastCGI Record. 153 | 154 | Used for encoding/decoding records. 155 | """ 156 | def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID): 157 | self.version = FCGI_VERSION_1 158 | self.type = type 159 | self.requestId = requestId 160 | self.contentLength = 0 161 | self.paddingLength = 0 162 | self.contentData = b'' 163 | 164 | def _recvall(sock, length): 165 | """ 166 | Attempts to receive length bytes from a socket, blocking if necessary. 167 | (Socket may be blocking or non-blocking.) 168 | """ 169 | dataList = [] 170 | recvLen = 0 171 | while length: 172 | try: 173 | data = sock.recv(length) 174 | except socket.error as e: 175 | if e.errno == errno.EAGAIN: 176 | select.select([sock], [], []) 177 | continue 178 | else: 179 | raise 180 | if not data: # EOF 181 | break 182 | dataList.append(data) 183 | dataLen = len(data) 184 | recvLen += dataLen 185 | length -= dataLen 186 | return b''.join(dataList), recvLen 187 | _recvall = staticmethod(_recvall) 188 | 189 | def read(self, sock): 190 | """Read and decode a Record from a socket.""" 191 | try: 192 | header, length = self._recvall(sock, FCGI_HEADER_LEN) 193 | except: 194 | raise EOFError 195 | 196 | if length < FCGI_HEADER_LEN: 197 | raise EOFError 198 | 199 | self.version, self.type, self.requestId, self.contentLength, \ 200 | self.paddingLength = struct.unpack(FCGI_Header, header) 201 | 202 | if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, ' 203 | 'contentLength = %d' % 204 | (sock.fileno(), self.type, self.requestId, 205 | self.contentLength)) 206 | 207 | if self.contentLength: 208 | try: 209 | self.contentData, length = self._recvall(sock, 210 | self.contentLength) 211 | except: 212 | raise EOFError 213 | 214 | if length < self.contentLength: 215 | raise EOFError 216 | 217 | if self.paddingLength: 218 | try: 219 | self._recvall(sock, self.paddingLength) 220 | except: 221 | raise EOFError 222 | 223 | def _sendall(sock, data): 224 | """ 225 | Writes data to a socket and does not return until all the data is sent. 226 | """ 227 | length = len(data) 228 | while length: 229 | try: 230 | sent = sock.send(data) 231 | except socket.error as e: 232 | if e.errno == errno.EAGAIN: 233 | select.select([], [sock], []) 234 | continue 235 | else: 236 | raise 237 | data = data[sent:] 238 | length -= sent 239 | _sendall = staticmethod(_sendall) 240 | 241 | def write(self, sock): 242 | """Encode and write a Record to a socket.""" 243 | self.paddingLength = -self.contentLength & 7 244 | 245 | if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, ' 246 | 'contentLength = %d' % 247 | (sock.fileno(), self.type, self.requestId, 248 | self.contentLength)) 249 | 250 | header = struct.pack(FCGI_Header, self.version, self.type, 251 | self.requestId, self.contentLength, 252 | self.paddingLength) 253 | self._sendall(sock, header) 254 | if self.contentLength: 255 | self._sendall(sock, self.contentData) 256 | if self.paddingLength: 257 | self._sendall(sock, b'\x00'*self.paddingLength) 258 | 259 | class FCGIApp(object): 260 | def __init__(self, command=None, connect=None, host=None, port=None, 261 | filterEnviron=True): 262 | if host is not None: 263 | assert port is not None 264 | connect=(host, port) 265 | 266 | assert (command is not None and connect is None) or \ 267 | (command is None and connect is not None) 268 | 269 | self._command = command 270 | self._connect = connect 271 | 272 | self._filterEnviron = filterEnviron 273 | 274 | #sock = self._getConnection() 275 | #print self._fcgiGetValues(sock, ['FCGI_MAX_CONNS', 'FCGI_MAX_REQS', 'FCGI_MPXS_CONNS']) 276 | #sock.close() 277 | 278 | def __call__(self, environ, start_response): 279 | # For sanity's sake, we don't care about FCGI_MPXS_CONN 280 | # (connection multiplexing). For every request, we obtain a new 281 | # transport socket, perform the request, then discard the socket. 282 | # This is, I believe, how mod_fastcgi does things... 283 | 284 | sock = self._getConnection() 285 | 286 | # Since this is going to be the only request on this connection, 287 | # set the request ID to 1. 288 | requestId = 1 289 | 290 | # Begin the request 291 | rec = Record(FCGI_BEGIN_REQUEST, requestId) 292 | rec.contentData = struct.pack(FCGI_BeginRequestBody, FCGI_RESPONDER, 0) 293 | rec.contentLength = FCGI_BeginRequestBody_LEN 294 | rec.write(sock) 295 | 296 | # Filter WSGI environ and send it as FCGI_PARAMS 297 | if self._filterEnviron: 298 | params = self._defaultFilterEnviron(environ) 299 | else: 300 | params = self._lightFilterEnviron(environ) 301 | # TODO: Anything not from environ that needs to be sent also? 302 | self._fcgiParams(sock, requestId, params) 303 | self._fcgiParams(sock, requestId, {}) 304 | 305 | # Transfer wsgi.input to FCGI_STDIN 306 | content_length = int(environ.get('CONTENT_LENGTH') or 0) 307 | while True: 308 | chunk_size = min(content_length, 4096) 309 | s = environ['wsgi.input'].read(chunk_size) 310 | content_length -= len(s) 311 | rec = Record(FCGI_STDIN, requestId) 312 | rec.contentData = s 313 | rec.contentLength = len(s) 314 | rec.write(sock) 315 | 316 | if not s: break 317 | 318 | # Empty FCGI_DATA stream 319 | rec = Record(FCGI_DATA, requestId) 320 | rec.write(sock) 321 | 322 | # Main loop. Process FCGI_STDOUT, FCGI_STDERR, FCGI_END_REQUEST 323 | # records from the application. 324 | result = [] 325 | while True: 326 | inrec = Record() 327 | inrec.read(sock) 328 | if inrec.type == FCGI_STDOUT: 329 | if inrec.contentData: 330 | result.append(inrec.contentData) 331 | else: 332 | # TODO: Should probably be pedantic and no longer 333 | # accept FCGI_STDOUT records? 334 | pass 335 | elif inrec.type == FCGI_STDERR: 336 | # Simply forward to wsgi.errors 337 | environ['wsgi.errors'].write(inrec.contentData.decode(sys.getdefaultencoding())) 338 | elif inrec.type == FCGI_END_REQUEST: 339 | # TODO: Process appStatus/protocolStatus fields? 340 | break 341 | 342 | # Done with this transport socket, close it. (FCGI_KEEP_CONN was not 343 | # set in the FCGI_BEGIN_REQUEST record we sent above. So the 344 | # application is expected to do the same.) 345 | sock.close() 346 | 347 | result = b''.join(result) 348 | 349 | # Parse response headers from FCGI_STDOUT 350 | status = b'200 OK' 351 | headers = [] 352 | pos = 0 353 | while True: 354 | eolpos = result.find(b'\n', pos) 355 | if eolpos < 0: break 356 | line = result[pos:eolpos-1] 357 | pos = eolpos + 1 358 | 359 | # strip in case of CR. NB: This will also strip other 360 | # whitespace... 361 | line = line.strip() 362 | 363 | # Empty line signifies end of headers 364 | if not line: break 365 | 366 | # TODO: Better error handling 367 | header, value = line.split(b':', 1) 368 | header = header.strip().lower() 369 | value = value.strip() 370 | 371 | if header == b'status': 372 | # Special handling of Status header 373 | status = value 374 | if status.find(b' ') < 0: 375 | # Append a dummy reason phrase if one was not provided 376 | status += b' FCGIApp' 377 | else: 378 | headers.append((header, value)) 379 | 380 | result = result[pos:] 381 | 382 | # Set WSGI status, headers, and return result. 383 | start_response(status, headers) 384 | return [result] 385 | 386 | def _getConnection(self): 387 | if self._connect is not None: 388 | # The simple case. Create a socket and connect to the 389 | # application. 390 | if isinstance(self._connect, types.StringTypes): 391 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 392 | sock.connect(self._connect) 393 | elif hasattr(socket, 'create_connection'): 394 | sock = socket.create_connection(self._connect) 395 | else: 396 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 397 | sock.connect(self._connect) 398 | return sock 399 | 400 | # To be done when I have more time... 401 | raise NotImplementedError('Launching and managing FastCGI programs not yet implemented') 402 | 403 | def _fcgiGetValues(self, sock, vars): 404 | # Construct FCGI_GET_VALUES record 405 | outrec = Record(FCGI_GET_VALUES) 406 | data = [] 407 | for name in vars: 408 | data.append(encode_pair(name, b'')) 409 | data = b''.join(data) 410 | outrec.contentData = data 411 | outrec.contentLength = len(data) 412 | outrec.write(sock) 413 | 414 | # Await response 415 | inrec = Record() 416 | inrec.read(sock) 417 | result = {} 418 | if inrec.type == FCGI_GET_VALUES_RESULT: 419 | pos = 0 420 | while pos < inrec.contentLength: 421 | pos, (name, value) = decode_pair(inrec.contentData, pos) 422 | result[name] = value 423 | return result 424 | 425 | def _fcgiParams(self, sock, requestId, params): 426 | rec = Record(FCGI_PARAMS, requestId) 427 | data = [] 428 | for name,value in list(params.items()): 429 | data.append(encode_pair(name.encode('latin-1'), value.encode('latin-1'))) 430 | data = b''.join(data) 431 | rec.contentData = data 432 | rec.contentLength = len(data) 433 | rec.write(sock) 434 | 435 | _environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_', 436 | 'CONTENT_'] 437 | _environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE'] 438 | _environRenames = {} 439 | 440 | def _defaultFilterEnviron(self, environ): 441 | result = {} 442 | for n in list(environ.keys()): 443 | for p in self._environPrefixes: 444 | if n.startswith(p): 445 | result[n] = environ[n] 446 | if n in self._environCopies: 447 | result[n] = environ[n] 448 | if n in self._environRenames: 449 | result[self._environRenames[n]] = environ[n] 450 | 451 | return result 452 | 453 | def _lightFilterEnviron(self, environ): 454 | result = {} 455 | for n in list(environ.keys()): 456 | if n.upper() == n: 457 | result[n] = environ[n] 458 | return result 459 | 460 | if __name__ == '__main__': 461 | from flup.server.ajp import WSGIServer 462 | app = FCGIApp(connect=('localhost', 4242)) 463 | #import paste.lint 464 | #app = paste.lint.middleware(app) 465 | WSGIServer(app).run() 466 | -------------------------------------------------------------------------------- /flup/server/preforkserver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | __author__ = 'Allan Saddi ' 28 | __version__ = '$Revision$' 29 | 30 | import sys 31 | import os 32 | import socket 33 | import select 34 | import errno 35 | import signal 36 | import random 37 | import time 38 | 39 | try: 40 | import fcntl 41 | except ImportError: 42 | def setCloseOnExec(sock): 43 | pass 44 | else: 45 | def setCloseOnExec(sock): 46 | fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC) 47 | 48 | # If running Python < 2.4, require eunuchs module for socket.socketpair(). 49 | # See . 50 | if not hasattr(socket, 'socketpair'): 51 | try: 52 | import eunuchs.socketpair 53 | except ImportError: 54 | # TODO: Other alternatives? Perhaps using os.pipe()? 55 | raise ImportError('Requires eunuchs module for Python < 2.4') 56 | 57 | def socketpair(): 58 | s1, s2 = eunuchs.socketpair.socketpair() 59 | p, c = (socket.fromfd(s1, socket.AF_UNIX, socket.SOCK_STREAM), 60 | socket.fromfd(s2, socket.AF_UNIX, socket.SOCK_STREAM)) 61 | os.close(s1) 62 | os.close(s2) 63 | return p, c 64 | 65 | socket.socketpair = socketpair 66 | 67 | class PreforkServer(object): 68 | """ 69 | A preforked server model conceptually similar to Apache httpd(2). At 70 | any given time, ensures there are at least minSpare children ready to 71 | process new requests (up to a maximum of maxChildren children total). 72 | If the number of idle children is ever above maxSpare, the extra 73 | children are killed. 74 | 75 | If maxRequests is positive, each child will only handle that many 76 | requests in its lifetime before exiting. 77 | 78 | jobClass should be a class whose constructor takes at least two 79 | arguments: the client socket and client address. jobArgs, which 80 | must be a list or tuple, is any additional (static) arguments you 81 | wish to pass to the constructor. 82 | 83 | jobClass should have a run() method (taking no arguments) that does 84 | the actual work. When run() returns, the request is considered 85 | complete and the child process moves to idle state. 86 | 87 | -- added by Velko Ivanov 88 | sockTimeout is a timeout in seconds for the server's sockets. This 89 | should be 0 (pure non-blocking mode, default) or more (timeout mode). 90 | Fractional values (float) accepted. Setting this could help limit the 91 | damage in situations of bad connectivity. 92 | 93 | """ 94 | def __init__(self, minSpare=1, maxSpare=5, maxChildren=50, 95 | maxRequests=0, sockTimeout=0, jobClass=None, jobArgs=()): 96 | self._minSpare = minSpare 97 | self._maxSpare = maxSpare 98 | self._maxChildren = max(maxSpare, maxChildren) 99 | self._maxRequests = maxRequests 100 | self._sockTimeout = sockTimeout 101 | self._jobClass = jobClass 102 | self._jobArgs = jobArgs 103 | 104 | # Internal state of children. Maps pids to dictionaries with two 105 | # members: 'file' and 'avail'. 'file' is the socket to that 106 | # individidual child and 'avail' is whether or not the child is 107 | # free to process requests. 108 | self._children = {} 109 | 110 | self._children_to_purge = [] 111 | self._last_purge = 0 112 | 113 | if minSpare < 1: 114 | raise ValueError("minSpare must be at least 1!") 115 | if maxSpare < minSpare: 116 | raise ValueError("maxSpare must be greater than, or equal to, minSpare!") 117 | t = type(sockTimeout) 118 | if (t != int and t != float) or (sockTimeout < 0): 119 | raise ValueError("sockTimeout must be an int or float, greater than or equal to 0") 120 | 121 | def run(self, sock): 122 | """ 123 | The main loop. Pass a socket that is ready to accept() client 124 | connections. Return value will be True or False indiciating whether 125 | or not the loop was exited due to SIGHUP. 126 | """ 127 | # Set up signal handlers. 128 | self._keepGoing = True 129 | self._hupReceived = False 130 | self._installSignalHandlers() 131 | 132 | # Don't want operations on main socket to block. 133 | sock.setblocking(self._sockTimeout) 134 | 135 | # Set close-on-exec 136 | setCloseOnExec(sock) 137 | 138 | # Main loop. 139 | while self._keepGoing: 140 | # Maintain minimum number of children. Note that we are checking 141 | # the absolute number of children, not the number of "available" 142 | # children. We explicitly test against _maxSpare to maintain 143 | # an *optimistic* absolute minimum. The number of children will 144 | # always be in the range [_maxSpare, _maxChildren]. 145 | while len(self._children) < self._maxSpare: 146 | if not self._spawnChild(sock): break 147 | 148 | # Wait on any socket activity from live children. 149 | r = [x['file'] for x in list(self._children.values()) 150 | if x['file'] is not None] 151 | 152 | if len(r) == len(self._children) and not self._children_to_purge: 153 | timeout = None 154 | else: 155 | # There are dead children that need to be reaped, ensure 156 | # that they are by timing out, if necessary. Or there are some 157 | # children that need to die. 158 | timeout = 2 159 | 160 | w = [] 161 | if (time.time() > self._last_purge + 10): 162 | w = [x for x in self._children_to_purge if x.fileno() != -1] 163 | try: 164 | r, w, e = select.select(r, w, [], timeout) 165 | except select.error as e: 166 | if e.args[0] != errno.EINTR: 167 | raise 168 | 169 | # Scan child sockets and tend to those that need attention. 170 | for child in r: 171 | # Receive status byte. 172 | try: 173 | state = child.recv(1) 174 | except socket.error as e: 175 | if e.args[0] in (errno.EAGAIN, errno.EINTR): 176 | # Guess it really didn't need attention? 177 | continue 178 | raise 179 | # Try to match it with a child. (Do we need a reverse map?) 180 | for pid,d in list(self._children.items()): 181 | if child is d['file']: 182 | if state: 183 | # Set availability status accordingly. 184 | self._children[pid]['avail'] = state != '\x00' 185 | else: 186 | # Didn't receive anything. Child is most likely 187 | # dead. 188 | d = self._children[pid] 189 | d['file'].close() 190 | d['file'] = None 191 | d['avail'] = False 192 | 193 | for child in w: 194 | # purging child 195 | child.send('bye, bye') 196 | del self._children_to_purge[self._children_to_purge.index(child)] 197 | self._last_purge = time.time() 198 | 199 | # Try to match it with a child. (Do we need a reverse map?) 200 | for pid,d in self._children.items(): 201 | if child is d['file']: 202 | d['file'].close() 203 | d['file'] = None 204 | d['avail'] = False 205 | break 206 | 207 | # Reap children. 208 | self._reapChildren() 209 | 210 | # See who and how many children are available. 211 | availList = [x for x in list(self._children.items()) if x[1]['avail']] 212 | avail = len(availList) 213 | 214 | if avail < self._minSpare: 215 | # Need to spawn more children. 216 | while avail < self._minSpare and \ 217 | len(self._children) < self._maxChildren: 218 | if not self._spawnChild(sock): break 219 | avail += 1 220 | elif avail > self._maxSpare: 221 | # Too many spares, kill off the extras. 222 | pids = [x[0] for x in availList] 223 | pids.sort() 224 | pids = pids[self._maxSpare:] 225 | for pid in pids: 226 | d = self._children[pid] 227 | d['file'].close() 228 | d['file'] = None 229 | d['avail'] = False 230 | 231 | # Clean up all child processes. 232 | self._cleanupChildren() 233 | 234 | # Restore signal handlers. 235 | self._restoreSignalHandlers() 236 | 237 | # Return bool based on whether or not SIGHUP was received. 238 | return self._hupReceived 239 | 240 | def _cleanupChildren(self): 241 | """ 242 | Closes all child sockets (letting those that are available know 243 | that it's time to exit). Sends SIGINT to those that are currently 244 | processing (and hopes that it finishses ASAP). 245 | 246 | Any children remaining after 10 seconds is SIGKILLed. 247 | """ 248 | # Let all children know it's time to go. 249 | for pid,d in list(self._children.items()): 250 | if d['file'] is not None: 251 | d['file'].close() 252 | d['file'] = None 253 | if not d['avail']: 254 | # Child is unavailable. SIGINT it. 255 | try: 256 | os.kill(pid, signal.SIGINT) 257 | except OSError as e: 258 | if e.args[0] != errno.ESRCH: 259 | raise 260 | 261 | def alrmHandler(signum, frame): 262 | pass 263 | 264 | # Set up alarm to wake us up after 10 seconds. 265 | oldSIGALRM = signal.getsignal(signal.SIGALRM) 266 | signal.signal(signal.SIGALRM, alrmHandler) 267 | signal.alarm(10) 268 | 269 | # Wait for all children to die. 270 | while len(self._children): 271 | try: 272 | pid, status = os.wait() 273 | except OSError as e: 274 | if e.args[0] in (errno.ECHILD, errno.EINTR): 275 | break 276 | if pid in self._children: 277 | del self._children[pid] 278 | 279 | signal.alarm(0) 280 | signal.signal(signal.SIGALRM, oldSIGALRM) 281 | 282 | # Forcefully kill any remaining children. 283 | for pid in list(self._children.keys()): 284 | try: 285 | os.kill(pid, signal.SIGKILL) 286 | except OSError as e: 287 | if e.args[0] != errno.ESRCH: 288 | raise 289 | 290 | def _reapChildren(self): 291 | """Cleans up self._children whenever children die.""" 292 | while True: 293 | try: 294 | pid, status = os.waitpid(-1, os.WNOHANG) 295 | except OSError as e: 296 | if e.args[0] == errno.ECHILD: 297 | break 298 | raise 299 | if pid <= 0: 300 | break 301 | if pid in self._children: # Sanity check. 302 | if self._children[pid]['file'] is not None: 303 | self._children[pid]['file'].close() 304 | self._children[pid]['file'] = None 305 | del self._children[pid] 306 | 307 | def _spawnChild(self, sock): 308 | """ 309 | Spawn a single child. Returns True if successful, False otherwise. 310 | """ 311 | # This socket pair is used for very simple communication between 312 | # the parent and its children. 313 | parent, child = socket.socketpair() 314 | parent.setblocking(0) 315 | setCloseOnExec(parent) 316 | child.setblocking(0) 317 | setCloseOnExec(child) 318 | try: 319 | pid = os.fork() 320 | except OSError as e: 321 | if e.args[0] in (errno.EAGAIN, errno.ENOMEM): 322 | return False # Can't fork anymore. 323 | raise 324 | if not pid: 325 | # Child 326 | child.close() 327 | # Put child into its own process group. 328 | pid = os.getpid() 329 | os.setpgid(pid, pid) 330 | # Restore signal handlers. 331 | self._restoreSignalHandlers() 332 | # Close copies of child sockets. 333 | for f in [x['file'] for x in list(self._children.values()) 334 | if x['file'] is not None]: 335 | f.close() 336 | self._children = {} 337 | try: 338 | # Enter main loop. 339 | self._child(sock, parent) 340 | except KeyboardInterrupt: 341 | pass 342 | sys.exit(0) 343 | else: 344 | # Parent 345 | parent.close() 346 | d = self._children[pid] = {} 347 | d['file'] = child 348 | d['avail'] = True 349 | return True 350 | 351 | def _isClientAllowed(self, addr): 352 | """Override to provide access control.""" 353 | return True 354 | 355 | def _notifyParent(self, parent, msg): 356 | """Send message to parent, ignoring EPIPE and retrying on EAGAIN""" 357 | while True: 358 | try: 359 | parent.send(msg) 360 | return True 361 | except socket.error as e: 362 | if e.args[0] == errno.EPIPE: 363 | return False # Parent is gone 364 | if e.args[0] == errno.EAGAIN: 365 | # Wait for socket change before sending again 366 | select.select([], [parent], []) 367 | else: 368 | raise 369 | 370 | def _child(self, sock, parent): 371 | """Main loop for children.""" 372 | requestCount = 0 373 | 374 | # Re-seed random module 375 | preseed = '' 376 | # urandom only exists in Python >= 2.4 377 | if hasattr(os, 'urandom'): 378 | try: 379 | preseed = os.urandom(16) 380 | except NotImplementedError: 381 | pass 382 | # Have doubts about this. random.seed will just hash the string 383 | random.seed('%s%s%s' % (preseed, os.getpid(), time.time())) 384 | del preseed 385 | 386 | while True: 387 | # Wait for any activity on the main socket or parent socket. 388 | r, w, e = select.select([sock, parent], [], []) 389 | 390 | for f in r: 391 | # If there's any activity on the parent socket, it 392 | # means the parent wants us to die or has died itself. 393 | # Either way, exit. 394 | if f is parent: 395 | return 396 | 397 | # Otherwise, there's activity on the main socket... 398 | try: 399 | clientSock, addr = sock.accept() 400 | except socket.error as e: 401 | if e.args[0] == errno.EAGAIN: 402 | # Or maybe not. 403 | continue 404 | raise 405 | 406 | setCloseOnExec(clientSock) 407 | 408 | # Check if this client is allowed. 409 | if not self._isClientAllowed(addr): 410 | clientSock.close() 411 | continue 412 | 413 | # Notify parent we're no longer available. 414 | self._notifyParent(parent, b'\x00') 415 | 416 | # Do the job. 417 | self._jobClass(clientSock, addr, *self._jobArgs).run() 418 | 419 | # If we've serviced the maximum number of requests, exit. 420 | if self._maxRequests > 0: 421 | requestCount += 1 422 | if requestCount >= self._maxRequests: 423 | break 424 | 425 | # Tell parent we're free again. 426 | if not self._notifyParent(parent, b'\xff'): 427 | return # Parent is gone. 428 | 429 | # Signal handlers 430 | 431 | def _hupHandler(self, signum, frame): 432 | self._keepGoing = False 433 | self._hupReceived = True 434 | 435 | def _intHandler(self, signum, frame): 436 | self._keepGoing = False 437 | 438 | def _chldHandler(self, signum, frame): 439 | # Do nothing (breaks us out of select and allows us to reap children). 440 | pass 441 | 442 | def _usr1Handler(self, signum, frame): 443 | self._children_to_purge = [x['file'] for x in self._children.values() 444 | if x['file'] is not None] 445 | 446 | def _installSignalHandlers(self): 447 | supportedSignals = [signal.SIGINT, signal.SIGTERM] 448 | if hasattr(signal, 'SIGHUP'): 449 | supportedSignals.append(signal.SIGHUP) 450 | if hasattr(signal, 'SIGUSR1'): 451 | supportedSignals.append(signal.SIGUSR1) 452 | 453 | self._oldSIGs = [(x,signal.getsignal(x)) for x in supportedSignals] 454 | 455 | for sig in supportedSignals: 456 | if hasattr(signal, 'SIGHUP') and sig == signal.SIGHUP: 457 | signal.signal(sig, self._hupHandler) 458 | elif hasattr(signal, 'SIGUSR1') and sig == signal.SIGUSR1: 459 | signal.signal(sig, self._usr1Handler) 460 | else: 461 | signal.signal(sig, self._intHandler) 462 | 463 | def _restoreSignalHandlers(self): 464 | """Restores previous signal handlers.""" 465 | for signum,handler in self._oldSIGs: 466 | signal.signal(signum, handler) 467 | 468 | if __name__ == '__main__': 469 | class TestJob(object): 470 | def __init__(self, sock, addr): 471 | self._sock = sock 472 | self._addr = addr 473 | def run(self): 474 | print("Client connection opened from %s:%d" % self._addr) 475 | self._sock.send('Hello World!\n') 476 | self._sock.setblocking(1) 477 | self._sock.recv(1) 478 | self._sock.close() 479 | print("Client connection closed from %s:%d" % self._addr) 480 | sock = socket.socket() 481 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 482 | sock.bind(('', 8080)) 483 | sock.listen(socket.SOMAXCONN) 484 | PreforkServer(maxChildren=10, jobClass=TestJob).run(sock) 485 | -------------------------------------------------------------------------------- /flup/server/scgi_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005, 2006 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | __author__ = 'Allan Saddi ' 28 | __version__ = '$Revision$' 29 | 30 | import sys 31 | import logging 32 | import socket 33 | import select 34 | import errno 35 | import io as StringIO 36 | import signal 37 | import datetime 38 | import os 39 | import warnings 40 | import traceback 41 | 42 | # Threads are required. If you want a non-threaded (forking) version, look at 43 | # SWAP . 44 | import _thread 45 | import threading 46 | 47 | __all__ = ['BaseSCGIServer'] 48 | 49 | from flup.server import NoDefault 50 | 51 | # The main classes use this name for logging. 52 | LoggerName = 'scgi-wsgi' 53 | 54 | # Set up module-level logger. 55 | console = logging.StreamHandler() 56 | console.setLevel(logging.DEBUG) 57 | console.setFormatter(logging.Formatter('%(asctime)s : %(message)s', 58 | '%Y-%m-%d %H:%M:%S')) 59 | logging.getLogger(LoggerName).addHandler(console) 60 | del console 61 | 62 | class ProtocolError(Exception): 63 | """ 64 | Exception raised when the server does something unexpected or 65 | sends garbled data. Usually leads to a Connection closing. 66 | """ 67 | pass 68 | 69 | def recvall(sock, length): 70 | """ 71 | Attempts to receive length bytes from a socket, blocking if necessary. 72 | (Socket may be blocking or non-blocking.) 73 | """ 74 | dataList = [] 75 | recvLen = 0 76 | while length: 77 | try: 78 | data = sock.recv(length) 79 | except socket.error as e: 80 | if e.args[0] == errno.EAGAIN: 81 | select.select([sock], [], []) 82 | continue 83 | else: 84 | raise 85 | if not data: # EOF 86 | break 87 | dataList.append(data) 88 | dataLen = len(data) 89 | recvLen += dataLen 90 | length -= dataLen 91 | return b''.join(dataList), recvLen 92 | 93 | def readNetstring(sock): 94 | """ 95 | Attempt to read a netstring from a socket. 96 | """ 97 | # First attempt to read the length. 98 | size = b'' 99 | while True: 100 | try: 101 | c = sock.recv(1) 102 | except socket.error as e: 103 | if e.args[0] == errno.EAGAIN: 104 | select.select([sock], [], []) 105 | continue 106 | else: 107 | raise 108 | if c == b':': 109 | break 110 | if not c: 111 | raise EOFError 112 | size += c 113 | 114 | # Try to decode the length. 115 | try: 116 | size = int(size) 117 | if size < 0: 118 | raise ValueError 119 | except ValueError: 120 | raise ProtocolError('invalid netstring length') 121 | 122 | # Now read the string. 123 | s, length = recvall(sock, size) 124 | 125 | if length < size: 126 | raise EOFError 127 | 128 | # Lastly, the trailer. 129 | trailer, length = recvall(sock, 1) 130 | 131 | if length < 1: 132 | raise EOFError 133 | 134 | if trailer != b',': 135 | raise ProtocolError('invalid netstring trailer') 136 | 137 | return s 138 | 139 | class StdoutWrapper(object): 140 | """ 141 | Wrapper for sys.stdout so we know if data has actually been written. 142 | """ 143 | def __init__(self, stdout): 144 | self._file = stdout 145 | self.dataWritten = False 146 | 147 | def write(self, data): 148 | if data: 149 | self.dataWritten = True 150 | self._file.write(data) 151 | 152 | def writelines(self, lines): 153 | for line in lines: 154 | self.write(line) 155 | 156 | def __getattr__(self, name): 157 | return getattr(self._file, name) 158 | 159 | class Request(object): 160 | """ 161 | Encapsulates data related to a single request. 162 | 163 | Public attributes: 164 | environ - Environment variables from web server. 165 | stdin - File-like object representing the request body. 166 | stdout - File-like object for writing the response. 167 | """ 168 | def __init__(self, conn, environ, input, output): 169 | self._conn = conn 170 | self.environ = environ 171 | self.stdin = input 172 | self.stdout = StdoutWrapper(output) 173 | 174 | self.logger = logging.getLogger(LoggerName) 175 | 176 | def run(self): 177 | self.logger.info('%s %s%s', 178 | self.environ['REQUEST_METHOD'], 179 | self.environ.get('SCRIPT_NAME', ''), 180 | self.environ.get('PATH_INFO', '')) 181 | 182 | start = datetime.datetime.now() 183 | 184 | try: 185 | self._conn.server.handler(self) 186 | except: 187 | self.logger.exception('Exception caught from handler') 188 | if not self.stdout.dataWritten: 189 | self._conn.server.error(self) 190 | 191 | end = datetime.datetime.now() 192 | 193 | handlerTime = end - start 194 | self.logger.debug('%s %s%s done (%.3f secs)', 195 | self.environ['REQUEST_METHOD'], 196 | self.environ.get('SCRIPT_NAME', ''), 197 | self.environ.get('PATH_INFO', ''), 198 | handlerTime.seconds + 199 | handlerTime.microseconds / 1000000.0) 200 | 201 | class TimeoutException(Exception): 202 | pass 203 | 204 | class Connection(object): 205 | """ 206 | Represents a single client (web server) connection. A single request 207 | is handled, after which the socket is closed. 208 | """ 209 | def __init__(self, sock, addr, server, timeout): 210 | self._sock = sock 211 | self._addr = addr 212 | self.server = server 213 | self._timeout = timeout 214 | 215 | self.logger = logging.getLogger(LoggerName) 216 | 217 | def timeout_handler(self, signum, frame): 218 | self.logger.error('Timeout Exceeded') 219 | self.logger.error("\n".join(traceback.format_stack(frame))) 220 | 221 | raise TimeoutException 222 | 223 | def run(self): 224 | if len(self._addr) == 2: 225 | self.logger.debug('Connection starting up (%s:%d)', 226 | self._addr[0], self._addr[1]) 227 | 228 | try: 229 | self.processInput() 230 | except (EOFError, KeyboardInterrupt): 231 | pass 232 | except ProtocolError as e: 233 | self.logger.error("Protocol error '%s'", str(e)) 234 | except: 235 | self.logger.exception('Exception caught in Connection') 236 | 237 | if len(self._addr) == 2: 238 | self.logger.debug('Connection shutting down (%s:%d)', 239 | self._addr[0], self._addr[1]) 240 | 241 | # All done! 242 | self._sock.close() 243 | 244 | def processInput(self): 245 | # Read headers 246 | headers = readNetstring(self._sock) 247 | headers = headers.split(b'\x00')[:-1] 248 | if len(headers) % 2 != 0: 249 | raise ProtocolError('invalid headers') 250 | environ = {} 251 | for i in range(len(headers) // 2): 252 | environ[headers[2*i].decode('latin-1')] = headers[2*i+1].decode('latin-1') 253 | 254 | clen = environ.get('CONTENT_LENGTH') 255 | if clen is None: 256 | raise ProtocolError('missing CONTENT_LENGTH') 257 | try: 258 | clen = int(clen) 259 | if clen < 0: 260 | raise ValueError 261 | except ValueError: 262 | raise ProtocolError('invalid CONTENT_LENGTH') 263 | 264 | self._sock.setblocking(1) 265 | if clen: 266 | input = self._sock.makefile('rb') 267 | else: 268 | # Empty input. 269 | input = StringIO.StringIO() 270 | 271 | # stdout 272 | output = self._sock.makefile('wb') 273 | 274 | # Allocate Request 275 | req = Request(self, environ, input, output) 276 | 277 | # If there is a timeout 278 | if self._timeout: 279 | old_alarm = signal.signal(signal.SIGALRM, self.timeout_handler) 280 | signal.alarm(self._timeout) 281 | 282 | # Run it. 283 | req.run() 284 | 285 | output.close() 286 | input.close() 287 | 288 | # Restore old handler if timeout was given 289 | if self._timeout: 290 | signal.alarm(0) 291 | signal.signal(signal.SIGALRM, old_alarm) 292 | 293 | 294 | class BaseSCGIServer(object): 295 | # What Request class to use. 296 | requestClass = Request 297 | 298 | def __init__(self, application, scriptName=NoDefault, environ=None, 299 | multithreaded=True, multiprocess=False, 300 | bindAddress=('localhost', 4000), umask=None, 301 | allowedServers=NoDefault, 302 | loggingLevel=logging.INFO, debug=False): 303 | """ 304 | scriptName is the initial portion of the URL path that "belongs" 305 | to your application. It is used to determine PATH_INFO (which doesn't 306 | seem to be passed in). An empty scriptName means your application 307 | is mounted at the root of your virtual host. 308 | 309 | environ, which must be a dictionary, can contain any additional 310 | environment variables you want to pass to your application. 311 | 312 | Set multithreaded to False if your application is not thread-safe. 313 | 314 | Set multiprocess to True to explicitly set wsgi.multiprocess to 315 | True. (Only makes sense with threaded servers.) 316 | 317 | bindAddress is the address to bind to, which must be a string or 318 | a tuple of length 2. If a tuple, the first element must be a string, 319 | which is the host name or IPv4 address of a local interface. The 320 | 2nd element of the tuple is the port number. If a string, it will 321 | be interpreted as a filename and a UNIX socket will be opened. 322 | 323 | If binding to a UNIX socket, umask may be set to specify what 324 | the umask is to be changed to before the socket is created in the 325 | filesystem. After the socket is created, the previous umask is 326 | restored. 327 | 328 | allowedServers must be None or a list of strings representing the 329 | IPv4 addresses of servers allowed to connect. None means accept 330 | connections from anywhere. By default, it is a list containing 331 | the single item '127.0.0.1'. 332 | 333 | loggingLevel sets the logging level of the module-level logger. 334 | """ 335 | if environ is None: 336 | environ = {} 337 | 338 | self.application = application 339 | self.scriptName = scriptName 340 | self.environ = environ 341 | self.multithreaded = multithreaded 342 | self.multiprocess = multiprocess 343 | self.debug = debug 344 | self._bindAddress = bindAddress 345 | self._umask = umask 346 | if allowedServers is NoDefault: 347 | allowedServers = ['127.0.0.1'] 348 | self._allowedServers = allowedServers 349 | 350 | # Used to force single-threadedness. 351 | self._appLock = _thread.allocate_lock() 352 | 353 | self.logger = logging.getLogger(LoggerName) 354 | self.logger.setLevel(loggingLevel) 355 | 356 | def _setupSocket(self): 357 | """Creates and binds the socket for communication with the server.""" 358 | oldUmask = None 359 | if type(self._bindAddress) is str: 360 | # Unix socket 361 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 362 | try: 363 | os.unlink(self._bindAddress) 364 | except OSError: 365 | pass 366 | if self._umask is not None: 367 | oldUmask = os.umask(self._umask) 368 | else: 369 | # INET socket 370 | assert type(self._bindAddress) is tuple 371 | family = socket.AF_INET 372 | if len(self._bindAddress) > 2: 373 | family = socket.AF_INET6 374 | sock = socket.socket(family, socket.SOCK_STREAM) 375 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 376 | 377 | sock.bind(self._bindAddress) 378 | sock.listen(socket.SOMAXCONN) 379 | 380 | if oldUmask is not None: 381 | os.umask(oldUmask) 382 | 383 | return sock 384 | 385 | def _cleanupSocket(self, sock): 386 | """Closes the main socket.""" 387 | sock.close() 388 | 389 | def _isClientAllowed(self, addr): 390 | ret = self._allowedServers is None or \ 391 | len(addr) != 2 or \ 392 | (len(addr) == 2 and addr[0] in self._allowedServers) 393 | if not ret: 394 | self.logger.warning('Server connection from %s disallowed', 395 | addr[0]) 396 | return ret 397 | 398 | def handler(self, request): 399 | """ 400 | WSGI handler. Sets up WSGI environment, calls the application, 401 | and sends the application's response. 402 | """ 403 | environ = request.environ 404 | environ.update(self.environ) 405 | 406 | environ['wsgi.version'] = (1,0) 407 | environ['wsgi.input'] = request.stdin 408 | environ['wsgi.errors'] = sys.stderr 409 | environ['wsgi.multithread'] = self.multithreaded 410 | environ['wsgi.multiprocess'] = self.multiprocess 411 | environ['wsgi.run_once'] = False 412 | 413 | if environ.get('HTTPS', 'off') in ('on', '1'): 414 | environ['wsgi.url_scheme'] = 'https' 415 | else: 416 | environ['wsgi.url_scheme'] = 'http' 417 | 418 | self._sanitizeEnv(environ) 419 | 420 | headers_set = [] 421 | headers_sent = [] 422 | result = None 423 | 424 | def write(data): 425 | if type(data) is str: 426 | data = data.encode('latin-1') 427 | 428 | assert type(data) is bytes, 'write() argument must be bytes' 429 | assert headers_set, 'write() before start_response()' 430 | 431 | if not headers_sent: 432 | status, responseHeaders = headers_sent[:] = headers_set 433 | found = False 434 | for header,value in responseHeaders: 435 | if header.lower() == b'content-length': 436 | found = True 437 | break 438 | if not found and result is not None: 439 | try: 440 | if len(result) == 1: 441 | responseHeaders.append((b'Content-Length', 442 | str(len(data)).encode('latin-1'))) 443 | except: 444 | pass 445 | s = b'Status: ' + status + b'\r\n' 446 | for header,value in responseHeaders: 447 | s += header + b': ' + value + b'\r\n' 448 | s += b'\r\n' 449 | request.stdout.write(s) 450 | 451 | request.stdout.write(data) 452 | request.stdout.flush() 453 | 454 | def start_response(status, response_headers, exc_info=None): 455 | if exc_info: 456 | try: 457 | if headers_sent: 458 | # Re-raise if too late 459 | raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) 460 | finally: 461 | exc_info = None # avoid dangling circular ref 462 | else: 463 | assert not headers_set, 'Headers already set!' 464 | 465 | if type(status) is str: 466 | status = status.encode('latin-1') 467 | 468 | assert type(status) is bytes, 'Status must be a bytes' 469 | assert len(status) >= 4, 'Status must be at least 4 characters' 470 | assert int(status[:3]), 'Status must begin with 3-digit code' 471 | assert status[3] == 0x20, 'Status must have a space after code' 472 | assert type(response_headers) is list, 'Headers must be a list' 473 | new_response_headers = [] 474 | for name,val in response_headers: 475 | if type(name) is str: 476 | name = name.encode('latin-1') 477 | if type(val) is str: 478 | val = val.encode('latin-1') 479 | 480 | assert type(name) is bytes, 'Header name "%s" must be bytes' % name 481 | assert type(val) is bytes, 'Value of header "%s" must be bytes' % name 482 | 483 | new_response_headers.append((name, val)) 484 | 485 | headers_set[:] = [status, new_response_headers] 486 | return write 487 | 488 | if not self.multithreaded: 489 | self._appLock.acquire() 490 | try: 491 | try: 492 | result = self.application(environ, start_response) 493 | try: 494 | for data in result: 495 | if data: 496 | write(data) 497 | if not headers_sent: 498 | write(b'') # in case body was empty 499 | finally: 500 | if hasattr(result, 'close'): 501 | result.close() 502 | except socket.error as e: 503 | if e.args[0] != errno.EPIPE: 504 | raise # Don't let EPIPE propagate beyond server 505 | finally: 506 | if not self.multithreaded: 507 | self._appLock.release() 508 | 509 | def _sanitizeEnv(self, environ): 510 | """Fill-in/deduce missing values in environ.""" 511 | reqUri = None 512 | if 'REQUEST_URI' in environ: 513 | reqUri = environ['REQUEST_URI'].split('?', 1) 514 | 515 | # Ensure QUERY_STRING exists 516 | if 'QUERY_STRING' not in environ or not environ['QUERY_STRING']: 517 | if reqUri is not None and len(reqUri) > 1: 518 | environ['QUERY_STRING'] = reqUri[1] 519 | else: 520 | environ['QUERY_STRING'] = '' 521 | 522 | # Check WSGI_SCRIPT_NAME 523 | scriptName = environ.get('WSGI_SCRIPT_NAME') 524 | if scriptName is None: 525 | scriptName = self.scriptName 526 | else: 527 | warnings.warn('WSGI_SCRIPT_NAME environment variable for scgi ' 528 | 'servers is deprecated', 529 | DeprecationWarning) 530 | if scriptName.lower() == 'none': 531 | scriptName = None 532 | 533 | if scriptName is None: 534 | # Do nothing (most likely coming from cgi2scgi) 535 | return 536 | 537 | if scriptName is NoDefault: 538 | # Pull SCRIPT_NAME/PATH_INFO from environment, with empty defaults 539 | if 'SCRIPT_NAME' not in environ: 540 | environ['SCRIPT_NAME'] = '' 541 | if 'PATH_INFO' not in environ or not environ['PATH_INFO']: 542 | if reqUri is not None: 543 | scriptName = environ['SCRIPT_NAME'] 544 | if not reqUri[0].startswith(scriptName): 545 | self.logger.warning('SCRIPT_NAME does not match request URI') 546 | environ['PATH_INFO'] = reqUri[0][len(scriptName):] 547 | else: 548 | environ['PATH_INFO'] = '' 549 | else: 550 | # Configured scriptName 551 | warnings.warn('Configured SCRIPT_NAME is deprecated\n' 552 | 'Do not use WSGI_SCRIPT_NAME or the scriptName\n' 553 | 'keyword parameter -- they will be going away', 554 | DeprecationWarning) 555 | 556 | value = environ['SCRIPT_NAME'] 557 | value += environ.get('PATH_INFO', '') 558 | if not value.startswith(scriptName): 559 | self.logger.warning('scriptName does not match request URI') 560 | 561 | environ['PATH_INFO'] = value[len(scriptName):] 562 | environ['SCRIPT_NAME'] = scriptName 563 | 564 | def error(self, request): 565 | """ 566 | Override to provide custom error handling. Ideally, however, 567 | all errors should be caught at the application level. 568 | """ 569 | if self.debug: 570 | import cgitb 571 | request.stdout.write(b'Status: 500 Internal Server Error\r\n' + 572 | b'Content-Type: text/html\r\n\r\n' + 573 | cgitb.html(sys.exc_info()).encode('latin-1')) 574 | else: 575 | errorpage = b""" 576 | 577 | Unhandled Exception 578 | 579 |

Unhandled Exception

580 |

An unhandled exception was thrown by the application.

581 | 582 | """ 583 | request.stdout.write(b'Status: 500 Internal Server Error\r\n' + 584 | b'Content-Type: text/html\r\n\r\n' + 585 | errorpage) 586 | -------------------------------------------------------------------------------- /flup/server/ajp_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005, 2006 Allan Saddi 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | # $Id$ 26 | 27 | __author__ = 'Allan Saddi ' 28 | __version__ = '$Revision$' 29 | 30 | import sys 31 | import socket 32 | import select 33 | import struct 34 | import signal 35 | import logging 36 | import errno 37 | import datetime 38 | import time 39 | import traceback 40 | 41 | # Unfortunately, for now, threads are required. 42 | import _thread 43 | import threading 44 | 45 | from flup.server import NoDefault 46 | 47 | __all__ = ['BaseAJPServer'] 48 | 49 | # Packet header prefixes. 50 | SERVER_PREFIX = b'\x12\x34' 51 | CONTAINER_PREFIX = b'AB' 52 | 53 | # Server packet types. 54 | PKTTYPE_FWD_REQ = 0x02 55 | PKTTYPE_SHUTDOWN = 0x07 56 | PKTTYPE_PING = 0x08 57 | PKTTYPE_CPING = 0x0a 58 | 59 | # Container packet types. 60 | PKTTYPE_SEND_BODY = b'\x03' 61 | PKTTYPE_SEND_HEADERS = b'\x04' 62 | PKTTYPE_END_RESPONSE = b'\x05' 63 | PKTTYPE_GET_BODY = b'\x06' 64 | PKTTYPE_CPONG = b'\x09' 65 | 66 | # Code tables for methods/headers/attributes. 67 | methodTable = [ 68 | None, 69 | b'OPTIONS', 70 | b'GET', 71 | b'HEAD', 72 | b'POST', 73 | b'PUT', 74 | b'DELETE', 75 | b'TRACE', 76 | b'PROPFIND', 77 | b'PROPPATCH', 78 | b'MKCOL', 79 | b'COPY', 80 | b'MOVE', 81 | b'LOCK', 82 | b'UNLOCK', 83 | b'ACL', 84 | b'REPORT', 85 | b'VERSION-CONTROL', 86 | b'CHECKIN', 87 | b'CHECKOUT', 88 | b'UNCHECKOUT', 89 | b'SEARCH', 90 | b'MKWORKSPACE', 91 | b'UPDATE', 92 | b'LABEL', 93 | b'MERGE', 94 | b'BASELINE_CONTROL', 95 | b'MKACTIVITY' 96 | ] 97 | 98 | requestHeaderTable = [ 99 | None, 100 | b'Accept', 101 | b'Accept-Charset', 102 | b'Accept-Encoding', 103 | b'Accept-Language', 104 | b'Authorization', 105 | b'Connection', 106 | b'Content-Type', 107 | b'Content-Length', 108 | b'Cookie', 109 | b'Cookie2', 110 | b'Host', 111 | b'Pragma', 112 | b'Referer', 113 | b'User-Agent' 114 | ] 115 | 116 | attributeTable = [ 117 | None, 118 | b'CONTEXT', 119 | b'SERVLET_PATH', 120 | b'REMOTE_USER', 121 | b'AUTH_TYPE', 122 | b'QUERY_STRING', 123 | b'JVM_ROUTE', 124 | b'SSL_CERT', 125 | b'SSL_CIPHER', 126 | b'SSL_SESSION', 127 | None, # name follows 128 | b'SSL_KEY_SIZE' 129 | ] 130 | 131 | responseHeaderTable = [ 132 | None, 133 | b'content-type', 134 | b'content-language', 135 | b'content-length', 136 | b'date', 137 | b'last-modified', 138 | b'location', 139 | b'set-cookie', 140 | b'set-cookie2', 141 | b'servlet-engine', 142 | b'status', 143 | b'www-authenticate' 144 | ] 145 | 146 | # The main classes use this name for logging. 147 | LoggerName = 'ajp-wsgi' 148 | 149 | # Set up module-level logger. 150 | console = logging.StreamHandler() 151 | console.setLevel(logging.DEBUG) 152 | console.setFormatter(logging.Formatter('%(asctime)s : %(message)s', 153 | '%Y-%m-%d %H:%M:%S')) 154 | logging.getLogger(LoggerName).addHandler(console) 155 | del console 156 | 157 | class ProtocolError(Exception): 158 | """ 159 | Exception raised when the server does something unexpected or 160 | sends garbled data. Usually leads to a Connection closing. 161 | """ 162 | pass 163 | 164 | def decodeString(data, pos=0): 165 | """Decode a string.""" 166 | try: 167 | length = struct.unpack('>H', data[pos:pos+2])[0] 168 | pos += 2 169 | if length == 0xffff: # This was undocumented! 170 | return b'', pos 171 | s = data[pos:pos+length] 172 | return s, pos+length+1 # Don't forget NUL 173 | except Exception as e: 174 | raise ProtocolError('decodeString: '+str(e)) 175 | 176 | def decodeRequestHeader(data, pos=0): 177 | """Decode a request header/value pair.""" 178 | try: 179 | if data[pos] == 0xa0: 180 | # Use table 181 | i = data[pos+1] 182 | name = requestHeaderTable[i] 183 | if name is None: 184 | raise ValueError('bad request header code') 185 | pos += 2 186 | else: 187 | name, pos = decodeString(data, pos) 188 | value, pos = decodeString(data, pos) 189 | return name, value, pos 190 | except Exception as e: 191 | raise ProtocolError('decodeRequestHeader: '+str(e)) 192 | 193 | def decodeAttribute(data, pos=0): 194 | """Decode a request attribute.""" 195 | try: 196 | i = data[pos] 197 | pos += 1 198 | if i == 0xff: 199 | # end 200 | return None, None, pos 201 | elif i == 0x0a: 202 | # name follows 203 | name, pos = decodeString(data, pos) 204 | elif i == 0x0b: 205 | # Special handling of SSL_KEY_SIZE. 206 | name = attributeTable[i] 207 | # Value is an int, not a string. 208 | value = struct.unpack('>H', data[pos:pos+2])[0] 209 | return name, str(value), pos+2 210 | else: 211 | name = attributeTable[i] 212 | if name is None: 213 | raise ValueError('bad attribute code') 214 | value, pos = decodeString(data, pos) 215 | return name, value, pos 216 | except Exception as e: 217 | raise ProtocolError('decodeAttribute: '+str(e)) 218 | 219 | def encodeString(s): 220 | """Encode a string.""" 221 | return struct.pack('>H', len(s)) + s + b'\x00' 222 | 223 | def encodeResponseHeader(name, value): 224 | """Encode a response header/value pair.""" 225 | lname = name.lower() 226 | if lname in responseHeaderTable: 227 | # Use table 228 | i = responseHeaderTable.index(lname) 229 | out = b'\xa0' + bytes([i]) 230 | else: 231 | out = encodeString(name) 232 | out += encodeString(value) 233 | return out 234 | 235 | class Packet(object): 236 | """An AJP message packet.""" 237 | def __init__(self): 238 | self.data = b'' 239 | # Don't set this on write, it will be calculated automatically. 240 | self.length = 0 241 | 242 | def _recvall(sock, length): 243 | """ 244 | Attempts to receive length bytes from a socket, blocking if necessary. 245 | (Socket may be blocking or non-blocking.) 246 | """ 247 | dataList = [] 248 | recvLen = 0 249 | while length: 250 | try: 251 | data = sock.recv(length) 252 | except socket.error as e: 253 | if e.args[0] == errno.EAGAIN: 254 | select.select([sock], [], []) 255 | continue 256 | else: 257 | raise 258 | if not data: # EOF 259 | break 260 | dataList.append(data) 261 | dataLen = len(data) 262 | recvLen += dataLen 263 | length -= dataLen 264 | return b''.join(dataList), recvLen 265 | _recvall = staticmethod(_recvall) 266 | 267 | def read(self, sock): 268 | """Attempt to read a packet from the server.""" 269 | try: 270 | header, length = self._recvall(sock, 4) 271 | except socket.error: 272 | # Treat any sort of socket errors as EOF (close Connection). 273 | raise EOFError 274 | 275 | if length < 4: 276 | raise EOFError 277 | 278 | if header[:2] != SERVER_PREFIX: 279 | raise ProtocolError('invalid header') 280 | 281 | self.length = struct.unpack('>H', header[2:4])[0] 282 | if self.length: 283 | try: 284 | self.data, length = self._recvall(sock, self.length) 285 | except socket.error: 286 | raise EOFError 287 | 288 | if length < self.length: 289 | raise EOFError 290 | 291 | def _sendall(sock, data): 292 | """ 293 | Writes data to a socket and does not return until all the data is sent. 294 | """ 295 | length = len(data) 296 | while length: 297 | try: 298 | sent = sock.send(data) 299 | except socket.error as e: 300 | if e.args[0] == errno.EAGAIN: 301 | select.select([], [sock], []) 302 | continue 303 | else: 304 | raise 305 | data = data[sent:] 306 | length -= sent 307 | _sendall = staticmethod(_sendall) 308 | 309 | def write(self, sock): 310 | """Send a packet to the server.""" 311 | self.length = len(self.data) 312 | self._sendall(sock, CONTAINER_PREFIX + struct.pack('>H', self.length)) 313 | if self.length: 314 | self._sendall(sock, self.data) 315 | 316 | class InputStream(object): 317 | """ 318 | File-like object that represents the request body (if any). Supports 319 | the bare mininum methods required by the WSGI spec. Thanks to 320 | StringIO for ideas. 321 | """ 322 | def __init__(self, conn): 323 | self._conn = conn 324 | 325 | # See WSGIServer. 326 | self._shrinkThreshold = conn.server.inputStreamShrinkThreshold 327 | 328 | self._buf = b'' 329 | self._bufList = [] 330 | self._pos = 0 # Current read position. 331 | self._avail = 0 # Number of bytes currently available. 332 | self._length = 0 # Set to Content-Length in request. 333 | 334 | self.logger = logging.getLogger(LoggerName) 335 | 336 | def bytesAvailForAdd(self): 337 | return self._length - self._avail 338 | 339 | def _shrinkBuffer(self): 340 | """Gets rid of already read data (since we can't rewind).""" 341 | if self._pos >= self._shrinkThreshold: 342 | self._buf = self._buf[self._pos:] 343 | self._avail -= self._pos 344 | self._length -= self._pos 345 | self._pos = 0 346 | 347 | assert self._avail >= 0 and self._length >= 0 348 | 349 | def _waitForData(self): 350 | toAdd = min(self.bytesAvailForAdd(), 0xffff) 351 | assert toAdd > 0 352 | pkt = Packet() 353 | pkt.data = PKTTYPE_GET_BODY + \ 354 | struct.pack('>H', toAdd) 355 | self._conn.writePacket(pkt) 356 | self._conn.processInput() 357 | 358 | def read(self, n=-1): 359 | if self._pos == self._length: 360 | return b'' 361 | while True: 362 | if n < 0 or (self._avail - self._pos) < n: 363 | # Not enough data available. 364 | if not self.bytesAvailForAdd(): 365 | # And there's no more coming. 366 | newPos = self._avail 367 | break 368 | else: 369 | # Ask for more data and wait. 370 | self._waitForData() 371 | continue 372 | else: 373 | newPos = self._pos + n 374 | break 375 | # Merge buffer list, if necessary. 376 | if self._bufList: 377 | self._buf += b''.join(self._bufList) 378 | self._bufList = [] 379 | r = self._buf[self._pos:newPos] 380 | self._pos = newPos 381 | self._shrinkBuffer() 382 | return r 383 | 384 | def readline(self, length=None): 385 | if self._pos == self._length: 386 | return b'' 387 | while True: 388 | # Unfortunately, we need to merge the buffer list early. 389 | if self._bufList: 390 | self._buf += b''.join(self._bufList) 391 | self._bufList = [] 392 | # Find newline. 393 | i = self._buf.find(b'\n', self._pos) 394 | if i < 0: 395 | # Not found? 396 | if not self.bytesAvailForAdd(): 397 | # No more data coming. 398 | newPos = self._avail 399 | break 400 | else: 401 | if length is not None and len(self._buf) >= length + self._pos: 402 | newPos = self._pos + length 403 | break 404 | # Wait for more to come. 405 | self._waitForData() 406 | continue 407 | else: 408 | newPos = i + 1 409 | break 410 | r = self._buf[self._pos:newPos] 411 | self._pos = newPos 412 | self._shrinkBuffer() 413 | return r 414 | 415 | def readlines(self, sizehint=0): 416 | total = 0 417 | lines = [] 418 | line = self.readline() 419 | while line: 420 | lines.append(line) 421 | total += len(line) 422 | if 0 < sizehint <= total: 423 | break 424 | line = self.readline() 425 | return lines 426 | 427 | def __iter__(self): 428 | return self 429 | 430 | def __next__(self): 431 | r = self.readline() 432 | if not r: 433 | raise StopIteration 434 | return r 435 | 436 | def setDataLength(self, length): 437 | """ 438 | Once Content-Length is known, Request calls this method to set it. 439 | """ 440 | self._length = length 441 | 442 | def addData(self, data): 443 | """ 444 | Adds data from the server to this InputStream. Note that we never ask 445 | the server for data beyond the Content-Length, so the server should 446 | never send us an EOF (empty string argument). 447 | """ 448 | assert type(data) is bytes 449 | if not data: 450 | raise ProtocolError('short data') 451 | self._bufList.append(data) 452 | length = len(data) 453 | self._avail += length 454 | if self._avail > self._length: 455 | raise ProtocolError('too much data') 456 | 457 | class Request(object): 458 | """ 459 | A Request object. A more fitting name would probably be Transaction, but 460 | it's named Request to mirror my FastCGI driver. :) This object 461 | encapsulates all the data about the HTTP request and allows the handler 462 | to send a response. 463 | 464 | The only attributes/methods that the handler should concern itself 465 | with are: environ, input, startResponse(), and write(). 466 | """ 467 | # Do not ever change the following value. 468 | _maxWrite = 8192 - 4 - 3 - 1 # 8k - pkt header - send body header - NUL 469 | 470 | def __init__(self, conn): 471 | self._conn = conn 472 | 473 | self.environ = {} 474 | self.input = InputStream(conn) 475 | 476 | self._headersSent = False 477 | 478 | self.logger = logging.getLogger(LoggerName) 479 | 480 | def run(self): 481 | self.logger.info('%s %s', 482 | self.environ['REQUEST_METHOD'], 483 | self.environ['REQUEST_URI']) 484 | 485 | start = datetime.datetime.now() 486 | 487 | try: 488 | self._conn.server.handler(self) 489 | except: 490 | self.logger.exception('Exception caught from handler') 491 | if not self._headersSent: 492 | self._conn.server.error(self) 493 | 494 | end = datetime.datetime.now() 495 | 496 | # Notify server of end of response (reuse flag is set to true). 497 | pkt = Packet() 498 | pkt.data = PKTTYPE_END_RESPONSE + b'\x01' 499 | self._conn.writePacket(pkt) 500 | 501 | handlerTime = end - start 502 | self.logger.debug('%s %s done (%.3f secs)', 503 | self.environ['REQUEST_METHOD'], 504 | self.environ['REQUEST_URI'], 505 | handlerTime.seconds + 506 | handlerTime.microseconds / 1000000.0) 507 | 508 | # The following methods are called from the Connection to set up this 509 | # Request. 510 | 511 | def setMethod(self, value): 512 | self.environ['REQUEST_METHOD'] = value.decode('latin-1') 513 | 514 | def setProtocol(self, value): 515 | self.environ['SERVER_PROTOCOL'] = value.decode('latin-1') 516 | 517 | def setRequestURI(self, value): 518 | self.environ['REQUEST_URI'] = value.decode('latin-1') 519 | 520 | def setRemoteAddr(self, value): 521 | self.environ['REMOTE_ADDR'] = value.decode('latin-1') 522 | 523 | def setRemoteHost(self, value): 524 | self.environ['REMOTE_HOST'] = value.decode('latin-1') 525 | 526 | def setServerName(self, value): 527 | self.environ['SERVER_NAME'] = value.decode('latin-1') 528 | 529 | def setServerPort(self, value): 530 | self.environ['SERVER_PORT'] = str(value) 531 | 532 | def setIsSSL(self, value): 533 | if value: 534 | self.environ['HTTPS'] = 'on' 535 | 536 | def addHeader(self, name, value): 537 | name = name.replace(b'-', b'_').upper() 538 | if name in (b'CONTENT_TYPE', b'CONTENT_LENGTH'): 539 | self.environ[name.decode('latin-1')] = value.decode('latin-1') 540 | if name == b'CONTENT_LENGTH': 541 | length = int(value) 542 | self.input.setDataLength(length) 543 | else: 544 | self.environ[(b'HTTP_'+name).decode('latin-1')] = value.decode('latin-1') 545 | 546 | def addAttribute(self, name, value): 547 | self.environ[name.decode('latin-1')] = value.decode('latin-1') 548 | 549 | # The only two methods that should be called from the handler. 550 | 551 | def startResponse(self, statusCode, statusMsg, headers): 552 | """ 553 | Begin the HTTP response. This must only be called once and it 554 | must be called before any calls to write(). 555 | 556 | statusCode is the integer status code (e.g. 200). statusMsg 557 | is the associated reason message (e.g.'OK'). headers is a list 558 | of 2-tuples - header name/value pairs. (Both header name and value 559 | must be strings.) 560 | """ 561 | assert not self._headersSent, 'Headers already sent!' 562 | 563 | pkt = Packet() 564 | pkt.data = PKTTYPE_SEND_HEADERS + \ 565 | struct.pack('>H', statusCode) + \ 566 | encodeString(statusMsg) + \ 567 | struct.pack('>H', len(headers)) + \ 568 | b''.join([encodeResponseHeader(name, value) 569 | for name,value in headers]) 570 | 571 | self._conn.writePacket(pkt) 572 | 573 | self._headersSent = True 574 | 575 | def write(self, data): 576 | """ 577 | Write data (which comprises the response body). Note that due to 578 | restrictions on AJP packet size, we limit our writes to 8185 bytes 579 | each packet. 580 | """ 581 | assert self._headersSent, 'Headers must be sent first!' 582 | 583 | bytesLeft = len(data) 584 | while bytesLeft: 585 | toWrite = min(bytesLeft, self._maxWrite) 586 | 587 | pkt = Packet() 588 | pkt.data = PKTTYPE_SEND_BODY + \ 589 | struct.pack('>H', toWrite) + \ 590 | data[:toWrite] + b'\x00' # Undocumented 591 | self._conn.writePacket(pkt) 592 | 593 | data = data[toWrite:] 594 | bytesLeft -= toWrite 595 | 596 | class TimeoutException(Exception): 597 | pass 598 | 599 | class Connection(object): 600 | """ 601 | A single Connection with the server. Requests are not multiplexed over the 602 | same connection, so at any given time, the Connection is either 603 | waiting for a request, or processing a single request. 604 | """ 605 | def __init__(self, sock, addr, server, timeout): 606 | self.server = server 607 | self._sock = sock 608 | self._addr = addr 609 | self._timeout = timeout 610 | 611 | self._request = None 612 | 613 | self.logger = logging.getLogger(LoggerName) 614 | 615 | def timeout_handler(self, signum, frame): 616 | self.logger.error('Timeout Exceeded') 617 | self.logger.error("\n".join(traceback.format_stack(frame))) 618 | 619 | raise TimeoutException 620 | 621 | def run(self): 622 | self.logger.debug('Connection starting up (%s:%d)', 623 | self._addr[0], self._addr[1]) 624 | 625 | # Main loop. Errors will cause the loop to be exited and 626 | # the socket to be closed. 627 | while True: 628 | try: 629 | self.processInput() 630 | except ProtocolError as e: 631 | self.logger.error("Protocol error '%s'", str(e)) 632 | break 633 | except (EOFError, KeyboardInterrupt): 634 | break 635 | except: 636 | self.logger.exception('Exception caught in Connection') 637 | break 638 | 639 | self.logger.debug('Connection shutting down (%s:%d)', 640 | self._addr[0], self._addr[1]) 641 | 642 | self._sock.close() 643 | 644 | def processInput(self): 645 | """Wait for and process a single packet.""" 646 | pkt = Packet() 647 | select.select([self._sock], [], []) 648 | pkt.read(self._sock) 649 | 650 | # Body chunks have no packet type code. 651 | if self._request is not None: 652 | self._processBody(pkt) 653 | return 654 | 655 | if not pkt.length: 656 | raise ProtocolError('unexpected empty packet') 657 | 658 | pkttype = pkt.data[0] 659 | if pkttype == PKTTYPE_FWD_REQ: 660 | self._forwardRequest(pkt) 661 | elif pkttype == PKTTYPE_SHUTDOWN: 662 | self._shutdown(pkt) 663 | elif pkttype == PKTTYPE_PING: 664 | self._ping(pkt) 665 | elif pkttype == PKTTYPE_CPING: 666 | self._cping(pkt) 667 | else: 668 | raise ProtocolError('unknown packet type') 669 | 670 | def _forwardRequest(self, pkt): 671 | """ 672 | Creates a Request object, fills it in from the packet, then runs it. 673 | """ 674 | assert self._request is None 675 | 676 | req = self.server.requestClass(self) 677 | i = pkt.data[1] 678 | method = methodTable[i] 679 | if method is None: 680 | raise ValueError('bad method field') 681 | req.setMethod(method) 682 | value, pos = decodeString(pkt.data, 2) 683 | req.setProtocol(value) 684 | value, pos = decodeString(pkt.data, pos) 685 | req.setRequestURI(value) 686 | value, pos = decodeString(pkt.data, pos) 687 | req.setRemoteAddr(value) 688 | value, pos = decodeString(pkt.data, pos) 689 | req.setRemoteHost(value) 690 | value, pos = decodeString(pkt.data, pos) 691 | req.setServerName(value) 692 | value = struct.unpack('>H', pkt.data[pos:pos+2])[0] 693 | req.setServerPort(value) 694 | i = pkt.data[pos+2] 695 | req.setIsSSL(i != 0) 696 | 697 | # Request headers. 698 | numHeaders = struct.unpack('>H', pkt.data[pos+3:pos+5])[0] 699 | pos += 5 700 | for i in range(numHeaders): 701 | name, value, pos = decodeRequestHeader(pkt.data, pos) 702 | req.addHeader(name, value) 703 | 704 | # Attributes. 705 | while True: 706 | name, value, pos = decodeAttribute(pkt.data, pos) 707 | if name is None: 708 | break 709 | req.addAttribute(name, value) 710 | 711 | self._request = req 712 | 713 | # Read first body chunk, if needed. 714 | if req.input.bytesAvailForAdd(): 715 | self.processInput() 716 | 717 | # If there is a timeout 718 | if self._timeout: 719 | old_alarm = signal.signal(signal.SIGALRM, self.timeout_handler) 720 | signal.alarm(self._timeout) 721 | 722 | # Run Request. 723 | req.run() 724 | 725 | self._request = None 726 | 727 | # Restore old handler if timeout was given 728 | if self._timeout: 729 | signal.alarm(0) 730 | signal.signal(signal.SIGALRM, old_alarm) 731 | 732 | def _shutdown(self, pkt): 733 | """Not sure what to do with this yet.""" 734 | self.logger.info('Received shutdown request from server') 735 | 736 | def _ping(self, pkt): 737 | """I have no idea what this packet means.""" 738 | self.logger.debug('Received ping') 739 | 740 | def _cping(self, pkt): 741 | """Respond to a PING (CPING) packet.""" 742 | self.logger.debug('Received PING, sending PONG') 743 | pkt = Packet() 744 | pkt.data = PKTTYPE_CPONG 745 | self.writePacket(pkt) 746 | 747 | def _processBody(self, pkt): 748 | """ 749 | Handles a body chunk from the server by appending it to the 750 | InputStream. 751 | """ 752 | if pkt.length: 753 | length = struct.unpack('>H', pkt.data[:2])[0] 754 | self._request.input.addData(pkt.data[2:2+length]) 755 | else: 756 | # Shouldn't really ever get here. 757 | self._request.input.addData(b'') 758 | 759 | def writePacket(self, pkt): 760 | """Sends a Packet to the server.""" 761 | pkt.write(self._sock) 762 | 763 | class BaseAJPServer(object): 764 | # What Request class to use. 765 | requestClass = Request 766 | 767 | # Limits the size of the InputStream's string buffer to this size + 8k. 768 | # Since the InputStream is not seekable, we throw away already-read 769 | # data once this certain amount has been read. (The 8k is there because 770 | # it is the maximum size of new data added per chunk.) 771 | inputStreamShrinkThreshold = 102400 - 8192 772 | 773 | def __init__(self, application, scriptName='', environ=None, 774 | multithreaded=True, multiprocess=False, 775 | bindAddress=('localhost', 8009), allowedServers=NoDefault, 776 | loggingLevel=logging.INFO, debug=False): 777 | """ 778 | scriptName is the initial portion of the URL path that "belongs" 779 | to your application. It is used to determine PATH_INFO (which doesn't 780 | seem to be passed in). An empty scriptName means your application 781 | is mounted at the root of your virtual host. 782 | 783 | environ, which must be a dictionary, can contain any additional 784 | environment variables you want to pass to your application. 785 | 786 | Set multithreaded to False if your application is not thread-safe. 787 | 788 | Set multiprocess to True to explicitly set wsgi.multiprocess to 789 | True. (Only makes sense with threaded servers.) 790 | 791 | bindAddress is the address to bind to, which must be a tuple of 792 | length 2. The first element is a string, which is the host name 793 | or IPv4 address of a local interface. The 2nd element is the port 794 | number. 795 | 796 | allowedServers must be None or a list of strings representing the 797 | IPv4 addresses of servers allowed to connect. None means accept 798 | connections from anywhere. By default, it is a list containing 799 | the single item '127.0.0.1'. 800 | 801 | loggingLevel sets the logging level of the module-level logger. 802 | """ 803 | if environ is None: 804 | environ = {} 805 | 806 | self.application = application 807 | self.scriptName = scriptName 808 | self.environ = environ 809 | self.multithreaded = multithreaded 810 | self.multiprocess = multiprocess 811 | self.debug = debug 812 | self._bindAddress = bindAddress 813 | if allowedServers is NoDefault: 814 | allowedServers = ['127.0.0.1'] 815 | self._allowedServers = allowedServers 816 | 817 | # Used to force single-threadedness. 818 | self._appLock = _thread.allocate_lock() 819 | 820 | self.logger = logging.getLogger(LoggerName) 821 | self.logger.setLevel(loggingLevel) 822 | 823 | def _setupSocket(self): 824 | """Creates and binds the socket for communication with the server.""" 825 | sock = socket.socket() 826 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 827 | sock.bind(self._bindAddress) 828 | sock.listen(socket.SOMAXCONN) 829 | return sock 830 | 831 | def _cleanupSocket(self, sock): 832 | """Closes the main socket.""" 833 | sock.close() 834 | 835 | def _isClientAllowed(self, addr): 836 | ret = self._allowedServers is None or addr[0] in self._allowedServers 837 | if not ret: 838 | self.logger.warning('Server connection from %s disallowed', 839 | addr[0]) 840 | return ret 841 | 842 | def handler(self, request): 843 | """ 844 | WSGI handler. Sets up WSGI environment, calls the application, 845 | and sends the application's response. 846 | """ 847 | environ = request.environ 848 | environ.update(self.environ) 849 | 850 | environ['wsgi.version'] = (1,0) 851 | environ['wsgi.input'] = request.input 852 | environ['wsgi.errors'] = sys.stderr 853 | environ['wsgi.multithread'] = self.multithreaded 854 | environ['wsgi.multiprocess'] = self.multiprocess 855 | environ['wsgi.run_once'] = False 856 | 857 | if environ.get('HTTPS', 'off') in ('on', '1'): 858 | environ['wsgi.url_scheme'] = 'https' 859 | else: 860 | environ['wsgi.url_scheme'] = 'http' 861 | 862 | self._sanitizeEnv(environ) 863 | 864 | headers_set = [] 865 | headers_sent = [] 866 | result = None 867 | 868 | def write(data): 869 | if type(data) is str: 870 | data = data.encode('latin-1') 871 | 872 | assert type(data) is bytes, 'write() argument must be bytes' 873 | assert headers_set, 'write() before start_response()' 874 | 875 | if not headers_sent: 876 | status, responseHeaders = headers_sent[:] = headers_set 877 | statusCode = int(status[:3]) 878 | statusMsg = status[4:] 879 | found = False 880 | for header,value in responseHeaders: 881 | if header.lower() == b'content-length': 882 | found = True 883 | break 884 | if not found and result is not None: 885 | try: 886 | if len(result) == 1: 887 | responseHeaders.append((b'Content-Length', 888 | str(len(data)).encode('latin-1'))) 889 | except: 890 | pass 891 | request.startResponse(statusCode, statusMsg, responseHeaders) 892 | 893 | request.write(data) 894 | 895 | def start_response(status, response_headers, exc_info=None): 896 | if exc_info: 897 | try: 898 | if headers_sent: 899 | # Re-raise if too late 900 | raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) 901 | finally: 902 | exc_info = None # avoid dangling circular ref 903 | else: 904 | assert not headers_set, 'Headers already set!' 905 | 906 | if type(status) is str: 907 | status = status.encode('latin-1') 908 | 909 | assert type(status) is bytes, 'Status must be bytes' 910 | assert len(status) >= 4, 'Status must be at least 4 bytes' 911 | assert int(status[:3]), 'Status must begin with 3-digit code' 912 | assert status[3] == 0x20, 'Status must have a space after code' 913 | assert type(response_headers) is list, 'Headers must be a list' 914 | new_response_headers = [] 915 | for name,val in response_headers: 916 | if type(name) is str: 917 | name = name.encode('latin-1') 918 | if type(val) is str: 919 | val = val.encode('latin-1') 920 | 921 | assert type(name) is bytes, 'Header name "%s" must be bytes' % name 922 | assert type(val) is bytes, 'Value of header "%s" must be bytes' % name 923 | 924 | new_response_headers.append((name, val)) 925 | 926 | headers_set[:] = [status, new_response_headers] 927 | return write 928 | 929 | if not self.multithreaded: 930 | self._appLock.acquire() 931 | try: 932 | try: 933 | result = self.application(environ, start_response) 934 | try: 935 | for data in result: 936 | if data: 937 | write(data) 938 | if not headers_sent: 939 | write(b'') # in case body was empty 940 | finally: 941 | if hasattr(result, 'close'): 942 | result.close() 943 | except socket.error as e: 944 | if e.args[0] != errno.EPIPE: 945 | raise # Don't let EPIPE propagate beyond server 946 | finally: 947 | if not self.multithreaded: 948 | self._appLock.release() 949 | 950 | def _sanitizeEnv(self, environ): 951 | """Fill-in/deduce missing values in environ.""" 952 | # Namely SCRIPT_NAME/PATH_INFO 953 | value = environ['REQUEST_URI'] 954 | scriptName = environ.get('WSGI_SCRIPT_NAME', self.scriptName) 955 | if not value.startswith(scriptName): 956 | self.logger.warning('scriptName does not match request URI') 957 | 958 | environ['PATH_INFO'] = value[len(scriptName):] 959 | environ['SCRIPT_NAME'] = scriptName 960 | 961 | reqUri = None 962 | if 'REQUEST_URI' in environ: 963 | reqUri = environ['REQUEST_URI'].split('?', 1) 964 | 965 | if 'QUERY_STRING' not in environ or not environ['QUERY_STRING']: 966 | if reqUri is not None and len(reqUri) > 1: 967 | environ['QUERY_STRING'] = reqUri[1] 968 | else: 969 | environ['QUERY_STRING'] = '' 970 | 971 | def error(self, request): 972 | """ 973 | Override to provide custom error handling. Ideally, however, 974 | all errors should be caught at the application level. 975 | """ 976 | if self.debug: 977 | request.startResponse(500, b'Internal Server Error', [(b'Content-Type', b'text/html')]) 978 | import cgitb 979 | request.write(cgitb.html(sys.exc_info()).encode('latin-1')) 980 | else: 981 | errorpage = b""" 982 | 983 | Unhandled Exception 984 | 985 |

Unhandled Exception

986 |

An unhandled exception was thrown by the application.

987 | 988 | """ 989 | request.startResponse(200, b'Internal Server Error', [(b'Content-Type', b'text/html')]) 990 | request.write(errorpage) 991 | --------------------------------------------------------------------------------