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 |
| 12 | 13 |
| 13 import webapp2 | 14 import webapp2 |
| 14 | 15 |
| 15 from google.appengine.api import memcache | 16 from google.appengine.api import memcache |
| 16 from google.appengine.api import modules | 17 from google.appengine.api import modules |
| 17 from google.appengine.api import users | 18 from google.appengine.api import users |
| 18 | 19 |
| 19 import acl | 20 import acl |
| 20 import config | 21 import config |
| (...skipping 163 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 184 ### Non-restricted handlers | 185 ### Non-restricted handlers |
| 185 | 186 |
| 186 | 187 |
| 187 class BrowseHandler(auth.AuthenticatingHandler): | 188 class BrowseHandler(auth.AuthenticatingHandler): |
| 188 @auth.autologin | 189 @auth.autologin |
| 189 @auth.require(acl.isolate_readable) | 190 @auth.require(acl.isolate_readable) |
| 190 def get(self): | 191 def get(self): |
| 191 namespace = self.request.get('namespace', 'default-gzip') | 192 namespace = self.request.get('namespace', 'default-gzip') |
| 192 # Support 'hash' for compatibility with old links. To remove eventually. | 193 # Support 'hash' for compatibility with old links. To remove eventually. |
| 193 digest = self.request.get('digest', '') or self.request.get('hash', '') | 194 digest = self.request.get('digest', '') or self.request.get('hash', '') |
| 195 save_as = self.request.get('as', '') | |
| 194 params = { | 196 params = { |
| 195 u'digest': unicode(digest), | 197 u'digest': unicode(digest), |
| 196 u'namespace': unicode(namespace), | 198 u'namespace': unicode(namespace), |
| 199 u'as': unicode(save_as), | |
|
M-A Ruel
2017/02/14 21:01:16
keep keys sorted
jonesmi
2017/02/14 23:01:18
Done.
| |
| 197 } | 200 } |
| 198 # Check for existence of element, so we can 400/404 | 201 # Check for existence of element, so we can 400/404 |
| 199 if digest and namespace: | 202 if digest and namespace: |
| 200 try: | 203 try: |
| 201 model.get_content(namespace, digest) | 204 model.get_content(namespace, digest) |
| 202 except ValueError: | 205 except ValueError: |
| 203 self.abort(400, 'Invalid key') | 206 self.abort(400, 'Invalid key') |
| 204 except LookupError: | 207 except LookupError: |
| 205 self.abort(404, 'Unable to retrieve the entry') | 208 self.abort(404, 'Unable to retrieve the entry') |
| 206 self.response.write(template.render('isolate/browse.html', params)) | 209 self.response.write(template.render('isolate/browse.html', params)) |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 251 if len(content) > 33554000: | 254 if len(content) > 33554000: |
| 252 host = modules.get_hostname(module='default', version='default') | 255 host = modules.get_hostname(module='default', version='default') |
| 253 # host is something like default.default.myisolateserver.appspot.com | 256 # host is something like default.default.myisolateserver.appspot.com |
| 254 host = host.replace('default.default.','') | 257 host = host.replace('default.default.','') |
| 255 sizeInMib = len(content) / (1024.0 * 1024.0) | 258 sizeInMib = len(content) / (1024.0 * 1024.0) |
| 256 content = ('Sorry, your file is %1.1f MiB big, which exceeds the 32 MiB' | 259 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' | 260 ' App Engine limit.\nTo work around this, run the following command:\n' |
| 258 ' python isolateserver.py download -I %s --namespace %s -f %s %s' | 261 ' python isolateserver.py download -I %s --namespace %s -f %s %s' |
| 259 % (sizeInMib, host, namespace, digest, digest)) | 262 % (sizeInMib, host, namespace, digest, digest)) |
| 260 else: | 263 else: |
| 261 self.response.headers['Content-Disposition'] = str('filename=%s' | 264 self.response.headers['Content-Disposition'] = str( |
| 262 % digest) | 265 'filename=%s' % self.request.get('as') or digest) |
| 263 if content.startswith('{'): | 266 if content.startswith('{'): |
| 264 # Try to format as JSON. | 267 # Try to format as JSON. |
| 265 try: | 268 content = self._format_content_as_json(content, namespace) |
| 266 content = json.dumps( | 269 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' |
| 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 | 270 |
| 286 self.response.write(content) | 271 self.response.write(content) |
| 287 | 272 |
| 273 @staticmethod | |
| 274 def _format_content_as_json(content, namespace): | |
| 275 """Formats the content as json, and returns a string repr of the content.""" | |
| 276 try: | |
| 277 # Ensure we're working with HTML-safe content. Do this before adding | |
| 278 # our own hyperlinks because cgi.escape would replace our anchor symbols. | |
| 279 content = cgi.escape(content) | |
|
M-A Ruel
2017/02/14 21:01:16
$ python -c 'import cgi,json; print json.loads(cgi
jonesmi
2017/02/14 23:01:18
Sounds good. Now doing the checks in a static meth
M-A Ruel
2017/02/14 23:46:14
There is still no guarantee that someone uploaded
jonesmi
2017/02/16 19:17:30
Ahh I see. Nice catch, thanks! We have to do cgi.e
| |
| 280 data = json.loads(content) | |
| 281 except ValueError: | |
| 282 return content | |
| 283 | |
| 284 # Linkify all files | |
| 285 if 'files' in data: | |
|
M-A Ruel
2017/02/14 21:01:16
Let's add some more checks.
Ref: https://github.co
jonesmi
2017/02/14 23:01:18
Done, although it seems superfluous to check actua
| |
| 286 hyperlinked_files = {} | |
| 287 for filepath, metadata in data['files'].iteritems(): | |
| 288 if 'h' in metadata: # Only linkify files that have a digest property | |
| 289 save_as = os.path.basename(filepath) | |
| 290 anchor = (r'<a target="_blank" ' | |
|
M-A Ruel
2017/02/14 21:01:16
remove the quotes
jonesmi
2017/02/14 23:01:18
Done.
| |
| 291 r'href="/browse?namespace=%s&digest=%s&as=%s">' | |
|
M-A Ruel
2017/02/14 21:01:16
remove the quotes, and use urllib.quote(save_as) s
jonesmi
2017/02/14 23:01:18
Done.
| |
| 292 r'%s</a>') % (namespace, metadata['h'], save_as, filepath) | |
| 293 hyperlinked_files[anchor] = metadata | |
| 294 data['files'] = hyperlinked_files | |
| 295 | |
| 296 content = json.dumps(data, sort_keys=True, indent=2, separators=(',', ': ')) | |
| 297 # The string representation from json.dumps force-escapes all quotes, | |
|
M-A Ruel
2017/02/14 21:01:16
This is not needed anymore, as there's no quotes w
jonesmi
2017/02/14 23:01:18
Done.
| |
| 298 # which would break our href strings. Replace these with regular quotes. | |
| 299 content = content.replace(r'\"', '"') | |
| 300 | |
| 301 # If we don't wrap this in html, browsers will put content in a pre | |
| 302 # tag which is also styled with monospace/pre-wrap. We can't use | |
| 303 # anchor tags in <pre>, so we force it to be a <div>, which happily | |
| 304 # accepts links. | |
| 305 content = ( | |
| 306 '<div style="font-family:monospace;white-space:pre-wrap;">%s' | |
| 307 '</div>' % content) | |
| 308 return content | |
| 309 | |
| 288 | 310 |
| 289 class StatsHandler(webapp2.RequestHandler): | 311 class StatsHandler(webapp2.RequestHandler): |
| 290 """Returns the statistics web page.""" | 312 """Returns the statistics web page.""" |
| 291 def get(self): | 313 def get(self): |
| 292 """Presents nice recent statistics. | 314 """Presents nice recent statistics. |
| 293 | 315 |
| 294 It fetches data from the 'JSON' API. | 316 It fetches data from the 'JSON' API. |
| 295 """ | 317 """ |
| 296 # Preloads the data to save a complete request. | 318 # Preloads the data to save a complete request. |
| 297 resolution = self.request.params.get('resolution', 'hours') | 319 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): | 434 def create_application(debug): |
| 413 """Creates the url router. | 435 """Creates the url router. |
| 414 | 436 |
| 415 The basic layouts is as follow: | 437 The basic layouts is as follow: |
| 416 - /restricted/.* requires being an instance administrator. | 438 - /restricted/.* requires being an instance administrator. |
| 417 - /stats/.* has statistics. | 439 - /stats/.* has statistics. |
| 418 """ | 440 """ |
| 419 acl.bootstrap() | 441 acl.bootstrap() |
| 420 template.bootstrap() | 442 template.bootstrap() |
| 421 return webapp2.WSGIApplication(get_routes(), debug=debug) | 443 return webapp2.WSGIApplication(get_routes(), debug=debug) |
| OLD | NEW |