Robert Brewer avatar Robert Brewer committed 09e041b

Merging [2007] to [2051] from trunk in prep for releasing 3.1.1.

Comments (0)

Files changed (32)

cherrypy/_cpconfig.py

     elif k == 'autoreload_match':
         engine.autoreload.match = v
     elif k == 'reload_files':
-        engine.autoreload.files = v
+        engine.autoreload.files = set(v)
     elif k == 'deadlock_poll_freq':
         engine.timeout_monitor.frequency = v
     elif k == 'SIGHUP':
         engine.listeners['SIGHUP'] = set([v])
     elif k == 'SIGTERM':
         engine.listeners['SIGTERM'] = set([v])
+    elif "." in k:
+        plugin, attrname = k.split(".", 1)
+        plugin = getattr(engine, plugin)
+        if attrname == 'on':
+            if v and callable(getattr(plugin, 'subscribe', None)):
+                plugin.subscribe()
+                return
+            elif (not v) and callable(getattr(plugin, 'unsubscribe', None)):
+                plugin.unsubscribe()
+                return
+        setattr(plugin, attrname, v)
+    else:
+        setattr(engine, k, v)
 Config.namespaces["engine"] = _engine_namespace_handler
 
 

cherrypy/_cpdispatch.py

         self.kwargs = kwargs
     
     def __call__(self):
-        return self.callable(*self.args, **self.kwargs)
+        try:
+            return self.callable(*self.args, **self.kwargs)
+        except TypeError, x:
+            test_callable_spec(self.callable, self.args, self.kwargs)
+            raise
+
+def test_callable_spec(callable, callable_args, callable_kwargs):
+    """
+    Inspect callable and test to see if the given args are suitable for it.
+
+    When an error occurs during the handler's invoking stage there are 2
+    erroneous cases:
+    1.  Too many parameters passed to a function which doesn't define
+        one of *args or **kwargs.
+    2.  Too little parameters are passed to the function.
+
+    There are 3 sources of parameters to a cherrypy handler.
+    1.  query string parameters are passed as keyword parameters to the handler.
+    2.  body parameters are also passed as keyword parameters.
+    3.  when partial matching occurs, the final path atoms are passed as
+        positional args.
+    Both the query string and path atoms are part of the URI.  If they are
+    incorrect, then a 404 Not Found should be raised. Conversely the body
+    parameters are part of the request; if they are invalid a 400 Bad Request.
+    """
+    (args, varargs, varkw, defaults) = inspect.getargspec(callable)
+
+    if args and args[0] == 'self':
+        args = args[1:]
+
+    arg_usage = dict([(arg, 0,) for arg in args])
+    vararg_usage = 0
+    varkw_usage = 0
+    extra_kwargs = set()
+
+    for i, value in enumerate(callable_args):
+        try:
+            arg_usage[args[i]] += 1
+        except IndexError:
+            vararg_usage += 1
+
+    for key in callable_kwargs.keys():
+        try:
+            arg_usage[key] += 1
+        except KeyError:
+            varkw_usage += 1
+            extra_kwargs.add(key)
+
+    for i, val in enumerate(defaults or []):
+        # Defaults take effect only when the arg hasn't been used yet.
+        if arg_usage[args[i]] == 0:
+            arg_usage[args[i]] += 1
+
+    missing_args = []
+    multiple_args = []
+    for key, usage in arg_usage.iteritems():
+        if usage == 0:
+            missing_args.append(key)
+        elif usage > 1:
+            multiple_args.append(key)
+
+    if missing_args:
+        # In the case where the method allows body arguments
+        # there are 3 potential errors:
+        # 1. not enough query string parameters -> 404
+        # 2. not enough body parameters -> 400
+        # 3. not enough path parts (partial matches) -> 404
+        #
+        # We can't actually tell which case it is, 
+        # so I'm raising a 404 because that covers 2/3 of the
+        # possibilities
+        # 
+        # In the case where the method does not allow body
+        # arguments it's definitely a 404.
+        raise cherrypy.HTTPError(404,
+                message="Missing parameters: %s" % ",".join(missing_args))
+
+    # the extra positional arguments come from the path - 404 Not Found
+    if not varargs and vararg_usage > 0:
+        raise cherrypy.HTTPError(404)
+
+    body_params = cherrypy.request.body_params or {}
+    body_params = set(body_params.keys())
+    qs_params = set(callable_kwargs.keys()) - body_params
+
+    if multiple_args:
+
+        if qs_params.intersection(set(multiple_args)):
+            # If any of the multiple parameters came from the query string then
+            # it's a 404 Not Found
+            error = 404
+        else:
+            # Otherwise it's a 400 Bad Request
+            error = 400
+
+        raise cherrypy.HTTPError(error,
+                message="Multiple values for parameters: "\
+                        "%s" % ",".join(multiple_args))
+
+    if not varkw and varkw_usage > 0:
+
+        # If there were extra query string parameters, it's a 404 Not Found
+        extra_qs_params = set(qs_params).intersection(extra_kwargs)
+        if extra_qs_params:
+            raise cherrypy.HTTPError(404,
+                message="Unexpected query string "\
+                        "parameters: %s" % ", ".join(extra_qs_params))
+
+        # If there were any extra body parameters, it's a 400 Not Found
+        extra_body_params = set(body_params).intersection(extra_kwargs)
+        if extra_body_params:
+            raise cherrypy.HTTPError(400,
+                message="Unexpected body parameters: "\
+                        "%s" % ", ".join(extra_body_params))
+
+
+try:
+    import inspect
+except ImportError:
+    test_callable_spec = lambda callable, args, kwargs: None
+
 
 
 class LateParamPageHandler(PageHandler):

cherrypy/_cplogging.py

                 if stream is None:
                     stream=sys.stderr
                 h = logging.StreamHandler(stream)
-                h.setLevel(logging.DEBUG)
                 h.setFormatter(logfmt)
                 h._cpbuiltin = "screen"
                 log.addHandler(h)
     
     def _add_builtin_file_handler(self, log, fname):
         h = logging.FileHandler(fname)
-        h.setLevel(logging.DEBUG)
         h.setFormatter(logfmt)
         h._cpbuiltin = "file"
         log.addHandler(h)
         if enable:
             if not h:
                 h = WSGIErrorHandler()
-                h.setLevel(logging.DEBUG)
                 h.setFormatter(logfmt)
                 h._cpbuiltin = "wsgi"
                 log.addHandler(h)

cherrypy/_cprequest.py

 import cherrypy
 from cherrypy import _cpcgifs, _cpconfig
 from cherrypy._cperror import format_exc, bare_error
-from cherrypy.lib import http
+from cherrypy.lib import http, file_generator
 
 
 class Hook(object):
             cherrypy.response.finalize()
 
 
-def file_generator(input, chunkSize=65536):
-    """Yield the given input (a file object) in chunks (default 64k). (Core)"""
-    chunk = input.read(chunkSize)
-    while chunk:
-        yield chunk
-        chunk = input.read(chunkSize)
-    input.close()
-
-
 class Body(object):
     """The body of the HTTP response (the response entity)."""
     

cherrypy/_cpserver.py

     protocol_version = 'HTTP/1.1'
     reverse_dns = False
     thread_pool = 10
+    thread_pool_max = -1
     max_request_header_size = 500 * 1024
     max_request_body_size = 100 * 1024 * 1024
     instance = None

cherrypy/_cptree.py

         
         root: an instance of a "controller class" (a collection of page
             handler methods) which represents the root of the application.
+            This may also be an Application instance, or None if using
+            a dispatcher other than the default.
         script_name: a string containing the "mount point" of the application.
             This should start with a slash, and be the path portion of the
             URL at which to mount the given root. For example, if root.index()
         
         if isinstance(root, Application):
             app = root
+            if script_name != "" and script_name != app.script_name:
+                raise ValueError, "Cannot specify a different script name and pass an Application instance to cherrypy.mount"
+            script_name = app.script_name
         else:
             app = Application(root, script_name)
             
             # If mounted at "", add favicon.ico
-            if script_name == "" and root and not hasattr(root, "favicon_ico"):
+            if (script_name == "" and root is not None
+                    and not hasattr(root, "favicon_ico")):
                 favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
                                        "favicon.ico")
                 root.favicon_ico = tools.staticfile.handler(favicon)

cherrypy/_cpwsgi_server.py

         s.__init__(self, bind_addr, cherrypy.tree,
                    server.thread_pool,
                    server.socket_host,
+                   max = server.thread_pool_max,
                    request_queue_size = server.socket_queue_size,
                    timeout = server.socket_timeout,
                    shutdown_timeout = server.shutdown_timeout,
 
 
 def start(configfiles=None, daemonize=False, environment=None,
-          fastcgi=False, pidfile=None, imports=None):
+          fastcgi=False, scgi=False, pidfile=None, imports=None):
     """Subscribe all engine plugins and start the engine."""
+    sys.path = [''] + sys.path
     for i in imports or []:
         exec "import %s" % i
     
     if hasattr(engine, "console_control_handler"):
         engine.console_control_handler.subscribe()
     
-    if fastcgi:
-        # turn off autoreload when using fastcgi
-        cherrypy.config.update({'autoreload.on': False})
-        
+    if fastcgi and scgi:
+        # fastcgi and scgi aren't allowed together.
+        cherrypy.log.error("fastcgi and scgi aren't allowed together.", 'ENGINE')
+        sys.exit(1)
+    elif fastcgi or scgi:
+        # Turn off autoreload when using fastcgi or scgi.
+        cherrypy.config.update({'engine.autoreload_on': False})
+        # Turn off the default HTTP server (which is subscribed by default).
         cherrypy.server.unsubscribe()
         
-        fastcgi_port = cherrypy.config.get('server.socket_port', 4000)
-        fastcgi_bindaddr = cherrypy.config.get('server.socket_host', '0.0.0.0')
-        bindAddress = (fastcgi_bindaddr, fastcgi_port)
-        f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=bindAddress)
+        sock_file = cherrypy.config.get('server.socket_file', None)
+        if sock_file:
+            bindAddress = sock_file
+        else:
+            flup_port = cherrypy.config.get('server.socket_port', 4000)
+            flup_bindaddr = cherrypy.config.get('server.socket_host', '0.0.0.0')
+            bindAddress = (flup_bindaddr, flup_port)
+        if fastcgi:
+            f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=bindAddress)
+        else:
+            f = servers.FlupSCGIServer(application=cherrypy.tree, bindAddress=bindAddress)
         s = servers.ServerAdapter(engine, httpserver=f, bind_addr=bindAddress)
         s.subscribe()
     
                  help="apply the given config environment")
     p.add_option('-f', action="store_true", dest='fastcgi',
                  help="start a fastcgi server instead of the default HTTP server")
+    p.add_option('-s', action="store_true", dest='scgi',
+                 help="start a scgi server instead of the default HTTP server")
     p.add_option('-i', '--import', action="append", dest='imports',
                  help="specify modules to import")
     p.add_option('-p', '--pidfile', dest='pidfile', default=None,
     options, args = p.parse_args()
     
     start(options.config, options.daemonize,
-          options.environment, options.fastcgi, options.pidfile,
+          options.environment, options.fastcgi, options.scgi, options.pidfile,
           options.imports)
 

cherrypy/lib/__init__.py

     
     return _Builder().build(obj)
 
+
+def file_generator(input, chunkSize=65536):
+    """Yield the given input (a file object) in chunks (default 64k). (Core)"""
+    chunk = input.read(chunkSize)
+    while chunk:
+        yield chunk
+        chunk = input.read(chunkSize)
+    input.close()
+
+
+def file_generator_limited(fileobj, count, chunk_size=65536):
+    """Yield the given file object in chunks, stopping after `count`
+    bytes has been emitted.  Default chunk size is 64kB. (Core)
+    """
+    remaining = count
+    while remaining > 0:
+        chunk = fileobj.read(min(chunk_size, remaining))
+        chunklen = len(chunk)
+        if chunklen == 0:
+            return
+        remaining -= chunklen
+        yield chunk
+

cherrypy/lib/cptools.py

     def on_check(self, username):
         pass
     
-    def login_screen(self, from_page='..', username='', error_msg=''):
+    def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
         return """<html><body>
 Message: %(error_msg)s
 <form method="post" action="do_login">
 </body></html>""" % {'from_page': from_page, 'username': username,
                      'error_msg': error_msg}
     
-    def do_login(self, username, password, from_page='..'):
+    def do_login(self, username, password, from_page='..', **kwargs):
         """Login. May raise redirect, or return True if request handled."""
         error_msg = self.check_username_and_password(username, password)
         if error_msg:
             self.on_login(username)
             raise cherrypy.HTTPRedirect(from_page or "/")
     
-    def do_logout(self, from_page='..'):
+    def do_logout(self, from_page='..', **kwargs):
         """Logout. May raise redirect, or return True if request handled."""
         sess = cherrypy.session
         username = sess.get(self.session_key)

cherrypy/lib/http.py

 image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
 
 def parse_query_string(query_string, keep_blank_values=True):
-    """Build a params dictionary from a query_string."""
+    """Build a params dictionary from a query_string.
+    
+    Duplicate key/value pairs in the provided query_string will be
+    returned as {'key': [val1, val2, ...]}. Single key/values will
+    be returned as strings: {'key': 'value'}.
+    """
     if image_map_pattern.match(query_string):
         # Server-side image map. Map the coords to 'x' and 'y'
         # (like CGI::Request does).

cherrypy/lib/httpauth.py

     else:
         H_A1 = H(_A1(params, password))
 
-    if qop == "auth" or aop == "auth-int":
+    if qop in ("auth", "auth-int"):
         # If the "qop" value is "auth" or "auth-int":
         # request-digest  = <"> < KD ( H(A1),     unq(nonce-value)
         #                              ":" nc-value
             params["qop"],
             H_A2,
         )
-
     elif qop is None:
         # If the "qop" directive is not present (this construction is
         # for compatibility with RFC 2069):

cherrypy/lib/profiler.py

 
 class make_app:
     def __init__(self, nextapp, path=None, aggregate=False):
-        """Make a WSGI middleware app which wraps 'nextapp' with profiling."""
+        """Make a WSGI middleware app which wraps 'nextapp' with profiling.
+        
+        nextapp: the WSGI application to wrap, usually an instance of
+            cherrypy.Application.
+        path: where to dump the profiling output.
+        aggregate: if True, profile data for all HTTP requests will go in
+            a single file. If False (the default), each HTTP request will
+            dump its profile data into a separate file.
+        """
         self.nextapp = nextapp
         self.aggregate = aggregate
         if aggregate:

cherrypy/lib/sessions.py

         # Stick the clean_thread in the class, not the instance.
         # The instances are created and destroyed per-request.
         cls = self.__class__
-        if not cls.clean_thread:
+        if self.clean_freq and not cls.clean_thread:
             # clean_up is in instancemethod and not a classmethod,
             # so that tool config can be accessed inside the method.
             t = cherrypy.process.plugins.Monitor(
     SESSION_PREFIX = 'session-'
     LOCK_SUFFIX = '.lock'
     
+    def __init__(self, id=None, **kwargs):
+        # The 'storage_path' arg is required for file-based sessions.
+        kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+        Session.__init__(self, id=id, **kwargs)
+    
     def setup(cls, **kwargs):
         """Set up the storage system for file-based sessions.
         
         This should only be called once per process; this will be done
         automatically when using sessions.init (as the built-in Tool does).
         """
-        if 'storage_path' in kwargs:
-            kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+        # The 'storage_path' arg is required for file-based sessions.
+        kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
         
         for k, v in kwargs.iteritems():
             setattr(cls, k, v)

cherrypy/lib/static.py

 import urllib
 
 import cherrypy
-from cherrypy.lib import cptools, http
+from cherrypy.lib import cptools, http, file_generator_limited
 
 
 def serve_file(path, content_type=None, disposition=None, name=None):
             if len(r) == 1:
                 # Return a single-part response.
                 start, stop = r[0]
+                if stop > c_len:
+                    stop = c_len
                 r_len = stop - start
                 response.status = "206 Partial Content"
                 response.headers['Content-Range'] = ("bytes %s-%s/%s" %
                                                        (start, stop - 1, c_len))
                 response.headers['Content-Length'] = r_len
                 bodyfile.seek(start)
-                response.body = bodyfile.read(r_len)
+                response.body = file_generator_limited(bodyfile, r_len)
             else:
                 # Return a multipart/byteranges response.
                 response.status = "206 Partial Content"
                         yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
                                % (start, stop - 1, c_len))
                         bodyfile.seek(start)
-                        yield bodyfile.read(stop - start)
+                        for chunk in file_generator_limited(bodyfile, stop-start):
+                            yield chunk
                         yield "\r\n"
                     # Final boundary
                     yield "--" + boundary + "--"

cherrypy/lib/xmlrpc.py

                                   encoding=encoding,
                                   allow_none=allow_none))
 
-def on_error():
+def on_error(*args, **kwargs):
     body = str(sys.exc_info()[1])
     import xmlrpclib
     _set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))

cherrypy/process/plugins.py

     def subscribe(self):
         """Register this object as a (multi-channel) listener on the bus."""
         for channel in self.bus.listeners:
+            # Subscribe self.start, self.exit, etc. if present.
             method = getattr(self, channel, None)
             if method is not None:
                 self.bus.subscribe(channel, method)
     def unsubscribe(self):
         """Unregister this object as a listener on the bus."""
         for channel in self.bus.listeners:
+            # Unsubscribe self.start, self.exit, etc. if present.
             method = getattr(self, channel, None)
             if method is not None:
                 self.bus.unsubscribe(channel, method)
             else:
                 self.bus.log('Started as uid: %r gid: %r' % current_ids())
                 if self.gid is not None:
-                    os.setgid(gid)
+                    os.setgid(self.gid)
                 if self.uid is not None:
-                    os.setuid(uid)
+                    os.setuid(self.uid)
                 self.bus.log('Running as uid: %r gid: %r' % current_ids())
         
         # umask
                              (old_umask, self.umask))
         
         self.finalized = True
-    start.priority = 75
+    # This is slightly higher than the priority for server.start
+    # in order to facilitate the most common use: starting on a low
+    # port (which requires root) and then dropping to another user.
+    start.priority = 77
 
 
 class Daemonizer(SimplePlugin):

cherrypy/process/servers.py

     """Adapter for a flup.server.fcgi.WSGIServer."""
     
     def __init__(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+        self.ready = False
+    
+    def start(self):
+        """Start the FCGI server."""
+        # We have to instantiate the server class here because its __init__
+        # starts a threadpool. If we do it too early, daemonize won't work.
         from flup.server.fcgi import WSGIServer
-        self.fcgiserver = WSGIServer(*args, **kwargs)
+        self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
         # TODO: report this bug upstream to flup.
         # If we don't set _oldSIGs on Windows, we get:
         #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
         #   line 156, in _restoreSignalHandlers
         #     for signum,handler in self._oldSIGs:
         #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
+        self.fcgiserver._installSignalHandlers = lambda: None
         self.fcgiserver._oldSIGs = []
-        self.ready = False
-    
-    def start(self):
-        """Start the FCGI server."""
         self.ready = True
         self.fcgiserver.run()
     
         self.fcgiserver._threadPool.maxSpare = 0
 
 
+class FlupSCGIServer(object):
+    """Adapter for a flup.server.scgi.WSGIServer."""
+    
+    def __init__(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+        self.ready = False
+    
+    def start(self):
+        """Start the SCGI server."""
+        # We have to instantiate the server class here because its __init__
+        # starts a threadpool. If we do it too early, daemonize won't work.
+        from flup.server.scgi import WSGIServer
+        self.scgiserver = WSGIServer(*self.args, **self.kwargs)
+        # TODO: report this bug upstream to flup.
+        # If we don't set _oldSIGs on Windows, we get:
+        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+        #   line 108, in run
+        #     self._restoreSignalHandlers()
+        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+        #   line 156, in _restoreSignalHandlers
+        #     for signum,handler in self._oldSIGs:
+        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
+        self.scgiserver._installSignalHandlers = lambda: None
+        self.scgiserver._oldSIGs = []
+        self.ready = True
+        self.scgiserver.run()
+    
+    def stop(self):
+        """Stop the HTTP server."""
+        self.ready = False
+        # Forcibly stop the scgi server main event loop.
+        self.scgiserver._keepGoing = False
+        # Force all worker threads to die off.
+        self.scgiserver._threadPool.maxSpare = 0
+
+
 def client_host(server_host):
     """Return the host on which a client can connect to the given listener."""
     if server_host == '0.0.0.0':

cherrypy/process/win32.py

 class Win32Bus(wspbus.Bus):
     """A Web Site Process Bus implementation for Win32.
     
-    Instead of using time.sleep for blocking, this bus uses native
-    win32event objects. It also responds to console events.
+    Instead of time.sleep, this bus blocks using native win32event objects.
     """
     
     def __init__(self):
     state = property(_get_state, _set_state)
     
     def wait(self, state, interval=0.1):
-        """Wait for the given state, KeyboardInterrupt or SystemExit.
+        """Wait for the given state(s), KeyboardInterrupt or SystemExit.
         
         Since this class uses native win32event objects, the interval
         argument is ignored.
         """
-        # Don't wait for an event that beat us to the punch ;)
-        if self.state != state:
-            event = self._get_state_event(state)
-            win32event.WaitForSingleObject(event, win32event.INFINITE)
+        if isinstance(state, (tuple, list)):
+            # Don't wait for an event that beat us to the punch ;)
+            if self.state not in state:
+                events = tuple([self._get_state_event(s) for s in state])
+                win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE)
+        else:
+            # Don't wait for an event that beat us to the punch ;)
+            if self.state != state:
+                event = self._get_state_event(state)
+                win32event.WaitForSingleObject(event, win32event.INFINITE)
 
 
 class _ControlCodes(dict):

cherrypy/process/wspbus.py

                     e.code = 1
                 raise
             except:
-                self.log("Error in %r listener %r" % (channel, listener),
-                         level=40, traceback=True)
                 exc = sys.exc_info()[1]
+                if channel == 'log':
+                    # Assume any further messages to 'log' will fail.
+                    pass
+                else:
+                    self.log("Error in %r listener %r" % (channel, listener),
+                             level=40, traceback=True)
         if exc:
             raise
         return output
             self._do_execv()
     
     def wait(self, state, interval=0.1):
-        """Wait for the given state."""
+        """Wait for the given state(s)."""
+        if isinstance(state, (tuple, list)):
+            states = state
+        else:
+            states = [state]
+        
         def _wait():
-            while self.state != state:
+            while self.state not in states:
                 time.sleep(interval)
         
         # From http://psyco.sourceforge.net/psycoguide/bugs.html:

cherrypy/test/fcgi.conf

+
+# Apache2 server conf file for testing CherryPy with mod_fcgid.
+
+DocumentRoot "C:\Python25\Lib\site-packages\cherrypy\test"
+Listen 8080
+LoadModule fastcgi_module modules/mod_fastcgi.dll
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+FastCgiExternalServer "C:\\Python25\\Lib\\site-packages\\cherrypy\\test\\fastcgi.pyc" -host 127.0.0.1:4000

cherrypy/test/helper.py

             warnings.warn("Error importing wsgiref. The validator will not run.")
         else:
             app = validate.validator(app)
-    cherrypy.server.httpserver.wsgi_app = app
+    
+    h = cherrypy.server.httpserver
+    if hasattr(h, 'wsgi_app'):
+        # CherryPy's wsgiserver
+        h.wsgi_app = app
+    elif hasattr(h, 'fcgiserver'):
+        # flup's WSGIServer
+        h.fcgiserver.application = app
+    elif hasattr(h, 'scgiserver'):
+        # flup's WSGIServer
+        h.scgiserver.application = app
 
 def _run_test_suite_thread(moduleNames, conf):
     try:

cherrypy/test/modfcgid.py

+"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing.
+
+To autostart fcgid, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl", "apache2ctl",
+or "httpd"--create a symlink to them if needed.
+
+You'll also need the WSGIServer from flup.servers.
+See http://projects.amor.org/misc/wiki/ModPythonGateway
+
+
+KNOWN BUGS
+==========
+
+1. Apache processes Range headers automatically; CherryPy's truncated
+    output is then truncated again by Apache. See test_core.testRanges.
+    This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+    See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+4. Apache replaces status "reason phrases" automatically. For example,
+    CherryPy may set "304 Not modified" but Apache will write out
+    "304 Not Modified" (capital "M").
+5. Apache does not allow custom error codes as per the spec.
+6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+    Request-URI too early.
+7. mod_python will not read request bodies which use the "chunked"
+    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+    mod_python's requestobject.c).
+8. Apache will output a "Content-Length: 0" response header even if there's
+    no response entity body. This isn't really a bug; it just differs from
+    the CherryPy default.
+"""
+
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import re
+import sys
+import time
+
+import cherrypy
+from cherrypy.process import plugins, servers
+from cherrypy.test import test
+
+
+def read_process(cmd, args=""):
+    pipein, pipeout = os.popen4("%s %s" % (cmd, args))
+    try:
+        firstline = pipeout.readline()
+        if (re.search(r"(not recognized|No such file|not found)", firstline,
+                      re.IGNORECASE)):
+            raise IOError('%s must be on your system path.' % cmd)
+        output = firstline + pipeout.read()
+    finally:
+        pipeout.close()
+    return output
+
+
+APACHE_PATH = "httpd"
+CONF_PATH = "fcgi.conf"
+
+conf_fcgid = """
+# Apache2 server conf file for testing CherryPy with mod_fcgid.
+
+DocumentRoot "%(root)s"
+Listen %(port)s
+LoadModule fastcgi_module modules/mod_fastcgi.dll
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
+"""
+
+def start_apache(host, port, conf_template):
+    fcgiconf = CONF_PATH
+    if not os.path.isabs(fcgiconf):
+        fcgiconf = os.path.join(curdir, fcgiconf)
+    
+    # Write the Apache conf file.
+    f = open(fcgiconf, 'wb')
+    try:
+        server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
+        output = conf_template % {'port': port, 'root': curdir,
+                                  'server': server}
+        output = output.replace('\r\n', '\n')
+        f.write(output)
+    finally:
+        f.close()
+    
+    result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf)
+    if result:
+        print result
+
+def stop():
+    """Gracefully shutdown a server that is serving forever."""
+    read_process(APACHE_PATH, "-k stop")
+
+
+class FCGITestHarness(test.TestHarness):
+    """TestHarness for fcgid and CherryPy."""
+    
+    def _run(self, conf):
+        cherrypy.server.httpserver = servers.FlupFCGIServer(
+            application=cherrypy.tree, bindAddress=('127.0.0.1', 4000))
+        cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
+        try:
+            start_apache(self.host, self.port, conf_fcgid)
+            return test.TestHarness._run(self, conf)
+        finally:
+            stop()
+

cherrypy/test/modwsgi.py

 """
 
 import os
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+curdir = os.path.abspath(os.path.dirname(__file__))
 import re
+import sys
 import time
 
-from cherrypy.test import test
+import cherrypy
+from cherrypy.test import test, webtest
 
 
 def read_process(cmd, args=""):
     return output
 
 
-APACHE_PATH = "apache"
+if sys.platform == 'win32':
+    APACHE_PATH = "httpd"
+else:
+    APACHE_PATH = "apache"
+
 CONF_PATH = "test_mw.conf"
 
-conf_modwsgi = """
+conf_modwsgi = r"""
 # Apache2 server conf file for testing CherryPy with modpython_gateway.
 
+ServerName 127.0.0.1
 DocumentRoot "/"
-Listen %%s
+Listen %(port)s
+
+AllowEncodedSlashes On
+LoadModule rewrite_module modules/mod_rewrite.so
+RewriteEngine on
+RewriteMap escaping int:escape
+
+LoadModule log_config_module modules/mod_log_config.so
+LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined
+CustomLog "%(curdir)s/apache.access.log" combined
+ErrorLog "%(curdir)s/apache.error.log"
+LogLevel debug
+
 LoadModule wsgi_module modules/mod_wsgi.so
 LoadModule env_module modules/mod_env.so
 
-WSGIScriptAlias / %s
-SetEnv testmod %%s
-""" % os.path.join(curdir, 'modwsgi.py')
+WSGIScriptAlias / "%(curdir)s/modwsgi.py"
+SetEnv testmod %(testmod)s
+"""
 
 
 def start(testmod, port, conf_template):
     
     f = open(mpconf, 'wb')
     try:
-        f.write(conf_template % (port, testmod))
+        output = (conf_template %
+                  {'port': port, 'testmod': testmod, 'curdir': curdir})
+        f.write(output)
     finally:
         f.close()
     
         for testmod in self.tests:
             try:
                 start(testmod, self.port, conf_template)
+                cherrypy._cpserver.wait_for_occupied_port("127.0.0.1", self.port)
                 suite = webtest.ReloadingTestLoader().loadTestsFromName(testmod)
+                # Make a request so mod_wsgi starts up our app.
+                # If we don't, concurrent initial requests will 404.
+                webtest.openURL('/ihopetheresnodefault', port=self.port)
+                time.sleep(1)
                 result = webtest.TerseTestRunner(verbosity=2).run(suite)
                 success &= result.wasSuccessful()
             finally:
         mod.setup_server()
         
         cherrypy.config.update({
-            "log.error_file": os.path.join(curdir, "test.log"),
+            "log.error_file": os.path.join(curdir, "test.error.log"),
+            "log.access_file": os.path.join(curdir, "test.access.log"),
             "environment": "test_suite",
             "engine.SIGHUP": None,
             "engine.SIGTERM": None,
             })
-        cherrypy.server.unsubscribe()
-        cherrypy.engine.start(blocking=False)
     return cherrypy.tree(environ, start_response)
 

cherrypy/test/test.py

                          'cpmodpy': "cpmodpy",
                          'modpygw': "modpygw",
                          'modwsgi': "modwsgi",
+                         'modfcgid': "modfcgid",
                          }
     default_server = "wsgi"
     scheme = "http"
                                            self.protocol, self.port,
                                            "http", self.interactive)
             h.use_wsgi = True
+        elif self.server == 'modfcgid':
+            from cherrypy.test import modfcgid
+            h = modfcgid.FCGITestHarness(self.tests, self.server,
+                                         self.protocol, self.port,
+                                         "http", self.interactive)
         else:
             h = TestHarness(self.tests, self.server, self.protocol,
                             self.port, self.scheme, self.interactive,

cherrypy/test/test_core.py

         def default(self, *args, **kwargs):
             return "args: %s kwargs: %s" % (args, kwargs)
 
+    class ParamErrors(Test):
+
+        def one_positional(self, param1):
+            return "data"
+        one_positional.exposed = True
+
+        def one_positional_args(self, param1, *args):
+            return "data"
+        one_positional_args.exposed = True
+
+        def one_positional_args_kwargs(self, param1, *args, **kwargs):
+            return "data"
+        one_positional_args_kwargs.exposed = True
+
+        def one_positional_kwargs(self, param1, **kwargs):
+            return "data"
+        one_positional_kwargs.exposed = True
+
+        def no_positional(self):
+            return "data"
+        no_positional.exposed = True
+
+        def no_positional_args(self, *args):
+            return "data"
+        no_positional_args.exposed = True
+
+        def no_positional_args_kwargs(self, *args, **kwargs):
+            return "data"
+        no_positional_args_kwargs.exposed = True
+
+        def no_positional_kwargs(self, **kwargs):
+            return "data"
+        no_positional_kwargs.exposed = True
+
 
     class Status(Test):
         
         
         self.getPage("/params/?thing=a&thing=b&thing=c")
         self.assertBody("['a', 'b', 'c']")
-        
+
         # Test friendly error message when given params are not accepted.
-        ignore = helper.webtest.ignored_exceptions
-        ignore.append(TypeError)
-        try:
-            self.getPage("/params/?notathing=meeting")
-            self.assertInBody("index() got an unexpected keyword argument 'notathing'")
-        finally:
-            ignore.pop()
+        self.getPage("/params/?notathing=meeting")
+        self.assertInBody("Missing parameters: thing")
+        self.getPage("/params/?thing=meeting&notathing=meeting")
+        self.assertInBody("Unexpected query string parameters: notathing")
         
         # Test "% HEX HEX"-encoded URL, param keys, and values
         self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville")
         # Test coordinates sent by <img ismap>
         self.getPage("/params/ismap?223,114")
         self.assertBody("Coordinates: 223, 114")
-    
+
+    def testParamErrors(self):
+
+        # test that all of the handlers work when given 
+        # the correct parameters in order to ensure that the
+        # errors below aren't coming from some other source.
+        for uri in (
+                '/paramerrors/one_positional?param1=foo',
+                '/paramerrors/one_positional_args?param1=foo',
+                '/paramerrors/one_positional_args/foo',
+                '/paramerrors/one_positional_args/foo/bar/baz',
+                '/paramerrors/one_positional_args_kwargs?param1=foo&param2=bar',
+                '/paramerrors/one_positional_args_kwargs/foo?param2=bar&param3=baz',
+                '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
+                '/paramerrors/one_positional_kwargs?param1=foo&param2=bar&param3=baz',
+                '/paramerrors/one_positional_kwargs/foo?param4=foo&param2=bar&param3=baz',
+                '/paramerrors/no_positional',
+                '/paramerrors/no_positional_args/foo',
+                '/paramerrors/no_positional_args/foo/bar/baz',
+                '/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
+                '/paramerrors/no_positional_args_kwargs/foo?param2=bar',
+                '/paramerrors/no_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
+                '/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
+            ):
+            self.getPage(uri)
+            self.assertStatus(200)
+
+        # query string parameters are part of the URI, so if they are wrong
+        # for a particular handler, the status MUST be a 404.
+        for uri in (
+                '/paramerrors/one_positional',
+                '/paramerrors/one_positional?foo=foo',
+                '/paramerrors/one_positional/foo/bar/baz',
+                '/paramerrors/one_positional/foo?param1=foo',
+                '/paramerrors/one_positional/foo?param1=foo&param2=foo',
+                '/paramerrors/one_positional_args/foo?param1=foo&param2=foo',
+                '/paramerrors/one_positional_args/foo/bar/baz?param2=foo',
+                '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param1=bar&param3=baz',
+                '/paramerrors/one_positional_kwargs/foo?param1=foo&param2=bar&param3=baz',
+                '/paramerrors/no_positional/boo',
+                '/paramerrors/no_positional?param1=foo',
+                '/paramerrors/no_positional_args/boo?param1=foo',
+                '/paramerrors/no_positional_kwargs/boo?param1=foo',
+            ):
+            self.getPage(uri)
+            self.assertStatus(404)
+
+        # if body parameters are wrong, a 400 must be returned.
+        for uri, body in (
+                ('/paramerrors/one_positional/foo', 'param1=foo',),
+                ('/paramerrors/one_positional/foo', 'param1=foo&param2=foo',),
+                ('/paramerrors/one_positional_args/foo', 'param1=foo&param2=foo',),
+                ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo',),
+                ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar&param3=baz',),
+                ('/paramerrors/one_positional_kwargs/foo', 'param1=foo&param2=bar&param3=baz',),
+                ('/paramerrors/no_positional', 'param1=foo',),
+                ('/paramerrors/no_positional_args/boo', 'param1=foo',),
+            ):
+            self.getPage(uri, method='POST', body=body)
+            self.assertStatus(400)
+
+
+        # even if body parameters are wrong, if we get the uri wrong, then 
+        # it's a 404
+        for uri, body in (
+                ('/paramerrors/one_positional?param2=foo', 'param1=foo',),
+                ('/paramerrors/one_positional/foo/bar', 'param2=foo',),
+                ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo',),
+                ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar&param3=baz',),
+                ('/paramerrors/no_positional?param1=foo', 'param2=foo',),
+                ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo',),
+            ):
+            self.getPage(uri, method='POST', body=body)
+            self.assertStatus(404)
+
+
     def testStatus(self):
         self.getPage("/status/")
         self.assertBody('normal')
         self.assertBody('page1')
         self.getPage('/url/leaf?path_info=/page1&relative=True')
         self.assertBody('../page1')
+        self.getPage('/url/leaf?path_info=page1&relative=True')
+        self.assertBody('page1')
+        self.getPage('/url/leaf?path_info=leaf/page1&relative=True')
+        self.assertBody('leaf/page1')
         self.getPage('/url/leaf?path_info=../page1&relative=True')
         self.assertBody('../page1')
         self.getPage('/url/?path_info=other/../page1&relative=True')

cherrypy/test/test_http.py

         self.assertEquals(", ".join(["%s * 65536" % c for c in alphabet]),
                           response_body)
 
+    def test_malformed_request_line(self):
+        # Test missing version in Request-Line
+        if self.scheme == 'https':
+            c = httplib.HTTPSConnection('127.0.0.1:%s' % self.PORT)
+        else:
+            c = httplib.HTTPConnection('127.0.0.1:%s' % self.PORT)
+        c._output('GET /')
+        c._send_output()
+        response = c.response_class(c.sock, strict=c.strict, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 400)
+        self.assertEqual(response.fp.read(), "Malformed Request-Line")
+        c.close()
+
 
 if __name__ == '__main__':
     setup_server()

cherrypy/test/test_objectmapping.py

 from cherrypy.test import test
+from cherrypy._cptree import Application
 test.prefer_parent_path()
 
 import cherrypy
         self.getPage("/dir1/dir2/5/3/sir")
         self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
         
-        # test that extra positional args raises an error.
-        # 500 for now, maybe 404 in the future.
+        # test that extra positional args raises an 404 Not Found
         # See http://www.cherrypy.org/ticket/733.
         self.getPage("/dir1/dir2/script_name/extra/stuff")
-        self.assertStatus(500)
+        self.assertStatus(404)
     
     def testExpose(self):
         # Test the cherrypy.expose function/decorator
         self.getPage("/app")
         self.assertBody("milk")
 
+    def testTreeMounting(self):
+
+        # When mounting an application instance, 
+        # we can't specify a script name in the call to mount.
+        a = Application(object(), '/somewhere')
+        self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse')
+
+        # When mounting an application instance, 
+        # we can't specify a script name in the call to mount.
+        a = Application(object(), '/somewhere')
+        try:
+            cherrypy.tree.mount(a, '/somewhere')
+        except ValueError:
+            self.fail("tree.mount must allow script_names which are the same")
+
+        try:
+            cherrypy.tree.mount(a)
+        except ValueError:
+            self.fail("cherrypy.tree.mount incorrectly raised a ValueError")
+
+
+
+
 
 if __name__ == "__main__":
     setup_server()

cherrypy/test/test_sessionauthenticate.py

         if username != 'test' or password != 'password':
             return u'Wrong login/password'
     
+    def augment_params():
+        # A simple tool to add some things to request.params
+        # This is to check to make sure that session_auth can handle request
+        # params (ticket #780)
+        cherrypy.request.params["test"] = "test"
+
+    cherrypy.tools.augment_params = cherrypy.Tool('before_handler',
+             augment_params, None, priority=30)
+
     class Test:
         
         _cp_config = {'tools.sessions.on': True,
                       'tools.session_auth.on': True,
                       'tools.session_auth.check_username_and_password': check,
+                      'tools.augment_params.on': True,
                       }
         
-        def index(self):
+        def index(self, **kwargs):
             return "Hi %s, you are logged in" % cherrypy.request.login
         index.exposed = True
     

cherrypy/test/test_xmlrpc.py

     root.xmlrpc = XmlRpc()
     cherrypy.tree.mount(root, config={'/': {
         'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(),
+        'tools.xmlrpc.allow_none': 0,
         }})
     cherrypy.config.update({'environment': 'test_suite'})
 

cherrypy/wsgiserver/__init__.py

         
         environ = self.environ
         
-        method, path, req_protocol = request_line.strip().split(" ", 2)
+        try:
+            method, path, req_protocol = request_line.strip().split(" ", 2)
+        except ValueError:
+            self.simple_response(400, "Malformed Request-Line")
+            return
+        
         environ["REQUEST_METHOD"] = method
         
         # path may be an abs_path (including "http://host.domain.tld");
             self.simple_response("413 Request Entity Too Large")
             return
         
-        # Set AUTH_TYPE, REMOTE_USER
-        creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1)
-        environ["AUTH_TYPE"] = creds[0]
-        if creds[0].lower() == 'basic':
-            user, pw = base64.decodestring(creds[1]).split(":", 1)
-            environ["REMOTE_USER"] = user
-        
         # Persistent connection support
         if self.response_protocol == "HTTP/1.1":
             # Both server and client are HTTP/1.1
         buf.append("\r\n")
         if msg:
             buf.append(msg)
-        self.wfile.sendall("".join(buf))
+        
+        try:
+            self.wfile.sendall("".join(buf))
+        except socket.error, x:
+            if x.args[0] not in socket_errors_to_ignore:
+                raise
     
     def start_response(self, status, headers, exc_info = None):
         """WSGI callable to begin the HTTP response."""
 """ % (f, f)
 
 
+try:
+    import fcntl
+except ImportError:
+    try:
+        from ctypes import windll, WinError
+    except ImportError:
+        def prevent_socket_inheritance(sock):
+            """Dummy function, since neither fcntl nor ctypes are available."""
+            pass
+    else:
+        def prevent_socket_inheritance(sock):
+            """Mark the given socket fd as non-inheritable (Windows)."""
+            if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0):
+                raise WinError()
+else:
+    def prevent_socket_inheritance(sock):
+        """Mark the given socket fd as non-inheritable (POSIX)."""
+        fd = sock.fileno()
+        old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+        fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
+
 
 class CherryPyWSGIServer(object):
     """An HTTP server for WSGI.
     def bind(self, family, type, proto=0):
         """Create (or recreate) the actual socket object."""
         self.socket = socket.socket(family, type, proto)
+        prevent_socket_inheritance(self.socket)
         self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         if self.nodelay:
             self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
         """Accept a new connection and put it on the Queue."""
         try:
             s, addr = self.socket.accept()
+            prevent_socket_inheritance(s)
             if not self.ready:
                 return
             if hasattr(s, 'settimeout'):
             environ = self.environ.copy()
             # SERVER_SOFTWARE is common for IIS. It's also helpful for
             # us to pass a default value for the "Server" response header.
-            environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version
+            if environ.get("SERVER_SOFTWARE") is None:
+                environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version
             # set a non-standard environ entry so the WSGI app can know what
             # the *real* server protocol is (and what features to support).
             # See http://www.faqs.org/rfcs/rfc2145.html.
 
 import sys, os
 
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
 def use_setuptools(
     version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir
 ):
 
     return os.path.realpath(saveto)
 
-
-
-
-
-
-
-
-
-
-
 def main(argv, version=DEFAULT_VERSION):
     """Install or upgrade setuptools and EasyInstall"""
 
             print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
 if __name__=='__main__':
     main(sys.argv[1:])
-
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.