Search code examples
pythonwsgiwerkzeug

How to change response and content-length in uwsgi middleware?


I'm trying to write a middleware which replaces some data in the response thus changing the content length. For our development environment we want to simulate the behaviour of SSI includes of the actual webserver like Nginx or Apache for some static files which are not served through the application. We're using the included development server of werkzeug.

Here is what i have so far:

class ModifyBodyMiddleware(object):
    def __init__(self, app):
        self.app = app

    def __call__(self, environment, start_response):
        def my_start_response(status, headers, exc_info=None):
            # change content-length somehow
            start_response(status, headers, exc_info)

        body = self.app(environment, my_start_response)
        body = do_modifications(body)

        return body

For simplification just assume do_modifications does replace the whole content with foobar. I need the actual body to modify it but i also need to set the new content-length header somehow.

Thanks Goir


Solution

  • Ok i found a solution, instead of adding another Middleware I just overwrite the SharedDataMiddleware and modify the file when its read.

    EDIT: Added recursive calls to include file in included files. EDIT2: added support for #echo SSI

            class SharedDataSSIMiddleware(SharedDataMiddleware):
        """ Replace SSI includes with the real files on request
        """
        ssi_incl_expr = re.compile(r'<!-- *# *include *(virtual|file)=[\'\"]([^\'"]+)[\'\"] *-->')
        ssi_echo_expr = re.compile(r'<!-- *# *echo *encoding=[\'\"]([^\'"]+)[\'\"] *var=[\'\"]([^\'"]+)[\'\"] *-->')
    
        def __init__(self, app, exports, disallow=None, cache=True, cache_timeout=60 * 60 * 12, fallback_mimetype='text/plain'):
            super(SharedDataSSIMiddleware, self).__init__(app, exports, disallow, cache, cache_timeout, fallback_mimetype)
    
            self.environment = None
    
        def get_included_content(self, path_info, path):
            full_path = os.path.join(path_info, path)
            with open(full_path) as fp:
                data = fp.read()
                return self._ssi_include(full_path, data)
    
        def _get_ssi_echo_value(self, encoding, var_name):
            return self.environment.get(var_name)
    
        def _ssi_include(self, filename, content):
            content = re.sub(
                self.ssi_incl_expr,
                lambda x: self.get_included_content(os.path.dirname(filename), x.groups()[1]),
                content
            )
            content = re.sub(
                self.ssi_echo_expr,
                lambda x: self._get_ssi_echo_value(*x.groups()),
                content
            )
            return content
    
        def _opener(self, filename):
            file = cStringIO.StringIO()
            with open(filename, 'rb') as fp:
                content = fp.read()
                content = self._ssi_include(filename, content)
                file.write(content)
                file.flush()
                size = file.tell()
            file.reset()
    
            return lambda: (file, datetime.utcnow(), size)
    
        def __call__(self, environ, start_response):
            self.environment = environ
            response = super(SharedDataSSIMiddleware, self).__call__(environ, start_response)
            self.environment = None
            return response
    

    This reads the actual file, modifies it and returns a StringIO object with the modified data instead of the actual file. Don't use static_files parameter in run_simple of werkzeug, this will just add the default SharedDataMiddleware we don't want here.

    just wrap your app with the Middleware above:

    app = SharedDataSSIMiddleware(app, exports={'/foo': 'path'})