# coding: utf-8 """Basic tests for the CherryPy core: request handling.""" import os import sys import types import six import cherrypy from cherrypy._cpcompat import ntou from cherrypy import _cptools, tools from cherrypy.lib import httputil, static from cherrypy.test._test_decorators import ExposeExamples from cherrypy.test import helper localDir = os.path.dirname(__file__) favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico') # Client-side code # class CoreRequestHandlingTest(helper.CPWebCase): @staticmethod def setup_server(): class Root: @cherrypy.expose def index(self): return 'hello' favicon_ico = tools.staticfile.handler(filename=favicon_path) @cherrypy.expose def defct(self, newct): newct = 'text/%s' % newct cherrypy.config.update({'tools.response_headers.on': True, 'tools.response_headers.headers': [('Content-Type', newct)]}) @cherrypy.expose def baseurl(self, path_info, relative=None): return cherrypy.url(path_info, relative=bool(relative)) root = Root() root.expose_dec = ExposeExamples() class TestType(type): """Metaclass which automatically exposes all functions in each subclass, and adds an instance of the subclass as an attribute of root. """ def __init__(cls, name, bases, dct): type.__init__(cls, name, bases, dct) for value in six.itervalues(dct): if isinstance(value, types.FunctionType): value.exposed = True setattr(root, name.lower(), cls()) Test = TestType('Test', (object, ), {}) @cherrypy.config(**{'tools.trailing_slash.on': False}) class URL(Test): def index(self, path_info, relative=None): if relative != 'server': relative = bool(relative) return cherrypy.url(path_info, relative=relative) def leaf(self, path_info, relative=None): if relative != 'server': relative = bool(relative) return cherrypy.url(path_info, relative=relative) def qs(self, qs): return cherrypy.url(qs=qs) def log_status(): Status.statuses.append(cherrypy.response.status) cherrypy.tools.log_status = cherrypy.Tool( 'on_end_resource', log_status) class Status(Test): def index(self): return 'normal' def blank(self): cherrypy.response.status = '' # According to RFC 2616, new status codes are OK as long as they # are between 100 and 599. # Here is an illegal code... def illegal(self): cherrypy.response.status = 781 return 'oops' # ...and here is an unknown but legal code. def unknown(self): cherrypy.response.status = '431 My custom error' return 'funky' # Non-numeric code def bad(self): cherrypy.response.status = 'error' return 'bad news' statuses = [] @cherrypy.config(**{'tools.log_status.on': True}) def on_end_resource_stage(self): return repr(self.statuses) class Redirect(Test): @cherrypy.config(**{ 'tools.err_redirect.on': True, 'tools.err_redirect.url': '/errpage', 'tools.err_redirect.internal': False, }) class Error: @cherrypy.expose def index(self): raise NameError('redirect_test') error = Error() def index(self): return 'child' def custom(self, url, code): raise cherrypy.HTTPRedirect(url, code) @cherrypy.config(**{'tools.trailing_slash.extra': True}) def by_code(self, code): raise cherrypy.HTTPRedirect('somewhere%20else', code) def nomodify(self): raise cherrypy.HTTPRedirect('', 304) def proxy(self): raise cherrypy.HTTPRedirect('proxy', 305) def stringify(self): return str(cherrypy.HTTPRedirect('/')) def fragment(self, frag): raise cherrypy.HTTPRedirect('/some/url#%s' % frag) def url_with_quote(self): raise cherrypy.HTTPRedirect("/some\"url/that'we/want") def url_with_xss(self): raise cherrypy.HTTPRedirect( "/someurl/that'we/want") def url_with_unicode(self): raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8')) def login_redir(): if not getattr(cherrypy.request, 'login', None): raise cherrypy.InternalRedirect('/internalredirect/login') tools.login_redir = _cptools.Tool('before_handler', login_redir) def redir_custom(): raise cherrypy.InternalRedirect('/internalredirect/custom_err') class InternalRedirect(Test): def index(self): raise cherrypy.InternalRedirect('/') @cherrypy.expose @cherrypy.config(**{'hooks.before_error_response': redir_custom}) def choke(self): return 3 / 0 def relative(self, a, b): raise cherrypy.InternalRedirect('cousin?t=6') def cousin(self, t): assert cherrypy.request.prev.closed return cherrypy.request.prev.query_string def petshop(self, user_id): if user_id == 'parrot': # Trade it for a slug when redirecting raise cherrypy.InternalRedirect( '/image/getImagesByUser?user_id=slug') elif user_id == 'terrier': # Trade it for a fish when redirecting raise cherrypy.InternalRedirect( '/image/getImagesByUser?user_id=fish') else: # This should pass the user_id through to getImagesByUser raise cherrypy.InternalRedirect( '/image/getImagesByUser?user_id=%s' % str(user_id)) # We support Python 2.3, but the @-deco syntax would look like # this: # @tools.login_redir() def secure(self): return 'Welcome!' secure = tools.login_redir()(secure) # Since calling the tool returns the same function you pass in, # you could skip binding the return value, and just write: # tools.login_redir()(secure) def login(self): return 'Please log in' def custom_err(self): return 'Something went horribly wrong.' @cherrypy.config(**{'hooks.before_request_body': redir_custom}) def early_ir(self, arg): return 'whatever' class Image(Test): def getImagesByUser(self, user_id): return '0 images for %s' % user_id class Flatten(Test): def as_string(self): return 'content' def as_list(self): return ['con', 'tent'] def as_yield(self): yield b'content' @cherrypy.config(**{'tools.flatten.on': True}) def as_dblyield(self): yield self.as_yield() def as_refyield(self): for chunk in self.as_yield(): yield chunk class Ranges(Test): def get_ranges(self, bytes): return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) def slice_file(self): path = os.path.join(os.getcwd(), os.path.dirname(__file__)) return static.serve_file( os.path.join(path, 'static/index.html')) class Cookies(Test): def single(self, name): cookie = cherrypy.request.cookie[name] # Python2's SimpleCookie.__setitem__ won't take unicode keys. cherrypy.response.cookie[str(name)] = cookie.value def multiple(self, names): list(map(self.single, names)) def append_headers(header_list, debug=False): if debug: cherrypy.log( 'Extending response headers with %s' % repr(header_list), 'TOOLS.APPEND_HEADERS') cherrypy.serving.response.header_list.extend(header_list) cherrypy.tools.append_headers = cherrypy.Tool( 'on_end_resource', append_headers) class MultiHeader(Test): def header_list(self): pass header_list = cherrypy.tools.append_headers(header_list=[ (b'WWW-Authenticate', b'Negotiate'), (b'WWW-Authenticate', b'Basic realm="foo"'), ])(header_list) def commas(self): cherrypy.response.headers[ 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' cherrypy.tree.mount(root) def testStatus(self): self.getPage('/status/') self.assertBody('normal') self.assertStatus(200) self.getPage('/status/blank') self.assertBody('') self.assertStatus(200) self.getPage('/status/illegal') self.assertStatus(500) msg = 'Illegal response status from server (781 is out of range).' self.assertErrorPage(500, msg) if not getattr(cherrypy.server, 'using_apache', False): self.getPage('/status/unknown') self.assertBody('funky') self.assertStatus(431) self.getPage('/status/bad') self.assertStatus(500) msg = "Illegal response status from server ('error' is non-numeric)." self.assertErrorPage(500, msg) def test_on_end_resource_status(self): self.getPage('/status/on_end_resource_stage') self.assertBody('[]') self.getPage('/status/on_end_resource_stage') self.assertBody(repr(['200 OK'])) def testSlashes(self): # Test that requests for index methods without a trailing slash # get redirected to the same URI path with a trailing slash. # Make sure GET params are preserved. self.getPage('/redirect?id=3') self.assertStatus(301) self.assertMatchesBody( '' '%s/redirect/[?]id=3' % (self.base(), self.base()) ) if self.prefix(): # Corner case: the "trailing slash" redirect could be tricky if # we're using a virtual root and the URI is "/vroot" (no slash). self.getPage('') self.assertStatus(301) self.assertMatchesBody("%s/" % (self.base(), self.base())) # Test that requests for NON-index methods WITH a trailing slash # get redirected to the same URI path WITHOUT a trailing slash. # Make sure GET params are preserved. self.getPage('/redirect/by_code/?code=307') self.assertStatus(301) self.assertMatchesBody( "" '%s/redirect/by_code[?]code=307' % (self.base(), self.base()) ) # If the trailing_slash tool is off, CP should just continue # as if the slashes were correct. But it needs some help # inside cherrypy.url to form correct output. self.getPage('/url?path_info=page1') self.assertBody('%s/url/page1' % self.base()) self.getPage('/url/leaf/?path_info=page1') self.assertBody('%s/url/page1' % self.base()) def testRedirect(self): self.getPage('/redirect/') self.assertBody('child') self.assertStatus(200) self.getPage('/redirect/by_code?code=300') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(300) self.getPage('/redirect/by_code?code=301') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(301) self.getPage('/redirect/by_code?code=302') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(302) self.getPage('/redirect/by_code?code=303') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(303) self.getPage('/redirect/by_code?code=307') self.assertMatchesBody( r"\2somewhere%20else") self.assertStatus(307) self.getPage('/redirect/nomodify') self.assertBody('') self.assertStatus(304) self.getPage('/redirect/proxy') self.assertBody('') self.assertStatus(305) # HTTPRedirect on error self.getPage('/redirect/error/') self.assertStatus(('302 Found', '303 See Other')) self.assertInBody('/errpage') # Make sure str(HTTPRedirect()) works. self.getPage('/redirect/stringify', protocol='HTTP/1.0') self.assertStatus(200) self.assertBody("(['%s/'], 302)" % self.base()) if cherrypy.server.protocol_version == 'HTTP/1.1': self.getPage('/redirect/stringify', protocol='HTTP/1.1') self.assertStatus(200) self.assertBody("(['%s/'], 303)" % self.base()) # check that #fragments are handled properly # http://skrb.org/ietf/http_errata.html#location-fragments frag = 'foo' self.getPage('/redirect/fragment/%s' % frag) self.assertMatchesBody( r"\2\/some\/url\#%s" % ( frag, frag)) loc = self.assertHeader('Location') assert loc.endswith('#%s' % frag) self.assertStatus(('302 Found', '303 See Other')) # check injection protection # See https://github.com/cherrypy/cherrypy/issues/1003 self.getPage( '/redirect/custom?' 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval') self.assertStatus(303) loc = self.assertHeader('Location') assert 'Set-Cookie' in loc self.assertNoHeader('Set-Cookie') def assertValidXHTML(): from xml.etree import ElementTree try: ElementTree.fromstring( '%s' % self.body, ) except ElementTree.ParseError: self._handlewebError( 'automatically generated redirect did not ' 'generate well-formed html', ) # check redirects to URLs generated valid HTML - we check this # by seeing if it appears as valid XHTML. self.getPage('/redirect/by_code?code=303') self.assertStatus(303) assertValidXHTML() # do the same with a url containing quote characters. self.getPage('/redirect/url_with_quote') self.assertStatus(303) assertValidXHTML() def test_redirect_with_xss(self): """A redirect to a URL with HTML injected should result in page contents escaped.""" self.getPage('/redirect/url_with_xss') self.assertStatus(303) assert b'