diff options
| -rw-r--r-- | docs/release-notes/index.rst | 2 | ||||
| -rw-r--r-- | docs/release-notes/version-4.3.0.rst | 220 | ||||
| -rw-r--r-- | setup.py | 33 | ||||
| -rw-r--r-- | src/server/__init__.py | 538 | ||||
| -rw-r--r-- | src/server/management/commands/runmodwsgi.py | 11 | ||||
| -rw-r--r-- | src/server/mod_wsgi.c | 243 | ||||
| -rw-r--r-- | src/server/wsgi_interp.c | 21 | ||||
| -rw-r--r-- | src/server/wsgi_server.h | 1 | ||||
| -rw-r--r-- | src/server/wsgi_version.h | 6 | ||||
| -rw-r--r-- | tests/auth.wsgi | 29 | ||||
| -rw-r--r-- | tests/environ.wsgi | 3 |
11 files changed, 1032 insertions, 75 deletions
diff --git a/docs/release-notes/index.rst b/docs/release-notes/index.rst index 433b8f7..ef4d671 100644 --- a/docs/release-notes/index.rst +++ b/docs/release-notes/index.rst @@ -5,6 +5,8 @@ Release Notes .. toctree:: :maxdepth: 2 + version-4.3.0.rst + version-4.2.8.rst version-4.2.7.rst version-4.2.6.rst diff --git a/docs/release-notes/version-4.3.0.rst b/docs/release-notes/version-4.3.0.rst new file mode 100644 index 0000000..8f62be3 --- /dev/null +++ b/docs/release-notes/version-4.3.0.rst @@ -0,0 +1,220 @@ +============= +Version 4.3.0 +============= + +Version 4.3.0 of mod_wsgi can be obtained from: + + https://github.com/GrahamDumpleton/mod_wsgi/archive/4.3.0.tar.gz + +Known Issues +------------ + +1. The makefiles for building mod_wsgi on Windows are currently broken and +need updating. As most new changes relate to mod_wsgi daemon mode, which is +not supported under Windows, you should keep using the last available +binary for version 3.X on Windows instead. + +Bugs Fixed +---------- + +1. Performing authorization using the ``WSGIAuthGroupScript`` was not +working correctly on Apache 2.4 due to changes in how auth providers +and authentication/authorization works. The result could be that a user +could gain access to a resource even though they were not in the +required group. + +2. Under Apache 2.4, when creating the ``environ`` dictionary for +passing into access/authentication/authorisation handlers, the behvaiour +of Apache 2.4 as it pertained to the WSGI application, whereby it +blocked the passing of any HTTP headers with a name which did not contain +just alphanumerics or '-', was not being mirrored. This created the +possibility of HTTP header spoofing in certain circumstances. Such headers +are now being ignored. + +3. When ``home`` option was used with ``WSGIDaemonProcess`` directive an +empty string was added to ``sys.path``. This meant current working directory +would be searched. This was fine so long as the current working directory +wasn't changed, but if it was, it would no longer look in the home +directory. Need to use the actual home directory instead. + +4. Fixed Django management command integration so would work for versions +of Django prior to 1.6 where ``BASE_DIR`` didn't exist in Django settings +module. + +Features Changed +---------------- + +1. In Apache 2.4, any headers with a name which does not include only +alphanumerics or '-' are blocked from being passed into a WSGI application +when the CGI like WSGI ``environ`` dictionary is created. This is a +mechanism to prevent header spoofing when there are multiple headers where +the only difference is the use of non alphanumerics in a specific character +position. + +This protection mechanism from Apache 2.4 is now being restrospectively +applied even when Apache 2.2 is being used and even though Apache itself +doesn't do it. This may technically result in headers that were previously +being passed, no longer being passed. The change is also technically +against what the HTTP RFC says is allowed for HTTP header names, but such +blocking would occur in Apache 2.4 anyway due to changes in Apache. It is +also understood that other web servers such as nginx also perform the same +type of blocking. Reliance on HTTP headers which use characters other +than alphanumerics and '-' is therefore dubious as many servers will now +discard them when needing to be passed into a system which requires the +headers to be passed as CGI like variables such as is the case for WSGI. + +2. In Apache 2.4, only ``wsgi-group`` is allowed when using the ``Require`` +directive for group authorisation. In prior Apache versions ``group`` would +also be accepted and matched by the ``wsgi`` auth provider. The inability +to use ``group`` is due to a change in Apache itself and not mod_wsgi. To +avoid any issues going forward though, the mod_wsgi code will now no longer +check for ``group`` even if for some reason Apache still decides to pass +the authorisation check off to mod_wsgi even when it shouldn't. + +New Features +------------ + +1. The value of the ``REMOTE_USER`` variable for an authenticated user +when user ``Basic`` authentication can now be overridden from an +authentication handler specified using the ``WSGIAuthUserScript``. To +override the name used to identify the user, instead of returning ``True`` +when indicating that the user is allowed, return the name to be used for +that user as a string. That value will then be passed through in +``REMOTE_USER`` in place of any original value:: + + def check_password(environ, user, password): + if user == 'spy': + if password == 'secret': + return 'grumpy' + return False + return None + +2. Added the ``--debug-mode`` option to ``mod_wsgi-express`` which results +in Apache and the WSGI application being run in a single process which is +left attached to stdin/stdout of the shell where the script was run. Only a +single thread will be used to handle any requests. + +This feature enables the ability to interactively debug a Python WSGI +application using the Python debugger (``pdb``). The simplest way to +break into the Python debugger is by adding to your WSGI application code:: + + import pdb; pdb.set_trace() + +3. Added the ``--application-type`` option to ``mod_wsgi-express``. This +defaults to ``script`` indicating that the target WSGI application provided +to ``mod_wsgi-express`` is a WSGI script file defined by a relative or +absolute file system path. + +In addition to ``script``, it is also possible to supply for the application +type ``module`` and ``paste``. + +For the case of ``module``, the target WSGI application will be taken to +reside in a Python module with the specified name. This module will be +loaded using the standard Python module import system and so must reside +on the Python module search path. + +For the case of ``paste``, the target WSGI application will be taken to be +a Paste deployment configuration file. In loading the Paste deployment +configuration file, any WSGI application pipeline specified by the +configuration will be constructed and the resulting top level WSGI +application entry point returned used as the WSGI application. + +Note that the code file for the WSGI script file, Python module, or Paste +deployment configuration file, if modified, will all result in the WSGI +application being automatically reloaded on the next web request. + +4. Added the ``--auth-user-script`` and ``--auth-type`` options to +``mod_wsgi-express`` to enable the hosted site to implement user +authentication using either HTTP ``Basic`` or ``Digest`` authentication +mechanisms. The ``check_password()`` or ``get_realm_hash()`` functions +should follow the same form as if using the ``WSGIAuthUserScript`` direct +with mod_wsgi when using manual configuration. + +5. Added the ``--auth-group-script`` and ``--auth-group`` options to +``mod_wsgi-express`` to enable group authorization to be performed using a +group authorization script, in conjunction with a user authentication +script. The ``groups_for_user()`` function should follow the same form as +if using the ``WSGIAuthGroupScript`` direct with mod_wsgi when using manual +configuration. + +By default any users must be a member of the ``wsgi`` group. The name of +this group though can be overridden using the ``--auth-group`` option. +It is recommended that this be overridden rather than changing your own +application to use the ``wsgi`` group. + +6. Added the ``--directory-index`` option to ``mod_wsgi-express`` to enable +a index resource to be added to the document root directory which would +take precedence over the WSGI application for the root page for the site. + +7. Added the ``--with-php5`` option to ``mod_wsgi-express`` to enable the +concurrent hosting of a PHP web application in conjunction with the WSGI +application. Due to the limitations of PHP, this is currently only +supported if using prefork MPM. + +8. Added the ``--server-name`` option to ``mod_wsgi-express``. When this is +used and set to the host name for the web site, a virtual host will be +created to ensure that the server only accepts web requests for that host +name. + +If the host name starts with ``www.`` then web requests will also be +accepted against the parent domain, that is the host name without the +``www.``, but those requests will be automatically redirected to the +specified host name on the same port as that used for the original request. + +When the ``--server-name`` option is being used, the ``--server-alias`` +option can also be specified, multiple times if need be, to setup alternate +names for the web site on which web requests should also be accepted. +Wildcard aliases may be used in the name if wishing to match multiple +sub domains in one go. + +If for some reason you do still need to be able to access the server via +``localhost`` when a virtual host for a set server name is being used, you +can supply the ``--allow-localhost`` option. + +9. Added the ``--rotate-logs`` option to ``mod_wsgi-express`` to enable log +file rotation. By default the error log and access log, if enabled, will be +rotated when they reach 5MB in size. To change the size at which the log +files will be rotated, use the ``--max-log-size`` option. If the +``rotatelogs`` command is not being found properly, its location can be +specified using the ``--rotatelogs-executable`` option. + +10. Added the ``--ssl-port`` and ``--ssl-certificate`` options to +``mod_wsgi-express``. When both are set, with the latter being the stub +path for the SSL certificate ``.crt`` and ``.key`` file, then HTTPS +requests will be handled over the designated SSL port. + +When ``--https-only`` is supplied, any requests made over HTTP to the non +SSL port will be automatically redirected so as to use a HTTPS connection +over the SSL connection. + +Note that if using the ``--allow-localhost`` option, redirection from a +HTTP to HTTPS connection will not occur when access via ``localhost``. + +11. Added the ``--setenv`` option to ``mod_wsgi-express`` to enable request +specific name/value pairs to be added to the WSGI environ dictionary. The +values are restricted to string values. + +Also added a companion ``--passenv`` option to ``mod_wsgi-express`` to +indicate the names of normal process environment variables which should +be added to the per request WSGI environ dictionary. + +12. Added the ``WSGIMapHEADToGET`` directive for overriding the previous +behaviour of automatically mapping any ``HEAD`` request to a ``GET`` request +when an Apache output filter was registered that may want to see the complete +response in order to generate correct response headers. + +The directive can be set to be either ``Auto`` (the default), ``On`` which +will always map a ``HEAD`` to ``GET`` even if no output filters detected and +``Off`` to always preserve the original request method type. + +The original behaviour was to avoid problems with users trying to optimise +for ``HEAD`` requests and then breaking caching mechanisms because the +response headers for a ``HEAD`` request for a resource didn't match a ``GET`` +request against the same resource as required by HTTP. + +If using mod_wsgi-express, the ``--map-head-to-get`` option can be used with +the same values. + +12. Added the ``--compress-responses`` option to ``mod_wsgi-express`` to +enable compression of common text based responses such as plain text, HTML, +XML, CSS and Javascript. @@ -75,9 +75,17 @@ elif os.path.exists(os.path.join(BINDIR, PROGNAME)): else: HTTPD = PROGNAME +if os.path.exists(os.path.join(SBINDIR, 'rotatelogs')): + ROTATELOGS = os.path.join(SBINDIR, 'rotatelogs') +elif os.path.exists(os.path.join(BINDIR, 'rotatelogs')): + ROTATELOGS = os.path.join(BINDIR, 'rotatelogs') +else: + ROTATELOGS = 'rotatelogs' + with open(os.path.join(os.path.dirname(__file__), 'src/server/apxs_config.py'), 'w') as fp: print('HTTPD = "%s"' % HTTPD, file=fp) + print('ROTATELOGS = "%s"' % ROTATELOGS, file=fp) print('BINDIR = "%s"' % BINDIR, file=fp) print('SBINDIR = "%s"' % SBINDIR, file=fp) print('PROGNAME = "%s"' % PROGNAME, file=fp) @@ -122,13 +130,24 @@ LD_RUN_PATH = LD_RUN_PATH.lstrip(':') os.environ['LD_RUN_PATH'] = LD_RUN_PATH -# If using Python 3.4, then minimum MacOS X version you can use is 10.8. -# We have to force this with the compiler otherwise Python 3.4 sets it -# to 10.6 which screws up Apache APR % formats for apr_time_t, which -# breaks daemon mode queue time. - -if sys.version_info >= (3, 4): - os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.8' +# On MacOS X, recent versions of Apple's Apache do not support compiling +# Apache modules with a target older than 10.8. This is because it +# screws up Apache APR % formats for apr_time_t, which breaks daemon +# mode queue time. For the target to be 10.8 or newer for now if Python +# installation supports older versions. This means that things will not +# build for older MacOS X versions. Deal with these when they occur. + +if sys.platform == 'darwin': + target = os.environ.get('MACOSX_DEPLOYMENT_TARGET') + if target is None: + target = get_python_config('MACOSX_DEPLOYMENT_TARGET') + + if target: + target_version = tuple(map(int, target.split('.'))) + #assert target_version >= (10, 8), \ + # 'Minimum of 10.8 for MACOSX_DEPLOYMENT_TARGET' + if target_version < (10, 8): + os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.8' # Now add the definitions to build everything. diff --git a/src/server/__init__.py b/src/server/__init__.py index 7f5e46c..53f7391 100644 --- a/src/server/__init__.py +++ b/src/server/__init__.py @@ -97,16 +97,30 @@ LockFile '%(server_root)s/accept.lock' </IfVersion> <IfVersion >= 2.4> +<IfDefine WSGI_WITH_PHP5> <IfModule !mpm_event_module> <IfModule !mpm_worker_module> <IfModule !mpm_prefork_module> -<IfDefine WSGI_MPM_EVENT_MODULE> +<IfDefine WSGI_MPM_EXISTS_PREFORK_MODULE> +LoadModule mpm_prefork_module '%(modules_directory)s/mod_mpm_prefork.so' +</IfDefine> +</IfModule> +</IfModule> +</IfModule> +</IfDefine> +</IfVersion> + +<IfVersion >= 2.4> +<IfModule !mpm_event_module> +<IfModule !mpm_worker_module> +<IfModule !mpm_prefork_module> +<IfDefine WSGI_MPM_ENABLE_EVENT_MODULE> LoadModule mpm_event_module '%(modules_directory)s/mod_mpm_event.so' </IfDefine> -<IfDefine WSGI_MPM_WORKER_MODULE> +<IfDefine WSGI_MPM_ENABLE_WORKER_MODULE> LoadModule mpm_worker_module '%(modules_directory)s/mod_mpm_worker.so' </IfDefine> -<IfDefine WSGI_MPM_PREFORK_MODULE> +<IfDefine WSGI_MPM_ENABLE_PREFORK_MODULE> LoadModule mpm_prefork_module '%(modules_directory)s/mod_mpm_prefork.so' </IfDefine> </IfModule> @@ -144,6 +158,36 @@ LoadModule alias_module '%(modules_directory)s/mod_alias.so' <IfModule !dir_module> LoadModule dir_module '%(modules_directory)s/mod_dir.so' </IfModule> +<IfModule !env_module> +LoadModule env_module '%(modules_directory)s/mod_env.so' +</IfModule> + +<IfDefine WSGI_COMPRESS_RESPONSES> +<IfModule !deflate_module> +LoadModule deflate_module '%(modules_directory)s/mod_deflate.so' +</IfModule> +</IfDefine> + +<IfDefine WSGI_AUTH_USER> +<IfModule !auth_basic_module> +LoadModule auth_basic_module '%(modules_directory)s/mod_auth_basic.so' +</IfModule> +<IfModule !auth_digest_module> +LoadModule auth_digest_module '%(modules_directory)s/mod_auth_digest.so' +</IfModule> +<IfModule !authz_user_module> +LoadModule authz_user_module '%(modules_directory)s/mod_authz_user.so' +</IfModule> +</IfDefine> + +<IfModule mpm_prefork_module> +<IfDefine WSGI_WITH_PHP5> +<IfModule !php5_module> +Loadmodule php5_module '%(modules_directory)s/libphp5.so' +</IfModule> +AddHandler application/x-httpd-php .php +</IfDefine> +</IfModule> LoadModule wsgi_module '%(mod_wsgi_so)s' @@ -173,6 +217,8 @@ LimitRequestBody %(limit_request_body)s </Directory> WSGIPythonHome '%(python_home)s' + +<IfDefine !ONE_PROCESS> WSGIRestrictEmbedded On WSGISocketPrefix %(server_root)s/wsgi <IfDefine WSGI_MULTIPROCESS> @@ -222,8 +268,15 @@ WSGIDaemonProcess %(host)s:%(port)s \\ header-buffer-size=%(header_buffer_size)s \\ server-metrics=%(daemon_server_metrics_flag)s </IfDefine> +</IfDefine> + WSGICallableObject '%(callable_object)s' WSGIPassAuthorization On +WSGIMapHEADToGET %(map_head_to_get)s + +<IfDefine ONE_PROCESS> +WSGIRestrictStdin Off +</IfDefine> <IfDefine WSGI_SERVER_METRICS> ExtendedStatus On @@ -246,7 +299,23 @@ KeepAliveTimeout %(keep_alive_timeout)s KeepAlive Off </IfDefine> +<IfDefine WSGI_COMPRESS_RESPONSES> +AddOutputFilterByType DEFLATE text/plain +AddOutputFilterByType DEFLATE text/html +AddOutputFilterByType DEFLATE text/xml +AddOutputFilterByType DEFLATE text/css +AddOutputFilterByType DEFLATE text/javascript +AddOutputFilterByType DEFLATE application/xhtml+xml +AddOutputFilterByType DEFLATE application/javascript +</IfDefine> + +<IfDefine WSGI_ROTATE_LOGS> +ErrorLog "|%(rotatelogs_executable)s \\ + %(error_log)s.%%Y-%%m-%%d-%%H_%%M_%%S %(max_log_size)sM" +</IfDefine> +<IfDefine !WSGI_ROTATE_LOGS> ErrorLog '%(error_log)s' +</IfDefine> LogLevel %(log_level)s <IfDefine WSGI_ACCESS_LOG> @@ -254,19 +323,41 @@ LogLevel %(log_level)s LoadModule log_config_module %(modules_directory)s/mod_log_config.so </IfModule> LogFormat "%%h %%l %%u %%t \\"%%r\\" %%>s %%b" common +<IfDefine WSGI_ROTATE_LOGS> +CustomLog "|%(rotatelogs_executable)s \\ + %(log_directory)s/access_log.%%Y-%%m-%%d-%%H_%%M_%%S %(max_log_size)sM" common +</IfDefine> +<IfDefine !WSGI_ROTATE_LOGS> CustomLog "%(log_directory)s/access_log" common </IfDefine> +</IfDefine> + +<IfDefine WSGI_WITH_SSL> +<IfModule !ssl_module> +LoadModule ssl_module %(modules_directory)s/mod_ssl.so +</IfModule> +</IfDefine> <IfModule mpm_prefork_module> +<IfDefine !ONE_PROCESS> ServerLimit %(prefork_server_limit)s StartServers %(prefork_start_servers)s MaxClients %(prefork_max_clients)s MinSpareServers %(prefork_min_spare_servers)s MaxSpareServers %(prefork_max_spare_servers)s +</IfDefine> +<IfDefine ONE_PROCESS> +ServerLimit 1 +StartServers 1 +MaxClients 1 +MinSpareServers 1 +MaxSpareServers 1 +</IfDefine> MaxRequestsPerChild 0 </IfModule> <IfModule mpm_worker_module> +<IfDefine !ONE_PROCESS> ServerLimit %(worker_server_limit)s ThreadLimit %(worker_thread_limit)s StartServers %(worker_start_servers)s @@ -274,11 +365,22 @@ MaxClients %(worker_max_clients)s MinSpareThreads %(worker_min_spare_threads)s MaxSpareThreads %(worker_max_spare_threads)s ThreadsPerChild %(worker_threads_per_child)s +</IfDefine> +<IfDefine ONE_PROCESS> +ServerLimit 1 +ThreadLimit 1 +StartServers 1 +MaxClients 1 +MinSpareThreads 1 +MaxSpareThreads 1 +ThreadsPerChild 1 +</IfDefine> MaxRequestsPerChild 0 ThreadStackSize 262144 </IfModule> <IfModule mpm_event_module> +<IfDefine !ONE_PROCESS> ServerLimit %(worker_server_limit)s ThreadLimit %(worker_thread_limit)s StartServers %(worker_start_servers)s @@ -286,10 +388,117 @@ MaxClients %(worker_max_clients)s MinSpareThreads %(worker_min_spare_threads)s MaxSpareThreads %(worker_max_spare_threads)s ThreadsPerChild %(worker_threads_per_child)s +</IfDefine> +<IfDefine ONE_PROCESS> +ServerLimit 1 +ThreadLimit 1 +StartServers 1 +MaxClients 1 +MinSpareThreads 1 +MaxSpareThreads 1 +ThreadsPerChild 1 +</IfDefine> MaxRequestsPerChild 0 ThreadStackSize 262144 </IfModule> +<IfDefine WSGI_VIRTUAL_HOST> + +<IfVersion < 2.4> +NameVirtualHost *:%(port)s +</IfVersion> +<VirtualHost _default_:%(port)s> +<Location /> +Order deny,allow +Deny from all +<IfDefine WSGI_ALLOW_LOCALHOST> +Allow from localhost +</IfDefine> +</Location> +</VirtualHost> +<IfDefine !WSGI_HTTPS_ONLY> +<VirtualHost *:%(port)s> +ServerName %(server_name)s +<IfDefine WSGI_SERVER_ALIAS> +ServerAlias %(server_aliases)s +</IfDefine> +</VirtualHost> +<IfDefine WSGI_REDIRECT_WWW> +<VirtualHost *:%(port)s> +ServerName %(parent_domain)s +Redirect permanent / http://%(server_name)s:%(port)s/ +</VirtualHost> +</IfDefine> +</IfDefine> + +<IfDefine WSGI_HTTPS_ONLY> +<VirtualHost *:%(port)s> +ServerName %(server_name)s +<IfDefine WSGI_SERVER_ALIAS> +ServerAlias %(server_aliases)s +RewriteEngine On +RewriteCond %%{HTTPS} off +RewriteRule (.*) https://%%{HTTP_HOST}%%{REQUEST_URI} +</IfDefine> +</VirtualHost> +<IfDefine WSGI_REDIRECT_WWW> +<VirtualHost *:%(port)s> +ServerName %(parent_domain)s +RewriteEngine On +RewriteCond %%{HTTPS} off +RewriteRule (.*) https://%%{HTTP_HOST}%%{REQUEST_URI} +</VirtualHost> +</IfDefine> +</IfDefine> + +</IfDefine> + +<IfDefine WSGI_VIRTUAL_HOST> + +<IfDefine WSGI_WITH_SSL> +<IfDefine WSGI_LISTENER_HOST> +Listen %(host)s:%(ssl_port)s +</IfDefine> +<IfDefine !WSGI_LISTENER_HOST> +Listen %(ssl_port)s +</IfDefine> +<IfVersion < 2.4> +NameVirtualHost *:%(ssl_port)s +</IfVersion> +<VirtualHost _default_:%(ssl_port)s> +<Location /> +Order deny,allow +Deny from all +<IfDefine WSGI_ALLOW_LOCALHOST> +Allow from localhost +</IfDefine> +</Location> +SSLEngine On +SSLCertificateFile %(ssl_certificate)s.crt +SSLCertificateKeyFile %(ssl_certificate)s.key +</VirtualHost> +<VirtualHost *:%(ssl_port)s> +ServerName %(server_name)s +<IfDefine WSGI_SERVER_ALIAS> +ServerAlias %(server_aliases)s +</IfDefine> +SSLEngine On +SSLCertificateFile %(ssl_certificate)s.crt +SSLCertificateKeyFile %(ssl_certificate)s.key +</VirtualHost> +<IfDefine WSGI_REDIRECT_WWW> +<VirtualHost *:%(ssl_port)s> +ServerName %(parent_domain)s +Redirect permanent / https://%(server_name)s:%(ssl_port)s/ +SSLEngine On +SSLCertificateFile %(ssl_certificate)s.crt +SSLCertificateKeyFile %(ssl_certificate)s.key +</VirtualHost> +</IfDefine> +</IfDefine> + +</IfDefine> + DocumentRoot '%(document_root)s' <Directory '%(server_root)s'> @@ -300,8 +509,14 @@ DocumentRoot '%(document_root)s' </Directory> <Directory '%(document_root)s%(mount_point)s'> +<IfDefine WSGI_DIRECTORY_INDEX> + DirectoryIndex %(directory_index)s +</IfDefine> RewriteEngine On RewriteCond %%{REQUEST_FILENAME} !-f +<IfDefine WSGI_DIRECTORY_INDEX> + RewriteCond %%{REQUEST_FILENAME} !-d +</IfDefine> <IfDefine WSGI_SERVER_STATUS> RewriteCond %%{REQUEST_URI} !/server-status </IfDefine> @@ -314,10 +529,45 @@ DocumentRoot '%(document_root)s' WSGIErrorOverride On </IfDefine> +<IfDefine WSGI_AUTH_USER> +<Location /> + AuthType %(auth_type)s + AuthName '%(host)s:%(port)s' + Auth%(auth_type)sProvider wsgi + WSGIAuthUserScript '%(auth_user_script)s' +<IfDefine WSGI_AUTH_GROUP> + WSGIAuthGroupScript '%(auth_group_script)s' +</IfDefine> +<IfVersion < 2.4> + Require valid-user +<IfDefine WSGI_AUTH_GROUP> + Require wsgi-group '%(auth_group)s' +</IfDefine> +</IfVersion> +<IfVersion >= 2.4> + <RequireAll> + Require valid-user +<IfDefine WSGI_AUTH_GROUP> + Require wsgi-group '%(auth_group)s' +</IfDefine> + </RequireAll> +</IfVersion> +</Location> +</IfDefine> + +<IfDefine !ONE_PROCESS> WSGIHandlerScript wsgi-handler '%(server_root)s/handler.wsgi' \\ process-group='%(host)s:%(port)s' application-group=%%{GLOBAL} WSGIImportScript '%(server_root)s/handler.wsgi' \\ process-group='%(host)s:%(port)s' application-group=%%{GLOBAL} +</IfDefine> + +<IfDefine ONE_PROCESS> +WSGIHandlerScript wsgi-handler '%(server_root)s/handler.wsgi' \\ + process-group='%%{GLOBAL}' application-group=%%{GLOBAL} +WSGIImportScript '%(server_root)s/handler.wsgi' \\ + process-group='%%{GLOBAL}' application-group=%%{GLOBAL} +</IfDefine> """ APACHE_ALIAS_DIRECTORY_CONFIG = """ @@ -360,6 +610,14 @@ APACHE_ERROR_DOCUMENT_CONFIG = """ ErrorDocument '%(status)s' '%(document)s' """ +APACHE_SETENV_CONFIG = """ +SetEnv '%(name)s' '%(value)s' +""" + +APACHE_PASSENV_CONFIG = """ +PassEnv '%(name)s' +""" + APACHE_INCLUDE_CONFIG = """ Include '%(filename)s' """ @@ -410,6 +668,15 @@ def generate_apache_config(options): print(APACHE_ERROR_DOCUMENT_CONFIG % dict(status=status, document=document.replace("'", "\\'")), file=fp) + if options['setenv_variables']: + for name, value in options['setenv_variables']: + print(APACHE_SETENV_CONFIG % dict(name=name, value=value), + file=fp) + + if options['passenv_variables']: + for name in options['passenv_variables']: + print(APACHE_PASSENV_CONFIG % dict(name=name), file=fp) + if options['include_files']: for filename in options['include_files']: filename = os.path.abspath(filename) @@ -535,26 +802,44 @@ def start_reloader(interval=1.0): class ApplicationHandler(object): - def __init__(self, script, callable_object='application', mount_point='/', - with_newrelic=False, with_wdb=False): + def __init__(self, entry_point, application_type='script', + callable_object='application', mount_point='/', + with_newrelic=False, with_wdb=False, debug_mode=False): - self.script = script + self.entry_point = entry_point + self.application_type = application_type self.callable_object = callable_object self.mount_point = mount_point - self.module = imp.new_module('__wsgi__') - self.module.__file__ = script - - with open(script, 'r') as fp: - code = compile(fp.read(), script, 'exec', dont_inherit=True) - exec(code, self.module.__dict__) - - self.application = getattr(self.module, callable_object) - - sys.modules['__wsgi__'] = self.module + if application_type == 'module': + __import__(entry_point) + self.module = sys.modules[entry_point] + self.application = getattr(self.module, callable_object) + self.target = self.module.__file__ + parts = os.path.splitext(self.target)[-1] + if parts[-1].lower() in ('.pyc', '.pyd', '.pyd'): + self.target = parts[0] + '.py' + + elif application_type == 'paste': + from paste.deploy import loadapp + self.application = loadapp('config:%s' % entry_point) + self.target = entry_point + + else: + self.module = imp.new_module('__wsgi__') + self.module.__file__ = entry_point + + with open(entry_point, 'r') as fp: + code = compile(fp.read(), entry_point, 'exec', + dont_inherit=True) + exec(code, self.module.__dict__) + + sys.modules['__wsgi__'] = self.module + self.application = getattr(self.module, callable_object) + self.target = entry_point try: - self.mtime = os.path.getmtime(script) + self.mtime = os.path.getmtime(self.target) except Exception: self.mtime = None @@ -564,6 +849,8 @@ class ApplicationHandler(object): if with_wdb: self.setup_wdb() + self.debug_mode = debug_mode + def setup_newrelic(self): import newrelic.agent @@ -585,8 +872,11 @@ class ApplicationHandler(object): self.application = WdbMiddleware(self.application) def reload_required(self, environ): + if self.debug_mode: + return False + try: - mtime = os.path.getmtime(self.script) + mtime = os.path.getmtime(self.target) except Exception: mtime = None @@ -615,19 +905,24 @@ class ApplicationHandler(object): WSGI_HANDLER_SCRIPT = """ import mod_wsgi.server -script = '%(script)s' +entry_point = '%(entry_point)s' +application_type = '%(application_type)s' callable_object = '%(callable_object)s' mount_point = '%(mount_point)s' with_newrelic = %(with_newrelic_agent)s with_wdb = %(with_wdb)s +reload_on_changes = %(reload_on_changes)s +debug_mode = %(debug_mode)s -handler = mod_wsgi.server.ApplicationHandler(script, callable_object, - mount_point, with_newrelic=with_newrelic, with_wdb=with_wdb) +handler = mod_wsgi.server.ApplicationHandler(entry_point, + application_type=application_type, callable_object=callable_object, + mount_point=mount_point, with_newrelic=with_newrelic, + with_wdb=with_wdb, debug_mode=debug_mode) reload_required = handler.reload_required handle_request = handler.handle_request -if %(reload_on_changes)s: +if reload_on_changes and not debug_mode: mod_wsgi.server.start_reloader() """ @@ -742,6 +1037,8 @@ def generate_wdb_server_script(options): WSGI_CONTROL_SCRIPT = """ #!/bin/sh +# %(sys_argv)s + HTTPD="%(httpd_executable)s %(httpd_arguments)s" WSGI_RUN_USER="${WSGI_RUN_USER:-%(user)s}" @@ -750,6 +1047,12 @@ WSGI_RUN_GROUP="${WSGI_RUN_GROUP:-%(group)s}" export WSGI_RUN_USER export WSGI_RUN_GROUP +LANG='%(lang)s' +LC_ALL='%(locale)s' + +export LANG +export LOCALE + ACMD="$1" ARGV="$@" @@ -807,6 +1110,16 @@ def check_percentage(option, opt_str, value, parser): setattr(parser.values, option.dest, value) option_list = ( + optparse.make_option('--application-type', default='script', + metavar='TYPE', help='The type of WSGI application entry point ' + 'that was provided. Defaults to \'script\', indicating the ' + 'traditional mod_wsgi style WSGI script file specified by a ' + 'filesystem path. Alternatively one can supply \'module\', ' + 'indicating that the provided entry point is a Python module ' + 'which should be imported using the standard Python import ' + 'mechanism, or \'paste\' indicating that the provided entry ' + 'point is a Paste deployment configuration file.'), + optparse.make_option('--host', default=None, metavar='IP-ADDRESS', help='The specific host (IP address) interface on which ' 'requests are to be accepted. Defaults to listening on ' @@ -815,6 +1128,33 @@ option_list = ( metavar='NUMBER', help='The specific port to bind to and ' 'on which requests are to be accepted. Defaults to port 8000.'), + optparse.make_option('--ssl-port', type='int', metavar='NUMBER', + help='The specific port to bind to and on which requests are ' + 'to be accepted for SSL connections.'), + optparse.make_option('--ssl-certificate', default=None, + metavar='FILE-PATH', help='Specify the path to the SSL ' + 'certificate files. It is expected that the files have \'.crt\' ' + 'and \'.key\' extensions. This option should refer to the ' + 'common part of the names for both files which appears before ' + 'the extension.'), + optparse.make_option('--https-only', action='store_true', + default=False, help='Flag indicating whether any requests ' + 'made using a HTTP request over the non SSL connection should ' + 'be redirected automatically to use a HTTPS request over the ' + 'SSL connection.'), + + optparse.make_option('--server-name', default=None, metavar='HOSTNAME', + help='The primary host name of the web server. If this name ' + 'starts with \'www.\' then an automatic redirection from the ' + 'parent domain name to the \'www.\' server name will created.'), + optparse.make_option('--server-alias', action='append', + dest='server_aliases', metavar='HOSTNAME', help='A secondary ' + 'host name for the web server. May include wilcard patterns.'), + optparse.make_option('--allow-localhost', action='store_true', + default=False, help='Flag indicating whether access via ' + 'localhost should still be allowed when a server name has been ' + 'specified and a name based virtual host has been configured.'), + optparse.make_option('--processes', type='int', metavar='NUMBER', help='The number of worker processes (instances of the WSGI ' 'application) to be started up and which will handle requests ' @@ -956,9 +1296,22 @@ option_list = ( 'application within the WSGI script file. Defaults to ' 'the name \'application\'.'), + optparse.make_option('--map-head-to-get', default='Auto', + metavar='OFF|ON|AUTO', help='Flag indicating whether HEAD ' + 'requests should be mapped to a GET request. By default a HEAD ' + 'request will be automatically mapped to a GET request when an ' + 'Apache output filter is detected that may want to see the ' + 'entire response in order to set up response headers correctly ' + 'for a HEAD request. This can be disable by setting to \'Off\'.'), + optparse.make_option('--document-root', metavar='DIRECTORY-PATH', help='The directory which should be used as the document root ' 'and which contains any static files.'), + optparse.make_option('--directory-index', metavar='FILE-NAME', + help='The name of a directory index resource to be found in the ' + 'document root directory. Requests mapping to the directory ' + 'will be mapped to this resource rather than being passed ' + 'through to the WSGI application.'), optparse.make_option('--mount-point', metavar='URL-PATH', default='/', help='The URL path at which the WSGI application will be ' @@ -983,6 +1336,11 @@ option_list = ( 'to be made over the same connection. Defaults to 0, indicating ' 'that keep alive connections are disabled.'), + optparse.make_option('--compress-responses', action='store_true', + default=False, help='Flag indicating whether responses for ' + 'common text based responses, such as plain text, HTML, XML, ' + 'CSS and Javascript should be compressed.'), + optparse.make_option('--server-metrics', action='store_true', default=False, help='Flag indicating whether internal server ' 'metrics will be available within the WSGI application. ' @@ -992,6 +1350,25 @@ option_list = ( 'will be available at the /server-status sub URL. Defaults to ' 'being disabled.'), + optparse.make_option('--auth-user-script', metavar='SCRIPT-PATH', + default=None, help='Specify a Python script file for ' + 'performing user authentication.'), + optparse.make_option('--auth-type', metavar='TYPE', + default='Basic', help='Specify the type of authentication ' + 'scheme used when authenticating users. Defaults to using ' + '\'Basic\'. Alternate schemes available are \'Digest\'.'), + + optparse.make_option('--auth-group-script', metavar='SCRIPT-PATH', + default=None, help='Specify a Python script file for ' + 'performing group based authorization in conjunction with ' + 'a user authentication script.'), + optparse.make_option('--auth-group', metavar='SCRIPT-PATH', + default='wsgi', help='Specify the group which users should ' + 'be a member of when using a group based authorization script. ' + 'Defaults to \'wsgi\' as a place holder but should be ' + 'overridden to be the actual group you use rather than ' + 'making your group name match the default.'), + optparse.make_option('--include-file', action='append', dest='include_files', metavar='FILE-PATH', help='Specify the ' 'path to an additional web server configuration file to be ' @@ -1010,6 +1387,16 @@ option_list = ( 'as normally defined by the LC_ALL environment variable. ' 'Defaults to \'en_US.UTF-8\'.'), + optparse.make_option('--setenv', action='append', nargs=2, + dest='setenv_variables', metavar='KEY VALUE', help='Specify ' + 'a name/value pairs to be added to the per request WSGI environ ' + 'dictionary'), + optparse.make_option('--passenv', action='append', + dest='passenv_variables', metavar='KEY', help='Specify the ' + 'names of any process level environment variables which should ' + 'be passed as a name/value pair in the per request WSGI ' + 'environ dictionary.'), + optparse.make_option('--working-directory', metavar='DIRECTORY-PATH', help='Specify the directory which should be used as the ' 'current working directory of the WSGI application. This ' @@ -1038,6 +1425,16 @@ option_list = ( help='Flag indicating whether the web server startup log should ' 'be enabled. Defaults to being disabled.'), + optparse.make_option('--rotate-logs', action='store_true', default=False, + help='Flag indicating whether log rotation should be performed.'), + optparse.make_option('--max-log-size', default=5, type='int', + metavar='MB', help='The maximum size in MB the log file should ' + 'be allowed to reach before log file rotation is performed.'), + + optparse.make_option('--rotatelogs-executable', + default=apxs_config.ROTATELOGS, metavar='FILE-PATH', + help='Override the path to the rotatelogs executable.'), + optparse.make_option('--python-eggs', metavar='DIRECTORY-PATH', help='Specify an alternate directory which should be used for ' 'unpacking of Python eggs. Defaults to a sub directory of ' @@ -1070,10 +1467,21 @@ option_list = ( help='Flag indicating whether the wdb interactive debugger ' 'should be enabled for the WSGI application.'), + optparse.make_option('--with-php5', action='store_true', default=False, + help='Flag indicating whether PHP 5 support should be enabled.'), + optparse.make_option('--enable-docs', action='store_true', default=False, help='Flag indicating whether the mod_wsgi documentation should ' 'be made available at the /__wsgi__/docs sub URL.'), + optparse.make_option('--debug-mode', action='store_true', default=False, + help='Flag indicating whether to run in single process mode ' + 'to allow the running of an interactive Python debugger. This ' + 'will override all options related to processes, threads and ' + 'communication with workers. All forms of source code reloading ' + 'will also be disabled. Both stdin and stdout will be attached ' + 'to the console to allow interaction with the Python debugger.'), + optparse.make_option('--setup-only', action='store_true', default=False, help='Flag indicating that after the configuration files have ' 'been setup, that the command should then exit and not go on ' @@ -1098,14 +1506,19 @@ def cmd_setup_server(params): def _mpm_module_defines(modules_directory): result = [] workers = ['event', 'worker', 'prefork'] + found = False for name in workers: if os.path.exists(os.path.join(modules_directory, 'mod_mpm_%s.so' % name)): - result.append('-DWSGI_MPM_%s_MODULE' % name.upper()) - break + if not found: + result.append('-DWSGI_MPM_ENABLE_%s_MODULE' % name.upper()) + found = True + result.append('-DWSGI_MPM_EXISTS_%s_MODULE' % name.upper()) return result def _cmd_setup_server(command, args, options): + options['sys_argv'] = repr(sys.argv) + options['mod_wsgi_so'] = where() options['working_directory'] = options['working_directory'] or os.getcwd() @@ -1128,12 +1541,27 @@ def _cmd_setup_server(command, args, options): except Exception: pass + if options['ssl_certificate']: + options['ssl_certificate'] = os.path.abspath( + options['ssl_certificate']) + if not args: - options['script'] = os.path.join(options['server_root'], + options['entry_point'] = os.path.join(options['server_root'], 'default.wsgi') + options['application_type'] = 'script' options['enable_docs'] = True + elif options['application_type'] in ('script', 'paste'): + options['entry_point'] = os.path.abspath(args[0]) else: - options['script'] = os.path.abspath(args[0]) + options['entry_point'] = args[0] + + if options['auth_user_script']: + options['auth_user_script'] = os.path.abspath( + options['auth_user_script']) + + if options['auth_group_script']: + options['auth_group_script'] = os.path.abspath( + options['auth_group_script']) options['documentation_directory'] = os.path.join(os.path.dirname( os.path.dirname(__file__)), 'docs') @@ -1146,9 +1574,6 @@ def _cmd_setup_server(command, args, options): else: options['documentation_url'] = 'http://www.modwsgi.org/' - options['script_directory'] = os.path.dirname(options['script']) - options['script_filename'] = os.path.basename(options['script']) - if not os.path.isabs(options['server_root']): options['server_root'] = os.path.abspath(options['server_root']) @@ -1336,27 +1761,73 @@ def _cmd_setup_server(command, args, options): options['httpd_arguments_list'].append( options['startup_log_filename']) + if options['server_name']: + host = options['server_name'] + else: + host = options['host'] + if options['port'] == 80: - options['url'] = 'http://%s/' % options['host'] + options['url'] = 'http://%s/' % host + else: + options['url'] = 'http://%s:%s/' % (host, options['port']) + + if options['ssl_port'] == 443: + options['ssl_url'] = 'https://%s/' % host + elif options['ssl_port'] is not None: + options['ssl_url'] = 'https://%s:%s/' % (host, options['ssl_port']) else: - options['url'] = 'http://%s:%s/' % (options['host'], - options['port']) + options['ssl_url'] = None + + if options['debug_mode']: + options['httpd_arguments_list'].append('-DONE_PROCESS') + + options['parent_domain'] = 'unspecified' + + if options['server_name']: + options['httpd_arguments_list'].append('-DWSGI_VIRTUAL_HOST') + if options['server_name'].lower().startswith('www.'): + options['httpd_arguments_list'].append('-DWSGI_REDIRECT_WWW') + options['parent_domain'] = options['server_name'][4:] + + if options['ssl_port'] and options['ssl_certificate']: + options['httpd_arguments_list'].append('-DWSGI_WITH_SSL') + if options['https_only']: + options['httpd_arguments_list'].append('-DWSGI_HTTPS_ONLY') + + if options['server_aliases']: + options['httpd_arguments_list'].append('-DWSGI_SERVER_ALIAS') + options['server_aliases'] = ' '.join(options['server_aliases']) + + if options['allow_localhost']: + options['httpd_arguments_list'].append('-DWSGI_ALLOW_LOCALHOST') if options['server_metrics']: options['httpd_arguments_list'].append('-DWSGI_SERVER_METRICS') if options['server_status']: options['httpd_arguments_list'].append('-DWSGI_SERVER_METRICS') options['httpd_arguments_list'].append('-DWSGI_SERVER_STATUS') + if options['directory_index']: + options['httpd_arguments_list'].append('-DWSGI_DIRECTORY_INDEX') if options['access_log']: options['httpd_arguments_list'].append('-DWSGI_ACCESS_LOG') + if options['rotate_logs']: + options['httpd_arguments_list'].append('-DWSGI_ROTATE_LOGS') if options['keep_alive'] != 0: options['httpd_arguments_list'].append('-DWSGI_KEEP_ALIVE') + if options['compress_responses'] != 0: + options['httpd_arguments_list'].append('-DWSGI_COMPRESS_RESPONSES') if options['multiprocess']: options['httpd_arguments_list'].append('-DWSGI_MULTIPROCESS') if options['listener_host']: options['httpd_arguments_list'].append('-DWSGI_LISTENER_HOST') if options['error_override']: options['httpd_arguments_list'].append('-DWSGI_ERROR_OVERRIDE') + if options['auth_user_script']: + options['httpd_arguments_list'].append('-DWSGI_AUTH_USER') + if options['auth_group_script']: + options['httpd_arguments_list'].append('-DWSGI_AUTH_GROUP') + if options['with_php5']: + options['httpd_arguments_list'].append('-DWSGI_WITH_PHP5') options['httpd_arguments_list'].extend( _mpm_module_defines(options['modules_directory'])) @@ -1371,6 +1842,9 @@ def _cmd_setup_server(command, args, options): print('Server URL :', options['url']) + if options['ssl_url']: + print('Server URL (SSL) :', options['ssl_url']) + if options['server_status']: print('Server Status :', '%sserver-status' % options['url']) diff --git a/src/server/management/commands/runmodwsgi.py b/src/server/management/commands/runmodwsgi.py index b80541e..ad7cd8e 100644 --- a/src/server/management/commands/runmodwsgi.py +++ b/src/server/management/commands/runmodwsgi.py @@ -29,7 +29,16 @@ class Command(BaseCommand): args = [script_file] options['callable_object'] = callable_object - options['working_directory'] = settings.BASE_DIR + # If there is no BASE_DIR in Django settings, assume that + # the current working directory is the parent directory of + # the directory the settings module is in. + + if hasattr(settings, 'BASE_DIR'): + options['working_directory'] = settings.BASE_DIR + else: + settings_mod = sys.modules[os.environ['DJANGO_SETTINGS_MODULE']] + parent = os.path.dirname(os.path.dirname(settings_mod.__file__)) + options['working_directory'] = parent url_aliases = options.setdefault('url_aliases') or [] diff --git a/src/server/mod_wsgi.c b/src/server/mod_wsgi.c index 1dcd01f..332ff94 100644 --- a/src/server/mod_wsgi.c +++ b/src/server/mod_wsgi.c @@ -178,6 +178,11 @@ static void *wsgi_merge_server_config(apr_pool_t *p, void *base_conf, else config->chunked_request = parent->chunked_request; + if (child->map_head_to_get != -1) + config->map_head_to_get = child->map_head_to_get; + else + config->map_head_to_get = parent->map_head_to_get; + if (child->enable_sendfile != -1) config->enable_sendfile = child->enable_sendfile; else @@ -211,6 +216,7 @@ typedef struct { int script_reloading; int error_override; int chunked_request; + int map_head_to_get; int enable_sendfile; @@ -242,6 +248,7 @@ static WSGIDirectoryConfig *newWSGIDirectoryConfig(apr_pool_t *p) object->script_reloading = -1; object->error_override = -1; object->chunked_request = -1; + object->map_head_to_get = -1; object->enable_sendfile = -1; @@ -325,6 +332,11 @@ static void *wsgi_merge_dir_config(apr_pool_t *p, void *base_conf, else config->chunked_request = parent->chunked_request; + if (child->map_head_to_get != -1) + config->map_head_to_get = child->map_head_to_get; + else + config->map_head_to_get = parent->map_head_to_get; + if (child->enable_sendfile != -1) config->enable_sendfile = child->enable_sendfile; else @@ -383,6 +395,7 @@ typedef struct { int script_reloading; int error_override; int chunked_request; + int map_head_to_get; int enable_sendfile; @@ -732,6 +745,14 @@ static WSGIRequestConfig *wsgi_create_req_config(apr_pool_t *p, request_rec *r) config->chunked_request = 0; } + config->map_head_to_get = dconfig->map_head_to_get; + + if (config->map_head_to_get < 0) { + config->map_head_to_get = sconfig->map_head_to_get; + if (config->map_head_to_get < 0) + config->map_head_to_get = 2; + } + config->enable_sendfile = dconfig->enable_sendfile; if (config->enable_sendfile < 0) { @@ -4739,6 +4760,40 @@ static const char *wsgi_set_chunked_request(cmd_parms *cmd, void *mconfig, return NULL; } +static const char *wsgi_set_map_head_to_get(cmd_parms *cmd, void *mconfig, + const char *f) +{ + if (cmd->path) { + WSGIDirectoryConfig *dconfig = NULL; + dconfig = (WSGIDirectoryConfig *)mconfig; + + if (strcasecmp(f, "Off") == 0) + dconfig->map_head_to_get = 0; + else if (strcasecmp(f, "On") == 0) + dconfig->map_head_to_get = 1; + else if (strcasecmp(f, "Auto") == 0) + dconfig->map_head_to_get = 2; + else + return "WSGIMapHEADToGET must be one of: Off | On | Auto"; + } + else { + WSGIServerConfig *sconfig = NULL; + sconfig = ap_get_module_config(cmd->server->module_config, + &wsgi_module); + + if (strcasecmp(f, "Off") == 0) + sconfig->map_head_to_get = 0; + else if (strcasecmp(f, "On") == 0) + sconfig->map_head_to_get = 1; + else if (strcasecmp(f, "Auto") == 0) + sconfig->map_head_to_get = 2; + else + return "WSGIMapHEADToGET must be one of: Off | On | Auto"; + } + + return NULL; +} + static const char *wsgi_set_enable_sendfile(cmd_parms *cmd, void *mconfig, const char *f) { @@ -5203,6 +5258,8 @@ static void wsgi_log_script_error(request_rec *r, const char *e, const char *n) ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "%s", message); } +static void wsgi_drop_invalid_headers(request_rec *r); + static void wsgi_build_environment(request_rec *r) { WSGIRequestConfig *config = NULL; @@ -5218,7 +5275,21 @@ static void wsgi_build_environment(request_rec *r) config = (WSGIRequestConfig *)ap_get_module_config(r->request_config, &wsgi_module); - /* Populate environment with standard CGI variables. */ + /* + * Populate environment with standard CGI variables. Before + * we do this though, we ensure that we delete any headers + * which use invalid characters. This is necessary to ensure + * that someone doesn't try and take advantage of header + * spoofing. This can come about where characters other than + * alphanumerics or '-' are used as the conversion of non + * alphanumerics to '_' means one can get collisions. This + * is technically only an issue with Apache 2.2 as Apache + * 2.4 addresses the problem and drops them anyway. Still go + * through and drop them even for Apache 2.4 as not sure + * which version of Apache 2.4 introduces the change. + */ + + wsgi_drop_invalid_headers(r); ap_add_cgi_vars(r); ap_add_common_vars(r); @@ -5237,11 +5308,24 @@ static void wsgi_build_environment(request_rec *r) * content is generated. If using Apache 2.X we can skip * doing this if we know there is no output filter that * might change the content and/or headers. + * + * The default behaviour here of changing it if an output + * filter is detected can be overridden using the directive + * WSGIMapHEADToGet. The default value is 'Auto'. If set to + * 'On' then it remapped regardless of whether an output + * filter is present. If 'Off' then it will be left alone + * and the original value used. */ - if (r->method_number == M_GET && r->header_only && - r->output_filters->frec->ftype < AP_FTYPE_PROTOCOL) - apr_table_setn(r->subprocess_env, "REQUEST_METHOD", "GET"); + if (config->map_head_to_get == 2) { + if (r->method_number == M_GET && r->header_only && + r->output_filters->frec->ftype < AP_FTYPE_PROTOCOL) + apr_table_setn(r->subprocess_env, "REQUEST_METHOD", "GET"); + } + else if (config->map_head_to_get == 1) { + if (r->method_number == M_GET) + apr_table_setn(r->subprocess_env, "REQUEST_METHOD", "GET"); + } /* Determine whether connection uses HTTPS protocol. */ @@ -6907,6 +6991,17 @@ static void wsgi_signal_handler(int signum) } } +static void wsgi_exit_daemon_process(int status) +{ + if (wsgi_server && wsgi_daemon_group) { + ap_log_error(APLOG_MARK, APLOG_INFO, 0, wsgi_server, + "mod_wsgi (pid=%d): Exiting process '%s'.", getpid(), + wsgi_daemon_group); + } + + exit(status); +} + static int wsgi_start_process(apr_pool_t *p, WSGIDaemonProcess *daemon); static void wsgi_manage_process(int reason, void *data, apr_wait_t status) @@ -7229,7 +7324,7 @@ static int wsgi_setup_access(WSGIDaemonProcess *daemon) sleep(20); - exit(-1); + wsgi_exit_daemon_process(-1); } /* @@ -7895,7 +7990,7 @@ static void *wsgi_reaper_thread(apr_thread_t *thd, void *data) "mod_wsgi (pid=%d): Aborting process '%s'.", getpid(), daemon->group->name); - exit(-1); + wsgi_exit_daemon_process(-1); return NULL; } @@ -8612,7 +8707,7 @@ static int wsgi_start_process(apr_pool_t *p, WSGIDaemonProcess *daemon) sleep(20); - exit(-1); + wsgi_exit_daemon_process(-1); } /* Reinitialise accept mutex in daemon process. */ @@ -8631,7 +8726,7 @@ static int wsgi_start_process(apr_pool_t *p, WSGIDaemonProcess *daemon) sleep(20); - exit(-1); + wsgi_exit_daemon_process(-1); } } @@ -8740,7 +8835,7 @@ static int wsgi_start_process(apr_pool_t *p, WSGIDaemonProcess *daemon) sleep(20); - exit(-1); + wsgi_exit_daemon_process(-1); } wsgi_daemon_shutdown = 0; @@ -9076,7 +9171,7 @@ static int wsgi_start_process(apr_pool_t *p, WSGIDaemonProcess *daemon) /* Exit the daemon process when being shutdown. */ - exit(-1); + wsgi_exit_daemon_process(0); } apr_pool_note_subprocess(p, &daemon->process, APR_KILL_AFTER_TIMEOUT); @@ -11421,6 +11516,71 @@ static char *wsgi_original_uri(request_rec *r) return apr_pstrmemdup(r->pool, first, last - first); } +static int wsgi_http_invalid_header(const char *w) +{ + char c; + + while ((c = *w++) != 0) { + if (!apr_isalnum(c) && c != '-') + return 1; + } + + return 0; +} + +static void wsgi_drop_invalid_headers(request_rec *r) +{ + /* + * Apache 2.2 when converting headers for CGI variables, doesn't + * ignore headers with invalid names. That is, any which use any + * characters besides alphanumerics and the '-' character. This + * opens us up to header spoofing whereby something can inject + * multiple headers which differ by using non alphanumeric + * characters in the same position, which would then encode to same + * value. Since not easy to cleanup after the fact, as a workaround, + * is easier to simply remove the invalid headers. This will make + * things end up being the same as Apache 2.4. Doing this could + * annoy some users of Apache 2.2 who were using invalid headers, + * but things will break for them under Apache 2.4 anyway. + */ + + apr_array_header_t *to_delete = NULL; + + const apr_array_header_t *hdrs_arr; + const apr_table_entry_t *hdrs; + + int i; + + hdrs_arr = apr_table_elts(r->headers_in); + hdrs = (const apr_table_entry_t *) hdrs_arr->elts; + + for (i = 0; i < hdrs_arr->nelts; ++i) { + if (!hdrs[i].key) { + continue; + } + + if (wsgi_http_invalid_header(hdrs[i].key)) { + char **new; + + if (!to_delete) + to_delete = apr_array_make(r->pool, 1, sizeof(char *)); + + new = (char **)apr_array_push(to_delete); + *new = hdrs[i].key; + } + } + + if (to_delete) { + char *key; + + for (i = 0; i < to_delete->nelts; i++) { + key = ((char **)to_delete->elts)[i]; + + apr_table_unset(r->headers_in, key); + } + } +} + static char *wsgi_http2env(apr_pool_t *a, const char *w) { char *res = (char *)apr_palloc(a, sizeof("HTTP_") + strlen(w)); @@ -11434,12 +11594,14 @@ static char *wsgi_http2env(apr_pool_t *a, const char *w) *cp++ = '_'; while ((c = *w++) != 0) { - if (!apr_isalnum(c)) { - *cp++ = '_'; - } - else { + if (apr_isalnum(c)) { *cp++ = apr_toupper(c); } + else if (c == '-') { + *cp++ = '_'; + } + else + return NULL; } *cp = 0; @@ -11529,15 +11691,22 @@ static PyObject *Auth_environ(AuthObject *self, const char *group) continue; } else { + if (hdrs[i].val) { + char *header = wsgi_http2env(r->pool, hdrs[i].key); + + if (header) { #if PY_MAJOR_VERSION >= 3 - object = PyUnicode_DecodeLatin1(hdrs[i].val, - strlen(hdrs[i].val), NULL); + object = PyUnicode_DecodeLatin1(hdrs[i].val, + strlen(hdrs[i].val), NULL); #else - object = PyString_FromString(hdrs[i].val); + object = PyString_FromString(hdrs[i].val); #endif - PyDict_SetItemString(vars, wsgi_http2env(r->pool, hdrs[i].key), - object); - Py_DECREF(object); + + PyDict_SetItemString(vars, header, object); + + Py_DECREF(object); + } + } } } @@ -12134,10 +12303,31 @@ static authn_status wsgi_check_password(request_rec *r, const char *user, else if (result == Py_False) { status = AUTH_DENIED; } +#if PY_MAJOR_VERSION >= 3 + else if (PyUnicode_Check(result)) { + PyObject *str = NULL; + + str = PyUnicode_AsUTF8String(result); + + if (str) { + adapter->r->user = apr_pstrdup(adapter->r->pool, + PyString_AsString(str)); + + status = AUTH_GRANTED; + } + } +#else + else if (PyString_Check(result)) { + adapter->r->user = apr_pstrdup(adapter->r->pool, + PyString_AsString(result)); + + status = AUTH_GRANTED; + } +#endif else { PyErr_SetString(PyExc_TypeError, "Basic auth " "provider must return True, False " - "or None"); + "None or user name as string"); } Py_DECREF(result); @@ -13229,6 +13419,11 @@ static authz_status wsgi_check_authorization(request_rec *r, const char *t, *w; int status; +#if AP_MODULE_MAGIC_AT_LEAST(20100714,0) + if (!r->user) + return AUTHZ_DENIED_NO_USER; +#endif + config = wsgi_create_req_config(r->pool, r); if (!config->auth_group_script) { @@ -13311,7 +13506,11 @@ static int wsgi_hook_auth_checker(request_rec *r) t = reqs[x].requirement; w = ap_getword_white(r->pool, &t); +#if AP_MODULE_MAGIC_AT_LEAST(20100714,0) + if (!strcasecmp(w, "wsgi-group")) { +#else if (!strcasecmp(w, "group") || !strcasecmp(w, "wsgi-group")) { +#endif required_group = 1; if (!grpstatus) { @@ -13510,6 +13709,8 @@ static const command_rec wsgi_commands[] = NULL, OR_FILEINFO, "Enable/Disable overriding of error pages."), AP_INIT_TAKE1("WSGIChunkedRequest", wsgi_set_chunked_request, NULL, OR_FILEINFO, "Enable/Disable support for chunked requests."), + AP_INIT_TAKE1("WSGIMapHEADToGET", wsgi_set_map_head_to_get, + NULL, OR_FILEINFO, "Enable/Disable mapping of HEAD to GET."), #ifndef WIN32 AP_INIT_TAKE1("WSGIEnableSendfile", wsgi_set_enable_sendfile, diff --git a/src/server/wsgi_interp.c b/src/server/wsgi_interp.c index 98f011c..6d8e765 100644 --- a/src/server/wsgi_interp.c +++ b/src/server/wsgi_interp.c @@ -907,29 +907,30 @@ InterpreterObject *newInterpreterObject(const char *name) /* * If running in daemon mode and a home directory was set then - * insert an empty string at the start of the Python module search - * path so the current working directory will be searched. This - * makes things similar to when using the Python interpreter on the - * command line. If the current working directory changes then where - * it looks follows, so doesn't always look in home. + * insert the home directory at the start of the Python module + * search path. This makes things similar to when using the Python + * interpreter on the command line with a script. */ #if defined(MOD_WSGI_WITH_DAEMONS) if (wsgi_daemon_process && wsgi_daemon_process->group->home) { PyObject *path = NULL; + const char *home = wsgi_daemon_process->group->home; path = PySys_GetObject("path"); if (module && path) { - PyObject *empty; + PyObject *item; #if PY_MAJOR_VERSION >= 3 - empty = PyUnicode_DecodeLatin1("", strlen(""), NULL); + item = PyUnicode_Decode(home, strlen(home), + Py_FileSystemDefaultEncoding, + "surrogateescape"); #else - empty = PyString_FromString(""); + item = PyString_FromString(home); #endif - PyList_Insert(path, 0, empty); - Py_DECREF(empty); + PyList_Insert(path, 0, item); + Py_DECREF(item); } Py_XDECREF(module); diff --git a/src/server/wsgi_server.h b/src/server/wsgi_server.h index c58b980..a9fd74d 100644 --- a/src/server/wsgi_server.h +++ b/src/server/wsgi_server.h @@ -102,6 +102,7 @@ typedef struct { int script_reloading; int error_override; int chunked_request; + int map_head_to_get; int enable_sendfile; diff --git a/src/server/wsgi_version.h b/src/server/wsgi_version.h index ff2ae73..2ec827a 100644 --- a/src/server/wsgi_version.h +++ b/src/server/wsgi_version.h @@ -24,9 +24,9 @@ /* Module version information. */ #define MOD_WSGI_MAJORVERSION_NUMBER 4 -#define MOD_WSGI_MINORVERSION_NUMBER 2 -#define MOD_WSGI_MICROVERSION_NUMBER 8 -#define MOD_WSGI_VERSION_STRING "4.2.8" +#define MOD_WSGI_MINORVERSION_NUMBER 3 +#define MOD_WSGI_MICROVERSION_NUMBER 0 +#define MOD_WSGI_VERSION_STRING "4.3.0" /* ------------------------------------------------------------------------- */ diff --git a/tests/auth.wsgi b/tests/auth.wsgi new file mode 100644 index 0000000..37a90cb --- /dev/null +++ b/tests/auth.wsgi @@ -0,0 +1,29 @@ +def check_password(environ, user, password): + print('USER', user, environ['REQUEST_URI']) + if user == 'spy': + if password == 'secret': + return True + return False + elif user == 'witness': + if password == 'secret': + return 'protected' + return False + return None + +import md5 + +def get_realm_hash(environ, user, realm): + print('USER', user, environ['REQUEST_URI']) + if user == 'spy': + value = md5.new() + # user:realm:password + value.update('%s:%s:%s' % (user, realm, 'secret')) + hash = value.hexdigest() + return hash + return None + +def groups_for_user(environ, user): + print('GROUP', user, environ['REQUEST_URI']) + if user == 'spy': + return ['secret-agents'] + return [''] diff --git a/tests/environ.wsgi b/tests/environ.wsgi index 6ee1723..c26eae7 100644 --- a/tests/environ.wsgi +++ b/tests/environ.wsgi @@ -13,7 +13,7 @@ import apache def application(environ, start_response): headers = [] - headers.append(('Content-Type', 'text/plain')) + headers.append(('Content-Type', 'text/plain; charset="UTF-8"')) write = start_response('200 OK', headers) input = environ['wsgi.input'] @@ -24,6 +24,7 @@ def application(environ, start_response): print('GID: %s' % os.getgid(), file=output) print(file=output) + print('python.version: %r' % (sys.version,), file=output) print('apache.version: %r' % (apache.version,), file=output) print('mod_wsgi.version: %r' % (mod_wsgi.version,), file=output) print(file=output) |
