Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2012 The LUCI Authors. All rights reserved. | 1 # Copyright 2012 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 """This module defines Isolate Server frontend url handlers.""" | 5 """This module defines Isolate Server frontend url handlers.""" |
| 6 | 6 |
| 7 import cgi | 7 import cgi |
| 8 import datetime | 8 import datetime |
| 9 import json | 9 import json |
| 10 import logging | 10 import logging |
| 11 import os | |
| 11 import re | 12 import re |
| 13 import urllib | |
| 12 | 14 |
| 13 import webapp2 | 15 import webapp2 |
| 14 | 16 |
| 15 from google.appengine.api import memcache | 17 from google.appengine.api import memcache |
| 16 from google.appengine.api import modules | 18 from google.appengine.api import modules |
| 17 from google.appengine.api import users | 19 from google.appengine.api import users |
| 18 | 20 |
| 19 import acl | 21 import acl |
| 20 import config | 22 import config |
| 21 import gcs | 23 import gcs |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 51 'other_requests', | 53 'other_requests', |
| 52 'failures', | 54 'failures', |
| 53 'uploads', | 55 'uploads', |
| 54 'downloads', | 56 'downloads', |
| 55 'contains_requests', | 57 'contains_requests', |
| 56 'uploads_bytes', | 58 'uploads_bytes', |
| 57 'downloads_bytes', | 59 'downloads_bytes', |
| 58 'contains_lookups', | 60 'contains_lookups', |
| 59 ) | 61 ) |
| 60 | 62 |
| 63 _ISOLATED_ROOT_MEMBERS = ( | |
| 64 'algo', | |
| 65 'command', | |
| 66 'files', | |
| 67 'includes', | |
| 68 'read_only', | |
| 69 'relative_cwd', | |
| 70 'version', | |
| 71 ) | |
| 72 | |
| 61 | 73 |
| 62 ### Restricted handlers | 74 ### Restricted handlers |
| 63 | 75 |
| 64 | 76 |
| 65 class RestrictedConfigHandler(auth.AuthenticatingHandler): | 77 class RestrictedConfigHandler(auth.AuthenticatingHandler): |
| 66 @auth.autologin | 78 @auth.autologin |
| 67 @auth.require(auth.is_admin) | 79 @auth.require(auth.is_admin) |
| 68 def get(self): | 80 def get(self): |
| 69 self.common(None) | 81 self.common(None) |
| 70 | 82 |
| (...skipping 113 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 184 ### Non-restricted handlers | 196 ### Non-restricted handlers |
| 185 | 197 |
| 186 | 198 |
| 187 class BrowseHandler(auth.AuthenticatingHandler): | 199 class BrowseHandler(auth.AuthenticatingHandler): |
| 188 @auth.autologin | 200 @auth.autologin |
| 189 @auth.require(acl.isolate_readable) | 201 @auth.require(acl.isolate_readable) |
| 190 def get(self): | 202 def get(self): |
| 191 namespace = self.request.get('namespace', 'default-gzip') | 203 namespace = self.request.get('namespace', 'default-gzip') |
| 192 # Support 'hash' for compatibility with old links. To remove eventually. | 204 # Support 'hash' for compatibility with old links. To remove eventually. |
| 193 digest = self.request.get('digest', '') or self.request.get('hash', '') | 205 digest = self.request.get('digest', '') or self.request.get('hash', '') |
| 206 save_as = self.request.get('as', '') | |
| 194 params = { | 207 params = { |
| 208 u'as': unicode(save_as), | |
| 195 u'digest': unicode(digest), | 209 u'digest': unicode(digest), |
| 196 u'namespace': unicode(namespace), | 210 u'namespace': unicode(namespace), |
| 197 } | 211 } |
| 198 # Check for existence of element, so we can 400/404 | 212 # Check for existence of element, so we can 400/404 |
| 199 if digest and namespace: | 213 if digest and namespace: |
| 200 try: | 214 try: |
| 201 model.get_content(namespace, digest) | 215 model.get_content(namespace, digest) |
| 202 except ValueError: | 216 except ValueError: |
| 203 self.abort(400, 'Invalid key') | 217 self.abort(400, 'Invalid key') |
| 204 except LookupError: | 218 except LookupError: |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 251 if len(content) > 33554000: | 265 if len(content) > 33554000: |
| 252 host = modules.get_hostname(module='default', version='default') | 266 host = modules.get_hostname(module='default', version='default') |
| 253 # host is something like default.default.myisolateserver.appspot.com | 267 # host is something like default.default.myisolateserver.appspot.com |
| 254 host = host.replace('default.default.','') | 268 host = host.replace('default.default.','') |
| 255 sizeInMib = len(content) / (1024.0 * 1024.0) | 269 sizeInMib = len(content) / (1024.0 * 1024.0) |
| 256 content = ('Sorry, your file is %1.1f MiB big, which exceeds the 32 MiB' | 270 content = ('Sorry, your file is %1.1f MiB big, which exceeds the 32 MiB' |
| 257 ' App Engine limit.\nTo work around this, run the following command:\n' | 271 ' App Engine limit.\nTo work around this, run the following command:\n' |
| 258 ' python isolateserver.py download -I %s --namespace %s -f %s %s' | 272 ' python isolateserver.py download -I %s --namespace %s -f %s %s' |
| 259 % (sizeInMib, host, namespace, digest, digest)) | 273 % (sizeInMib, host, namespace, digest, digest)) |
| 260 else: | 274 else: |
| 261 self.response.headers['Content-Disposition'] = str('filename=%s' | 275 self.response.headers['Content-Disposition'] = str( |
| 262 % digest) | 276 'filename=%s' % self.request.get('as') or digest) |
| 263 if content.startswith('{'): | 277 try: |
| 264 # Try to format as JSON. | 278 json_data = json.loads(content) |
| 265 try: | 279 if self._is_isolated_format(json_data): |
| 266 content = json.dumps( | 280 content = self._format_isolated_content(json_data, namespace) |
| 267 json.loads(content), sort_keys=True, indent=2, | |
| 268 separators=(',', ': ')) | |
| 269 content = cgi.escape(content) | |
| 270 # If we don't wrap this in html, browsers will put content in a pre | |
| 271 # tag which is also styled with monospace/pre-wrap. We can't use | |
| 272 # anchor tags in <pre>, so we force it to be a <div>, which happily | |
| 273 # accepts links. | |
| 274 content = ( | |
| 275 '<div style="font-family:monospace;white-space:pre-wrap;">%s' | |
| 276 '</div>' % content) | |
| 277 # Linkify things that look like hashes | |
| 278 content = re.sub(r'([0-9a-f]{40})', | |
| 279 r'<a target="_blank" href="/browse?namespace=%s' % namespace + | |
| 280 r'&digest=\1">\1</a>', | |
| 281 content) | |
| 282 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' | 281 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' |
| 283 except ValueError: | 282 except ValueError: |
| 284 pass | 283 pass |
| 285 | 284 |
| 286 self.response.write(content) | 285 self.response.write(content) |
| 287 | 286 |
| 287 @staticmethod | |
| 288 def _is_isolated_format(json_data): | |
| 289 """Checks if json_data is a valid .isolated format.""" | |
| 290 if isinstance(json_data, dict): | |
|
M-A Ruel
2017/02/16 21:29:12
for "checker" functions, use early return instead;
jonesmi
2017/02/22 21:43:18
Done.
| |
| 291 actual = set(json_data) | |
| 292 return actual.issubset(_ISOLATED_ROOT_MEMBERS) and 'files' in actual | |
| 293 return False | |
| 294 | |
| 295 @staticmethod | |
|
M-A Ruel
2017/02/16 21:29:12
@classmethod
def _escape_all_dict_strings(cls, d):
mithro
2017/02/16 23:21:35
Guide from security - DON'T TRY AND ESCAPE STUFF Y
jonesmi
2017/02/22 21:43:18
Looks like we've already configured our jinja2 to
| |
| 296 def _escape_all_dict_strings(d): | |
| 297 """Recursively modify every str val in json dict to be cgi.escape()'d.""" | |
| 298 for k, v in d.iteritems(): | |
| 299 if isinstance(v, str): | |
|
M-A Ruel
2017/02/16 21:29:12
basestring
jonesmi
2017/02/22 21:43:18
Ack, however getting rid of this method in favor o
| |
| 300 d[k] = cgi.escape(v) | |
| 301 elif isinstance(v, dict): | |
|
M-A Ruel
2017/02/16 21:29:12
you need to iterate in lists too; so the function
jonesmi
2017/02/22 21:43:18
Ack, however getting rid of this method in favor o
| |
| 302 d[k] = ContentHandler._escape_all_dict_strings(v) | |
|
M-A Ruel
2017/02/16 21:29:12
d[k] = cls._escape_all_dict_strings(v)
jonesmi
2017/02/22 21:43:18
Ack, however getting rid of this method in favor o
| |
| 303 return d | |
| 304 | |
| 305 @staticmethod | |
|
M-A Ruel
2017/02/16 21:29:12
classmethod
jonesmi
2017/02/22 21:43:18
Done.
| |
| 306 def _format_isolated_content(json_data, namespace): | |
| 307 """Formats .isolated content and returns a string representation of it.""" | |
| 308 # Ensure we're working with HTML-safe content. Do this before adding | |
| 309 # our own hyperlinks because cgi.escape would replace our anchor symbols. | |
| 310 json_data = ContentHandler._escape_all_dict_strings(json_data) | |
| 311 | |
| 312 # Linkify all files | |
| 313 if 'files' in json_data: | |
|
M-A Ruel
2017/02/16 21:29:12
check is not needed anymore.
jonesmi
2017/02/22 21:43:18
Done.
| |
| 314 hyperlinked_files = {} | |
| 315 for filepath, metadata in json_data['files'].iteritems(): | |
| 316 if metadata.get('h'): | |
| 317 save_as = os.path.basename(filepath) | |
| 318 anchor = (r'<a target=_blank" ' | |
| 319 r'href=/browse?namespace=%s&digest=%s&as=%s>' | |
| 320 r'%s</a>') % ( | |
| 321 namespace, metadata['h'], urllib.quote(save_as), filepath) | |
| 322 hyperlinked_files[anchor] = metadata | |
| 323 json_data['files'] = hyperlinked_files | |
| 324 | |
| 325 content = json.dumps( | |
| 326 json_data, sort_keys=True, indent=2, separators=(',', ': ')) | |
| 327 # If we don't wrap this in html, browsers will put content in a pre | |
| 328 # tag which is also styled with monospace/pre-wrap. We can't use | |
| 329 # anchor tags in <pre>, so we force it to be a <div>, which happily | |
| 330 # accepts links. | |
| 331 content = ( | |
| 332 '<div style="font-family:monospace;white-space:pre-wrap;">%s' | |
| 333 '</div>' % content) | |
| 334 return content | |
| 335 | |
| 288 | 336 |
| 289 class StatsHandler(webapp2.RequestHandler): | 337 class StatsHandler(webapp2.RequestHandler): |
| 290 """Returns the statistics web page.""" | 338 """Returns the statistics web page.""" |
| 291 def get(self): | 339 def get(self): |
| 292 """Presents nice recent statistics. | 340 """Presents nice recent statistics. |
| 293 | 341 |
| 294 It fetches data from the 'JSON' API. | 342 It fetches data from the 'JSON' API. |
| 295 """ | 343 """ |
| 296 # Preloads the data to save a complete request. | 344 # Preloads the data to save a complete request. |
| 297 resolution = self.request.params.get('resolution', 'hours') | 345 resolution = self.request.params.get('resolution', 'hours') |
| (...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 412 def create_application(debug): | 460 def create_application(debug): |
| 413 """Creates the url router. | 461 """Creates the url router. |
| 414 | 462 |
| 415 The basic layouts is as follow: | 463 The basic layouts is as follow: |
| 416 - /restricted/.* requires being an instance administrator. | 464 - /restricted/.* requires being an instance administrator. |
| 417 - /stats/.* has statistics. | 465 - /stats/.* has statistics. |
| 418 """ | 466 """ |
| 419 acl.bootstrap() | 467 acl.bootstrap() |
| 420 template.bootstrap() | 468 template.bootstrap() |
| 421 return webapp2.WSGIApplication(get_routes(), debug=debug) | 469 return webapp2.WSGIApplication(get_routes(), debug=debug) |
| OLD | NEW |