Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774b7f7682 | ||
|
|
16bf25ffaa | ||
|
|
179da5895f | ||
|
|
aeed530e32 | ||
|
|
63268b3616 | ||
|
|
07a1952f28 | ||
|
|
fdae354e3e | ||
|
|
d5f67fd9e2 | ||
|
|
e04483bb90 | ||
|
|
4927ce2590 | ||
|
|
ecd9d1b3d9 | ||
|
|
5cef2a05e1 |
11 changed files with 111 additions and 8 deletions
16
CHANGES
16
CHANGES
|
|
@ -3,6 +3,22 @@ Flask Changelog
|
|||
|
||||
Here you can see the full list of changes between each Flask release.
|
||||
|
||||
Version 0.6.1
|
||||
-------------
|
||||
|
||||
Bugfix release, released on December 31st 2010
|
||||
|
||||
- Fixed an issue where the default `OPTIONS` response was
|
||||
not exposing all valid methods in the `Allow` header.
|
||||
- Jinja2 template loading syntax now allows "./" in front of
|
||||
a template load path. Previously this caused issues with
|
||||
module setups.
|
||||
- Fixed an issue where the subdomain setting for modules was
|
||||
ignored for the static folder.
|
||||
- Fixed a security problem that allowed clients to download arbitrary files
|
||||
if the host server was a windows based operating system and the client
|
||||
uses backslashes to escape the directory the files where exposed from.
|
||||
|
||||
Version 0.6
|
||||
-----------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
include Makefile CHANGES LICENSE AUTHORS
|
||||
recursive-include artwork *
|
||||
recursive-include tests *
|
||||
recursive-include examples *
|
||||
recursive-include docs *
|
||||
|
|
|
|||
23
flask/app.py
23
flask/app.py
|
|
@ -19,7 +19,8 @@ from jinja2 import Environment
|
|||
|
||||
from werkzeug import ImmutableDict
|
||||
from werkzeug.routing import Map, Rule
|
||||
from werkzeug.exceptions import HTTPException, InternalServerError
|
||||
from werkzeug.exceptions import HTTPException, InternalServerError, \
|
||||
MethodNotAllowed
|
||||
|
||||
from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
|
||||
_tojson_filter, _endpoint_from_view_func
|
||||
|
|
@ -689,14 +690,28 @@ class Flask(_PackageBoundObject):
|
|||
# if we provide automatic options for this URL and the
|
||||
# request came with the OPTIONS method, reply automatically
|
||||
if rule.provide_automatic_options and req.method == 'OPTIONS':
|
||||
rv = self.response_class()
|
||||
rv.allow.update(rule.methods)
|
||||
return rv
|
||||
return self._make_default_options_response()
|
||||
# otherwise dispatch to the handler for that endpoint
|
||||
return self.view_functions[rule.endpoint](**req.view_args)
|
||||
except HTTPException, e:
|
||||
return self.handle_http_exception(e)
|
||||
|
||||
def _make_default_options_response(self):
|
||||
# This would be nicer in Werkzeug 0.7, which however currently
|
||||
# is not released. Werkzeug 0.7 provides a method called
|
||||
# allowed_methods() that returns all methods that are valid for
|
||||
# a given path.
|
||||
methods = []
|
||||
try:
|
||||
_request_ctx_stack.top.url_adapter.match(method='--')
|
||||
except MethodNotAllowed, e:
|
||||
methods = e.valid_methods
|
||||
except HTTPException, e:
|
||||
pass
|
||||
rv = self.response_class()
|
||||
rv.allow.update(methods)
|
||||
return rv
|
||||
|
||||
def make_response(self, rv):
|
||||
"""Converts the return value from a view function to a real
|
||||
response object that is an instance of :attr:`response_class`.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ else:
|
|||
_tojson_filter = json.dumps
|
||||
|
||||
|
||||
# what separators does this operating system provide that are not a slash?
|
||||
# this is used by the send_from_directory function to ensure that nobody is
|
||||
# able to access files from outside the filesystem.
|
||||
_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep]
|
||||
if sep not in (None, '/'))
|
||||
|
||||
|
||||
def _endpoint_from_view_func(view_func):
|
||||
"""Internal helper that returns the default endpoint for a given
|
||||
function. This always is the function name.
|
||||
|
|
@ -386,7 +393,10 @@ def send_from_directory(directory, filename, **options):
|
|||
forwarded to :func:`send_file`.
|
||||
"""
|
||||
filename = posixpath.normpath(filename)
|
||||
if filename.startswith(('/', '../')):
|
||||
for sep in _os_alt_seps:
|
||||
if sep in filename:
|
||||
raise NotFound()
|
||||
if os.path.isabs(filename) or filename.startswith('../'):
|
||||
raise NotFound()
|
||||
filename = os.path.join(directory, filename)
|
||||
if not os.path.isfile(filename):
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ def _register_module(module, static_path):
|
|||
path = state.url_prefix + path
|
||||
state.app.add_url_rule(path + '/<path:filename>',
|
||||
endpoint='%s.static' % module.name,
|
||||
view_func=module.send_static_file)
|
||||
view_func=module.send_static_file,
|
||||
subdomain=module.subdomain)
|
||||
return _register
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
:copyright: (c) 2010 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import posixpath
|
||||
from jinja2 import BaseLoader, TemplateNotFound
|
||||
|
||||
from .globals import _request_ctx_stack
|
||||
|
|
@ -36,6 +37,9 @@ class _DispatchingJinjaLoader(BaseLoader):
|
|||
self.app = app
|
||||
|
||||
def get_source(self, environment, template):
|
||||
template = posixpath.normpath(template)
|
||||
if template.startswith('../'):
|
||||
raise TemplateNotFound(template)
|
||||
loader = None
|
||||
try:
|
||||
module, name = template.split('/', 1)
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -50,7 +50,7 @@ def run_tests():
|
|||
|
||||
setup(
|
||||
name='Flask',
|
||||
version='0.6',
|
||||
version='0.6.1',
|
||||
url='http://github.com/mitsuhiko/flask/',
|
||||
license='BSD',
|
||||
author='Armin Ronacher',
|
||||
|
|
|
|||
|
|
@ -120,6 +120,17 @@ class BasicFunctionalityTestCase(unittest.TestCase):
|
|||
assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST']
|
||||
assert rv.data == ''
|
||||
|
||||
def test_options_on_multiple_rules(self):
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
return 'Hello World'
|
||||
@app.route('/', methods=['PUT'])
|
||||
def index_put():
|
||||
return 'Aha!'
|
||||
rv = app.test_client().open('/', method='OPTIONS')
|
||||
assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
|
||||
|
||||
def test_request_dispatching(self):
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/')
|
||||
|
|
@ -219,7 +230,13 @@ class BasicFunctionalityTestCase(unittest.TestCase):
|
|||
flask.session['test'] = 42
|
||||
flask.session.permanent = permanent
|
||||
return ''
|
||||
rv = app.test_client().get('/')
|
||||
|
||||
@app.route('/test')
|
||||
def test():
|
||||
return unicode(flask.session.permanent)
|
||||
|
||||
client = app.test_client()
|
||||
rv = client.get('/')
|
||||
assert 'set-cookie' in rv.headers
|
||||
match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie'])
|
||||
expires = parse_date(match.group())
|
||||
|
|
@ -228,6 +245,9 @@ class BasicFunctionalityTestCase(unittest.TestCase):
|
|||
assert expires.month == expected.month
|
||||
assert expires.day == expected.day
|
||||
|
||||
rv = client.get('/test')
|
||||
assert rv.data == 'True'
|
||||
|
||||
permanent = False
|
||||
rv = app.test_client().get('/')
|
||||
assert 'set-cookie' in rv.headers
|
||||
|
|
@ -756,6 +776,8 @@ class ModuleTestCase(unittest.TestCase):
|
|||
assert rv.data == 'Hello from the Frontend'
|
||||
rv = c.get('/admin/')
|
||||
assert rv.data == 'Hello from the Admin'
|
||||
rv = c.get('/admin/index2')
|
||||
assert rv.data == 'Hello from the Admin'
|
||||
rv = c.get('/admin/static/test.txt')
|
||||
assert rv.data.strip() == 'Admin File'
|
||||
rv = c.get('/admin/static/css/test.css')
|
||||
|
|
@ -795,6 +817,21 @@ class ModuleTestCase(unittest.TestCase):
|
|||
else:
|
||||
assert 0, 'expected exception'
|
||||
|
||||
# testcase for a security issue that may exist on windows systems
|
||||
import os
|
||||
import ntpath
|
||||
old_path = os.path
|
||||
os.path = ntpath
|
||||
try:
|
||||
try:
|
||||
f('..\\__init__.py')
|
||||
except NotFound:
|
||||
pass
|
||||
else:
|
||||
assert 0, 'expected exception'
|
||||
finally:
|
||||
os.path = old_path
|
||||
|
||||
|
||||
class SendfileTestCase(unittest.TestCase):
|
||||
|
||||
|
|
@ -1027,6 +1064,15 @@ class SubdomainTestCase(unittest.TestCase):
|
|||
rv = c.get('/', 'http://test.localhost/')
|
||||
assert rv.data == 'test index'
|
||||
|
||||
def test_module_static_path_subdomain(self):
|
||||
app = flask.Flask(__name__)
|
||||
app.config['SERVER_NAME'] = 'example.com'
|
||||
from subdomaintestmodule import mod
|
||||
app.register_module(mod)
|
||||
c = app.test_client()
|
||||
rv = c.get('/static/hello.txt', 'http://foo.example.com/')
|
||||
assert rv.data.strip() == 'Hello Subdomain'
|
||||
|
||||
def test_subdomain_matching(self):
|
||||
app = flask.Flask(__name__)
|
||||
app.config['SERVER_NAME'] = 'localhost'
|
||||
|
|
|
|||
|
|
@ -7,3 +7,8 @@ admin = Module(__name__, url_prefix='/admin')
|
|||
@admin.route('/')
|
||||
def index():
|
||||
return render_template('admin/index.html')
|
||||
|
||||
|
||||
@admin.route('/index2')
|
||||
def index2():
|
||||
return render_template('./admin/index.html')
|
||||
|
|
|
|||
4
tests/subdomaintestmodule/__init__.py
Normal file
4
tests/subdomaintestmodule/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from flask import Module
|
||||
|
||||
|
||||
mod = Module(__name__, 'foo', subdomain='foo')
|
||||
1
tests/subdomaintestmodule/static/hello.txt
Normal file
1
tests/subdomaintestmodule/static/hello.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Hello Subdomain
|
||||
Loading…
Add table
Add a link
Reference in a new issue