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' | |
|
M-A Ruel
2017/02/14 23:46:14
include trailing comma, so when a new line is adde
jonesmi
2017/02/16 19:17:30
Done.
| |
| 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 if self._is_isolated_format(content): |
| 264 # Try to format as JSON. | 278 content = self._format_isolated_content(content, namespace) |
| 265 try: | 279 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' |
| 266 content = json.dumps( | |
| 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' | |
| 283 except ValueError: | |
| 284 pass | |
| 285 | 280 |
| 286 self.response.write(content) | 281 self.response.write(content) |
| 287 | 282 |
| 283 @staticmethod | |
| 284 def _is_isolated_format(content): | |
| 285 """Checks if content string is a valid .isolated format.""" | |
| 286 try: | |
| 287 data = json.loads(content) | |
| 288 except ValueError: | |
| 289 return False | |
| 290 if isinstance(data, dict): | |
| 291 actual = set(data) | |
| 292 if actual.issubset(_ISOLATED_ROOT_MEMBERS) and 'files' in actual: | |
|
M-A Ruel
2017/02/14 23:46:14
return actual.issubset(_ISOLATED_ROOT_MEMBERS) and
jonesmi
2017/02/16 19:17:30
Done.
| |
| 293 return True | |
| 294 return False | |
| 295 | |
| 296 @staticmethod | |
| 297 def _format_isolated_content(content, namespace): | |
|
mithro
2017/02/16 00:50:52
I feel like just giving the isolate json to a temp
| |
| 298 """Formats .isolated content and returns a string representation of it.""" | |
| 299 try: | |
| 300 # Ensure we're working with HTML-safe content. Do this before adding | |
| 301 # our own hyperlinks because cgi.escape would replace our anchor symbols. | |
| 302 content = cgi.escape(content) | |
| 303 data = json.loads(content) | |
| 304 except ValueError: | |
| 305 return content | |
| 306 | |
| 307 # Linkify all files | |
| 308 if 'files' in data: | |
| 309 hyperlinked_files = {} | |
| 310 for filepath, metadata in data['files'].iteritems(): | |
| 311 if 'h' in metadata: # Only linkify files that have a digest property | |
|
M-A Ruel
2017/02/14 23:46:14
if metadata.get('h'):
is safer. no need for the co
jonesmi
2017/02/16 19:17:30
Done
| |
| 312 save_as = os.path.basename(filepath) | |
| 313 anchor = (r'<a target=_blank" ' | |
| 314 r'href=/browse?namespace=%s&digest=%s&as=%s>' | |
| 315 r'%s</a>') % ( | |
| 316 namespace, metadata['h'], urllib.quote(save_as), filepath) | |
| 317 hyperlinked_files[anchor] = metadata | |
| 318 data['files'] = hyperlinked_files | |
| 319 | |
| 320 content = json.dumps(data, sort_keys=True, indent=2, separators=(',', ': ')) | |
| 321 # If we don't wrap this in html, browsers will put content in a pre | |
| 322 # tag which is also styled with monospace/pre-wrap. We can't use | |
| 323 # anchor tags in <pre>, so we force it to be a <div>, which happily | |
| 324 # accepts links. | |
| 325 content = ( | |
| 326 '<div style="font-family:monospace;white-space:pre-wrap;">%s' | |
| 327 '</div>' % content) | |
| 328 return content | |
| 329 | |
| 288 | 330 |
| 289 class StatsHandler(webapp2.RequestHandler): | 331 class StatsHandler(webapp2.RequestHandler): |
| 290 """Returns the statistics web page.""" | 332 """Returns the statistics web page.""" |
| 291 def get(self): | 333 def get(self): |
| 292 """Presents nice recent statistics. | 334 """Presents nice recent statistics. |
| 293 | 335 |
| 294 It fetches data from the 'JSON' API. | 336 It fetches data from the 'JSON' API. |
| 295 """ | 337 """ |
| 296 # Preloads the data to save a complete request. | 338 # Preloads the data to save a complete request. |
| 297 resolution = self.request.params.get('resolution', 'hours') | 339 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): | 454 def create_application(debug): |
| 413 """Creates the url router. | 455 """Creates the url router. |
| 414 | 456 |
| 415 The basic layouts is as follow: | 457 The basic layouts is as follow: |
| 416 - /restricted/.* requires being an instance administrator. | 458 - /restricted/.* requires being an instance administrator. |
| 417 - /stats/.* has statistics. | 459 - /stats/.* has statistics. |
| 418 """ | 460 """ |
| 419 acl.bootstrap() | 461 acl.bootstrap() |
| 420 template.bootstrap() | 462 template.bootstrap() |
| 421 return webapp2.WSGIApplication(get_routes(), debug=debug) | 463 return webapp2.WSGIApplication(get_routes(), debug=debug) |
| OLD | NEW |