Ticket #268 (new defect)

Opened 4 years ago

Last modified 4 years ago

PrefixMiddleware shouldn't strip prefix from PATH_INFO

Reported by: catlee Owned by: ianb
Priority: normal Milestone:
Component: deploy Version: svn-trunk
Severity: normal Keywords:
Cc: chris@…

Description

I don't think that PrefixMiddleware? should be stripping the prefix from environPATH_INFO?.

I've run into this a few times when trying to deploy simple single-controller applications. One example is an application called 'signon' that manages single-signon for our intranet.

The app is deployed behind an apache reverse proxy which maps /signon on the apache server to the paste server. The signon application expects urls relative to its root to be actions like "signon", "signoff". So the client-visible url would be "/signon/signon".

PrefixMiddleware? currently strips the prefix from PATH_INFO, so "/signon/signon" gets passed to the application in PATH_INFO as "/" whereas "/signon/signoff" would get passed in as "/signoff". Other strange effects include "/signon/signon123" being passed in as "123"

I'm not sure if there is (or was) a use case for the current behaviour, in which case maybe an option to PrefixMiddleware? would be appropriate. Otherwise, the patch below should suffice.

  • tests/test_prefix_middleware.py

     
     1from paste.deploy.config import PrefixMiddleware 
     2from paste.fixture import TestApp 
     3from py.test import raises 
     4 
     5class Bug(Exception): pass 
     6 
     7def app(environ, start_response): 
     8    start_response('200 OK', [('Content-type', 'text/html')]) 
     9    retval = "%s %s" % (environ['PATH_INFO'], environ['SCRIPT_NAME']) 
     10    return iter(retval) 
     11 
     12def test_prefix(): 
     13    wrapped = PrefixMiddleware(app, {'test': 1}, "/signon") 
     14    test_app = TestApp(wrapped) 
     15    assert "/ /signon" == test_app.get("/").normal_body 
     16    assert "/foo /signon" == test_app.get("/foo").normal_body 
     17    assert "/signoff /signon" == test_app.get("/signoff").normal_body 
     18    assert "/signon /signon" == test_app.get("/signon").normal_body 
  • paste/deploy/config.py

     
    252252     
    253253    def __call__(self, environ, start_response): 
    254254        url = environ['PATH_INFO'] 
    255         url = re.sub(self.regprefix, r'\1', url) 
    256255        if not url: url = '/' 
    257256        environ['PATH_INFO'] = url 
    258257        environ['SCRIPT_NAME'] = self.prefix 

Change History

Changed 4 years ago by catlee

  • cc chris@… added

Changed 4 years ago by catlee

The following patch makes the stripping behaviour configurable, and updates the docstrings to help the user determine when the prefix should be stripped, and when it shouldn't be stripped.

I also slightly changed how the prefix regexp is created and used. re.escape is just in case somebody tries to use a prefix that contains regular expression meta characters.

  • tests/test_prefix_middleware.py

     
     1from paste.deploy.config import PrefixMiddleware 
     2from paste.fixture import TestApp 
     3from py.test import raises 
     4 
     5class Bug(Exception): pass 
     6 
     7def app(environ, start_response): 
     8    start_response('200 OK', [('Content-type', 'text/html')]) 
     9    retval = "%s %s" % (environ['PATH_INFO'], environ['SCRIPT_NAME']) 
     10    return iter(retval) 
     11 
     12def test_prefix(): 
     13    wrapped = PrefixMiddleware(app, {"test": "1"}, "/signon") 
     14    test_app = TestApp(wrapped) 
     15    assert "/ /signon" == test_app.get("/").normal_body 
     16    assert "/foo /signon" == test_app.get("/foo").normal_body 
     17    assert "/signoff /signon" == test_app.get("/signoff").normal_body 
     18    assert "/ /signon" == test_app.get("/signon").normal_body 
     19 
     20def test_prefix2(): 
     21    wrapped = PrefixMiddleware(app, {"test": "1"}, "/signon", strip_prefix=False) 
     22    test_app = TestApp(wrapped) 
     23    assert "/ /signon" == test_app.get("/").normal_body 
     24    assert "/foo /signon" == test_app.get("/foo").normal_body 
     25    assert "/signoff /signon" == test_app.get("/signoff").normal_body 
     26    assert "/signon /signon" == test_app.get("/signon").normal_body 
  • paste/deploy/config.py

     
    205205    is accessed via a reverse proxy with a prefix. The application is accessed 
    206206    through the reverse proxy via the the URL prefix '/james', whereas the 
    207207    reverse proxy forwards those requests to the application at the prefix '/'. 
     208    In this case you should also disable ``strip_prefix`` since the reverse 
     209    proxy is already removing the prefix from the request to the application. 
    208210 
    209211    The reverse proxy, being an entirely separate web server, has no way of 
    210212    specifying the SCRIPT_NAME variable; it must be manually set by a 
     
    239241 
    240242    You can also use ``scheme`` to explicitly set the scheme (like 
    241243    ``scheme = https``). 
     244 
     245    If ``strip_prefix`` is set, ``prefix`` will be stripped from 
     246    PATH_INFO when it appears as the first part of the requested url. 
     247 
     248    In some configurations, the reverse proxy will strip the prefix before 
     249    sending the request to your application, in which case PrefixMiddleware 
     250    should not try and strip the prefix again.  In configurations like these, 
     251    set ``strip_prefix`` to False 
    242252    """ 
    243253    def __init__(self, app, global_conf=None, prefix='/', 
    244254                 translate_forwarded_server=True, 
    245                  force_port=None, scheme=None): 
     255                 force_port=None, scheme=None, 
     256                 strip_prefix=True): 
    246257        self.app = app 
    247258        self.prefix = prefix.rstrip('/') 
    248259        self.translate_forwarded_server = translate_forwarded_server 
    249         self.regprefix = re.compile("^%s(.*)$" % self.prefix) 
     260        self.regprefix = re.compile("^%s(.*)$" % re.escape(self.prefix)) 
    250261        self.force_port = force_port 
    251262        self.scheme = scheme 
     263        self.strip_prefix = strip_prefix 
    252264     
    253265    def __call__(self, environ, start_response): 
    254266        url = environ['PATH_INFO'] 
    255         url = re.sub(self.regprefix, r'\1', url) 
     267        if self.strip_prefix: 
     268            url = self.regprefix.sub(r'\1', url) 
    256269        if not url: url = '/' 
    257270        environ['PATH_INFO'] = url 
    258271        environ['SCRIPT_NAME'] = self.prefix 
     
    280293def make_prefix_middleware( 
    281294    app, global_conf, prefix='/', 
    282295    translate_forwarded_server=True, 
    283     force_port=None, scheme=None): 
     296    force_port=None, scheme=None, strip_prefix=True): 
    284297    from paste.deploy.converters import asbool 
    285298    translate_forwarded_server = asbool(translate_forwarded_server) 
     299    strip_prefix = asbool(strip_prefix) 
    286300    return PrefixMiddleware( 
    287301        app, prefix=prefix, 
    288302        translate_forwarded_server=translate_forwarded_server, 
    289         force_port=force_port, scheme=scheme) 
     303        force_port=force_port, scheme=scheme, strip_prefix=strip_prefix) 
    290304 
    291305make_prefix_middleware.__doc__ = PrefixMiddleware.__doc__ 
Note: See TracTickets for help on using tickets.