Skip to content
This repository was archived by the owner on Jan 12, 2022. It is now read-only.

Commit c2b2f2e

Browse files
committed
static_files and static_dir handling and tests
1 parent cf6a30e commit c2b2f2e

6 files changed

Lines changed: 186 additions & 32 deletions

File tree

multicore_runtime/dispatcher.py

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import re
1919

2020
from werkzeug.wrappers import Request
21+
from werkzeug.wrappers import Response
2122

2223

2324
def dispatcher(handlers):
@@ -32,29 +33,21 @@ def dispatcher(handlers):
3233
A WSGI app that dispatches to the user apps specified in the input.
3334
"""
3435

35-
def dispatch(env, start_response):
36+
def dispatch(wsgi_env, start_response):
3637
"""Handle one request."""
37-
request = Request(env)
38-
for url, script, app in handlers: # Closure over parent func's handlers.
38+
request = Request(wsgi_env)
39+
for url, script, app in handlers: # pylint: disable=unused-variable
3940
matcher = re.match(url, request.path)
4041
if matcher and matcher.end() == len(request.path):
41-
if script:
42-
if app is not None:
43-
# Send a response via the app specified in the handler.
44-
return app(env, start_response)
45-
else:
46-
# The import must have failed. This will have been logged at import
47-
# time. Send a 500 error response.
48-
start_response('500 Internal Server Error', [])
49-
return ['<h1>500 Internal Server Error</h1>\n']
50-
# This is a static file; normally these are handled by the appserver
51-
# but in development or for local queries they may end up here.
52-
# TODO(apphosting): support static files.
42+
if app is not None:
43+
# Send a response via the app specified in the handler.
44+
return app(wsgi_env, start_response)
5345
else:
54-
logging.error('Static file serving is not supported: %s ',
55-
request.path)
56-
start_response('500 Internal Server Error', [])
57-
return ['<h1>500 Internal Server Error</h1>\n']
46+
# The import must have failed. This will have been logged at import
47+
# time. Send a 500 error response.
48+
response = Response('<h1>500 Internal Server Error</h1>\n',
49+
status=500)
50+
return response(wsgi_env, start_response)
5851
logging.error('No handler found for %s', request.path)
5952
start_response('404 Not Found', [])
6053
return ['<h1>404 Not Found</h1>\n']

multicore_runtime/static_files.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2015 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""Static file serving functionality invoked by wsgi_config.py."""
16+
17+
import logging
18+
import mimetypes
19+
import os
20+
import re
21+
22+
from werkzeug.wrappers import Request
23+
from werkzeug.wrappers import Response
24+
from werkzeug.wsgi import wrap_file
25+
26+
27+
def static_app_for_regex_and_files(regex, files, mime_type=None):
28+
"""Returns a WSGI app that serves static files.
29+
30+
Args:
31+
regex: A url-matching regex as specified in appinfo.URLMap.
32+
files: A static_files definition as specified in appinfo.URLMap.
33+
May include a regex backref. See the appinfo.URLMap docstring
34+
for more information.
35+
mime_type: A mime type to apply to all files. If absent,
36+
mimetypes.guess_type() is used.
37+
38+
Returns:
39+
A static file-serving WSGI app closed over the inputs.
40+
"""
41+
@Request.application # Transforms wsgi_env, start_response args into request
42+
def serve_static_files(request):
43+
"""Serve a static file."""
44+
# First, match the path against the regex...
45+
matcher = re.match(regex, request.path)
46+
# ... and use the files regex backref to choose a filename.
47+
filename = matcher.expand(files)
48+
49+
# Rudimentary protection against path traversal outside of the app. This
50+
# code should never be hit in production, as Google's frontend servers
51+
# (GFE) rewrite traversal attempts and respond with a 302 immediately.
52+
filename = os.path.abspath(filename)
53+
if not filename.startswith(os.path.join(os.getcwd(), '')):
54+
logging.warn('Path traversal protection triggered for %s, '
55+
'returning 404', filename)
56+
return Response(status=404)
57+
58+
try:
59+
fp = open(filename, 'rb')
60+
# fp is not closed in this function as it is handed to the WSGI server
61+
# directly.
62+
except IOError:
63+
logging.warn('Requested non-existent filename %s', filename)
64+
return Response(status=404)
65+
66+
wrapped_file = wrap_file(request.environ, fp)
67+
return Response(wrapped_file, direct_passthrough=True,
68+
mimetype=mime_type or mimetypes.guess_type(filename)[0])
69+
return serve_static_files
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
favicon.ico contents

multicore_runtime/wsgi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
import os
2525

2626
from dispatcher import dispatcher
27-
from middleware import reset_environment_middleware
2827
from middleware import health_check_middleware
28+
from middleware import reset_environment_middleware
2929
from wsgi_config import env_vars_from_env_config
3030
from wsgi_config import get_module_config
3131
from wsgi_config import get_module_config_filename
@@ -91,4 +91,4 @@
9191
# Reset os.environ to the frozen state and add request-specific data.
9292
meta_app = reset_environment_middleware(meta_app, frozen_environment,
9393
frozen_user_env,
94-
frozen_env_config_env)
94+
frozen_env_config_env)

multicore_runtime/wsgi_config.py

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import threading
2020
from UserDict import UserDict
2121

22+
from static_files import static_app_for_regex_and_files
23+
2224
from google.appengine.api import appinfo_includes
2325
from google.appengine.runtime import wsgi
2426

@@ -38,14 +40,13 @@ def get_module_config(filename):
3840

3941
def app_for_script(script):
4042
"""Returns the WSGI app specified in the input string, or None on failure."""
41-
if script:
42-
app, filename, err = wsgi.LoadObject(script) # pylint: disable=unused-variable
43-
if err:
44-
# Log the exception but do not reraise.
45-
logging.exception('Failed to import %s: %s', script, err)
46-
return None
47-
else:
48-
return app_wrapped_in_user_middleware(app)
43+
app, filename, err = wsgi.LoadObject(script) # pylint: disable=unused-variable
44+
if err:
45+
# Log the exception but do not reraise.
46+
logging.exception('Failed to import %s: %s', script, err)
47+
return None
48+
else:
49+
return app_wrapped_in_user_middleware(app)
4950

5051

5152
def app_wrapped_in_user_middleware(app):
@@ -80,6 +81,51 @@ def get_add_middleware_from_appengine_config():
8081
return None
8182

8283

84+
def static_app_for_handler(handler):
85+
"""Returns a WSGI app that serves static files as directed by the handler.
86+
87+
Args:
88+
handler: An individual handler from appinfo_external.handlers
89+
(appinfo.URLMap)
90+
91+
Returns:
92+
A static file-serving WSGI app closed over the handler information.
93+
"""
94+
regex = handler.url
95+
files = handler.static_files
96+
if not files:
97+
if handler.static_dir:
98+
# If static_files is not set, convert static_dir to static_files and also
99+
# modify the url regex accordingly. See the appinfo.URLMap docstring for
100+
# more information.
101+
regex = static_dir_url(handler)
102+
files = handler.static_dir + r'/\1'
103+
else:
104+
# Neither static_files nor static_dir is set; log an error and return.
105+
logging.error('No script, static_files or static_dir found for %s',
106+
handler)
107+
return None
108+
return static_app_for_regex_and_files(regex, files,
109+
mime_type=handler.mime_type)
110+
111+
112+
def static_dir_url(handler):
113+
"""Converts a static_dir regex into a static_files regex if needed.
114+
115+
See the appinfo.URLMap docstring for more information.
116+
117+
Args:
118+
handler: A handler (appinfo.URLMap)
119+
120+
Returns:
121+
A modified url regex
122+
"""
123+
if not handler.script and not handler.static_files and handler.static_dir:
124+
return handler.url + '/(.*)'
125+
else:
126+
return handler.url
127+
128+
83129
def load_user_scripts_into_handlers(handlers):
84130
"""Preloads user scripts, wrapped in env_config middleware if present.
85131
@@ -94,9 +140,9 @@ def load_user_scripts_into_handlers(handlers):
94140
- app: The fully loaded app corresponding to the script.
95141
"""
96142
loaded_handlers = [
97-
(x.url,
143+
(x.url if x.script or x.static_files else static_dir_url(x),
98144
x.script.replace('$PYTHON_LIB/', '') if x.script else x.script,
99-
app_for_script(x.script) if x.script else None)
145+
app_for_script(x.script) if x.script else static_app_for_handler(x))
100146
for x in handlers]
101147
logging.info('Parsed handlers: %s',
102148
[(url, script) for (url, script, _) in loaded_handlers])

multicore_runtime/wsgi_test.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@
3434
appinfo.URLMap(url='/failure', script='wsgi_test.nonexistent_function'),
3535
appinfo.URLMap(url='/env', script='wsgi_test.dump_os_environ'),
3636
appinfo.URLMap(url='/setenv', script='wsgi_test.add_to_os_environ'),
37-
appinfo.URLMap(url='/wait', script='wsgi_test.wait_on_global_event')]
37+
appinfo.URLMap(url='/wait', script='wsgi_test.wait_on_global_event'),
38+
appinfo.URLMap(url='/favicon.ico', static_files='test_statics/favicon.ico'),
39+
appinfo.URLMap(url='/faketype.ico', static_files='test_statics/favicon.ico',
40+
mime_type='application/fake_type'),
41+
appinfo.URLMap(url='/wildcard_statics/(.*)',
42+
static_files=r'test_statics/\1'),
43+
appinfo.URLMap(url='/static_dir',
44+
static_dir='test_statics'),
45+
]
3846
HELLO_STRING = 'Hello World!'
3947

4048
FAKE_ENV_KEY = 'KEY'
@@ -118,6 +126,10 @@ def test_notfound(self):
118126
response = self.client.get('/notfound')
119127
self.assertEqual(response.status_code, 404)
120128

129+
def test_health(self):
130+
response = self.client.get('/_ah/health')
131+
self.assertEqual(response.status_code, 200)
132+
121133
# Test PATH is present in env. If this breaks, each request is properly
122134
# wiping the environment but not properly reconstituting the frozen initial
123135
# state.
@@ -126,6 +138,39 @@ def test_basic_env(self):
126138
# Assumes PATH will be present in the env in all cases, including tests!
127139
self.assertIn('PATH', json.loads(response.data))
128140

141+
def test_static_file(self):
142+
response = self.client.get('/favicon.ico')
143+
self.assertEqual(response.status_code, 200)
144+
with open('test_statics/favicon.ico') as f:
145+
self.assertEqual(response.data, f.read())
146+
147+
def test_static_file_mime_type(self):
148+
response = self.client.get('/faketype.ico')
149+
self.assertEqual(response.status_code, 200)
150+
with open('test_statics/favicon.ico') as f:
151+
self.assertEqual(response.data, f.read())
152+
self.assertEqual(response.mimetype, 'application/fake_type')
153+
154+
def test_static_file_wildcard(self):
155+
response = self.client.get('/wildcard_statics/favicon.ico')
156+
self.assertEqual(response.status_code, 200)
157+
with open('test_statics/favicon.ico') as f:
158+
self.assertEqual(response.data, f.read())
159+
160+
def test_static_file_wildcard_404(self):
161+
response = self.client.get('/wildcard_statics/no_file')
162+
self.assertEqual(response.status_code, 404)
163+
164+
def test_static_file_wildcard_directory_traversal(self):
165+
response = self.client.get('/wildcard_statics/../../setup.py')
166+
self.assertEqual(response.status_code, 404)
167+
168+
def test_static_dir(self):
169+
response = self.client.get('/static_dir/favicon.ico')
170+
self.assertEqual(response.status_code, 200)
171+
with open('test_statics/favicon.ico') as f:
172+
self.assertEqual(response.data, f.read())
173+
129174
def test_wsgi_vars_in_env(self):
130175
response = self.client.get('/env')
131176
env = json.loads(response.data)

0 commit comments

Comments
 (0)