|
@@
-17,21
+17,15
b' Authors:'
|
|
17
|
#-----------------------------------------------------------------------------
|
|
17
|
#-----------------------------------------------------------------------------
|
|
18
|
|
|
18
|
|
|
19
|
|
|
19
|
|
|
20
|
import datetime
|
|
|
|
|
21
|
import email.utils
|
|
|
|
|
22
|
import functools
|
|
20
|
import functools
|
|
23
|
import hashlib
|
|
|
|
|
24
|
import json
|
|
21
|
import json
|
|
25
|
import logging
|
|
22
|
import logging
|
|
26
|
import mimetypes
|
|
|
|
|
27
|
import os
|
|
23
|
import os
|
|
28
|
import stat
|
|
24
|
import stat
|
|
29
|
import sys
|
|
25
|
import sys
|
|
30
|
import threading
|
|
|
|
|
31
|
import traceback
|
|
26
|
import traceback
|
|
32
|
|
|
27
|
|
|
33
|
from tornado import web
|
|
28
|
from tornado import web
|
|
34
|
from tornado import websocket
|
|
|
|
|
35
|
|
|
29
|
|
|
36
|
try:
|
|
30
|
try:
|
|
37
|
from tornado.log import app_log
|
|
31
|
from tornado.log import app_log
|
|
@@
-39,66
+33,13
b' except ImportError:'
|
|
39
|
app_log = logging.getLogger()
|
|
33
|
app_log = logging.getLogger()
|
|
40
|
|
|
34
|
|
|
41
|
from IPython.config import Application
|
|
35
|
from IPython.config import Application
|
|
42
|
from IPython.external.decorator import decorator
|
|
|
|
|
43
|
from IPython.utils.path import filefind
|
|
36
|
from IPython.utils.path import filefind
|
|
44
|
from IPython.utils.jsonutil import date_default
|
|
|
|
|
45
|
|
|
37
|
|
|
46
|
# UF_HIDDEN is a stat flag not defined in the stat module.
|
|
38
|
# UF_HIDDEN is a stat flag not defined in the stat module.
|
|
47
|
# It is used by BSD to indicate hidden files.
|
|
39
|
# It is used by BSD to indicate hidden files.
|
|
48
|
UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
|
|
40
|
UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
|
|
49
|
|
|
41
|
|
|
50
|
#-----------------------------------------------------------------------------
|
|
42
|
#-----------------------------------------------------------------------------
|
|
51
|
# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
|
|
|
|
|
52
|
#-----------------------------------------------------------------------------
|
|
|
|
|
53
|
|
|
|
|
|
54
|
# Google Chrome, as of release 16, changed its websocket protocol number. The
|
|
|
|
|
55
|
# parts tornado cares about haven't really changed, so it's OK to continue
|
|
|
|
|
56
|
# accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
|
|
|
|
|
57
|
# version as of Oct 30/2011) the version check fails, see the issue report:
|
|
|
|
|
58
|
|
|
|
|
|
59
|
# https://github.com/facebook/tornado/issues/385
|
|
|
|
|
60
|
|
|
|
|
|
61
|
# This issue has been fixed in Tornado post 2.1.1:
|
|
|
|
|
62
|
|
|
|
|
|
63
|
# https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
|
|
|
|
|
64
|
|
|
|
|
|
65
|
# Here we manually apply the same patch as above so that users of IPython can
|
|
|
|
|
66
|
# continue to work with an officially released Tornado. We make the
|
|
|
|
|
67
|
# monkeypatch version check as narrow as possible to limit its effects; once
|
|
|
|
|
68
|
# Tornado 2.1.1 is no longer found in the wild we'll delete this code.
|
|
|
|
|
69
|
|
|
|
|
|
70
|
import tornado
|
|
|
|
|
71
|
|
|
|
|
|
72
|
if tornado.version_info <= (2,1,1):
|
|
|
|
|
73
|
|
|
|
|
|
74
|
def _execute(self, transforms, *args, **kwargs):
|
|
|
|
|
75
|
from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
|
|
|
|
|
76
|
|
|
|
|
|
77
|
self.open_args = args
|
|
|
|
|
78
|
self.open_kwargs = kwargs
|
|
|
|
|
79
|
|
|
|
|
|
80
|
# The difference between version 8 and 13 is that in 8 the
|
|
|
|
|
81
|
# client sends a "Sec-Websocket-Origin" header and in 13 it's
|
|
|
|
|
82
|
# simply "Origin".
|
|
|
|
|
83
|
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
|
|
|
|
|
84
|
self.ws_connection = WebSocketProtocol8(self)
|
|
|
|
|
85
|
self.ws_connection.accept_connection()
|
|
|
|
|
86
|
|
|
|
|
|
87
|
elif self.request.headers.get("Sec-WebSocket-Version"):
|
|
|
|
|
88
|
self.stream.write(tornado.escape.utf8(
|
|
|
|
|
89
|
"HTTP/1.1 426 Upgrade Required\r\n"
|
|
|
|
|
90
|
"Sec-WebSocket-Version: 8\r\n\r\n"))
|
|
|
|
|
91
|
self.stream.close()
|
|
|
|
|
92
|
|
|
|
|
|
93
|
else:
|
|
|
|
|
94
|
self.ws_connection = WebSocketProtocol76(self)
|
|
|
|
|
95
|
self.ws_connection.accept_connection()
|
|
|
|
|
96
|
|
|
|
|
|
97
|
websocket.WebSocketHandler._execute = _execute
|
|
|
|
|
98
|
del _execute
|
|
|
|
|
99
|
|
|
|
|
|
100
|
|
|
|
|
|
101
|
#-----------------------------------------------------------------------------
|
|
|
|
|
102
|
# Top-level handlers
|
|
43
|
# Top-level handlers
|
|
103
|
#-----------------------------------------------------------------------------
|
|
44
|
#-----------------------------------------------------------------------------
|
|
104
|
|
|
45
|
|
|
@@
-359,20
+300,20
b' HTTPError = web.HTTPError'
|
|
359
|
class FileFindHandler(web.StaticFileHandler):
|
|
300
|
class FileFindHandler(web.StaticFileHandler):
|
|
360
|
"""subclass of StaticFileHandler for serving files from a search path"""
|
|
301
|
"""subclass of StaticFileHandler for serving files from a search path"""
|
|
361
|
|
|
302
|
|
|
|
|
|
303
|
# cache search results, don't search for files more than once
|
|
362
|
_static_paths = {}
|
|
304
|
_static_paths = {}
|
|
363
|
# _lock is needed for tornado < 2.2.0 compat
|
|
|
|
|
364
|
_lock = threading.Lock() # protects _static_hashes
|
|
|
|
|
365
|
|
|
305
|
|
|
366
|
def initialize(self, path, default_filename=None):
|
|
306
|
def initialize(self, path, default_filename=None):
|
|
367
|
if isinstance(path, basestring):
|
|
307
|
if isinstance(path, basestring):
|
|
368
|
path = [path]
|
|
308
|
path = [path]
|
|
369
|
self.roots = tuple(
|
|
309
|
|
|
|
|
|
310
|
self.root = tuple(
|
|
370
|
os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
|
|
311
|
os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
|
|
371
|
)
|
|
312
|
)
|
|
372
|
self.default_filename = default_filename
|
|
313
|
self.default_filename = default_filename
|
|
373
|
|
|
314
|
|
|
374
|
@classmethod
|
|
315
|
@classmethod
|
|
375
|
def locate_file(cls, path, roots):
|
|
316
|
def get_absolute_path(cls, roots, path):
|
|
376
|
"""locate a file to serve on our static file search path"""
|
|
317
|
"""locate a file to serve on our static file search path"""
|
|
377
|
with cls._lock:
|
|
318
|
with cls._lock:
|
|
378
|
if path in cls._static_paths:
|
|
319
|
if path in cls._static_paths:
|
|
@@
-382,131
+323,18
b' class FileFindHandler(web.StaticFileHandler):'
|
|
382
|
except IOError:
|
|
323
|
except IOError:
|
|
383
|
# empty string should always give exists=False
|
|
324
|
# empty string should always give exists=False
|
|
384
|
return ''
|
|
325
|
return ''
|
|
385
|
|
|
326
|
|
|
386
|
# os.path.abspath strips a trailing /
|
|
|
|
|
387
|
# it needs to be temporarily added back for requests to root/
|
|
|
|
|
388
|
if not (abspath + os.sep).startswith(roots):
|
|
|
|
|
389
|
raise HTTPError(403, "%s is not in root static directory", path)
|
|
|
|
|
390
|
|
|
|
|
|
391
|
cls._static_paths[path] = abspath
|
|
327
|
cls._static_paths[path] = abspath
|
|
392
|
return abspath
|
|
328
|
return abspath
|
|
393
|
|
|
329
|
|
|
394
|
def get(self, path, include_body=True):
|
|
330
|
def validate_absolute_path(self, root, absolute_path):
|
|
395
|
path = self.parse_url_path(path)
|
|
331
|
"""check if the file should be served (raises 404, 403, etc.)"""
|
|
396
|
|
|
332
|
for root in self.root:
|
|
397
|
# begin subclass override
|
|
333
|
if (absolute_path + os.sep).startswith(root):
|
|
398
|
abspath = self.locate_file(path, self.roots)
|
|
334
|
break
|
|
399
|
# end subclass override
|
|
|
|
|
400
|
|
|
|
|
|
401
|
if os.path.isdir(abspath) and self.default_filename is not None:
|
|
|
|
|
402
|
# need to look at the request.path here for when path is empty
|
|
|
|
|
403
|
# but there is some prefix to the path that was already
|
|
|
|
|
404
|
# trimmed by the routing
|
|
|
|
|
405
|
if not self.request.path.endswith("/"):
|
|
|
|
|
406
|
self.redirect(self.request.path + "/")
|
|
|
|
|
407
|
return
|
|
|
|
|
408
|
abspath = os.path.join(abspath, self.default_filename)
|
|
|
|
|
409
|
if not os.path.exists(abspath):
|
|
|
|
|
410
|
raise HTTPError(404)
|
|
|
|
|
411
|
if not os.path.isfile(abspath):
|
|
|
|
|
412
|
raise HTTPError(403, "%s is not a file", path)
|
|
|
|
|
413
|
|
|
|
|
|
414
|
stat_result = os.stat(abspath)
|
|
|
|
|
415
|
modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
|
|
|
|
|
416
|
|
|
|
|
|
417
|
self.set_header("Last-Modified", modified)
|
|
|
|
|
418
|
|
|
|
|
|
419
|
mime_type, encoding = mimetypes.guess_type(abspath)
|
|
|
|
|
420
|
if mime_type:
|
|
|
|
|
421
|
self.set_header("Content-Type", mime_type)
|
|
|
|
|
422
|
|
|
|
|
|
423
|
cache_time = self.get_cache_time(path, modified, mime_type)
|
|
|
|
|
424
|
|
|
|
|
|
425
|
if cache_time > 0:
|
|
|
|
|
426
|
self.set_header("Expires", datetime.datetime.utcnow() + \
|
|
|
|
|
427
|
datetime.timedelta(seconds=cache_time))
|
|
|
|
|
428
|
self.set_header("Cache-Control", "max-age=" + str(cache_time))
|
|
|
|
|
429
|
else:
|
|
|
|
|
430
|
self.set_header("Cache-Control", "public")
|
|
|
|
|
431
|
|
|
|
|
|
432
|
self.set_extra_headers(path)
|
|
|
|
|
433
|
|
|
|
|
|
434
|
# Check the If-Modified-Since, and don't send the result if the
|
|
|
|
|
435
|
# content has not been modified
|
|
|
|
|
436
|
ims_value = self.request.headers.get("If-Modified-Since")
|
|
|
|
|
437
|
if ims_value is not None:
|
|
|
|
|
438
|
date_tuple = email.utils.parsedate(ims_value)
|
|
|
|
|
439
|
if_since = datetime.datetime(*date_tuple[:6])
|
|
|
|
|
440
|
if if_since >= modified:
|
|
|
|
|
441
|
self.set_status(304)
|
|
|
|
|
442
|
return
|
|
|
|
|
443
|
|
|
335
|
|
|
444
|
with open(abspath, "rb") as file:
|
|
336
|
return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
|
|
445
|
data = file.read()
|
|
|
|
|
446
|
hasher = hashlib.sha1()
|
|
|
|
|
447
|
hasher.update(data)
|
|
|
|
|
448
|
self.set_header("Etag", '"%s"' % hasher.hexdigest())
|
|
|
|
|
449
|
if include_body:
|
|
|
|
|
450
|
self.write(data)
|
|
|
|
|
451
|
else:
|
|
|
|
|
452
|
assert self.request.method == "HEAD"
|
|
|
|
|
453
|
self.set_header("Content-Length", len(data))
|
|
|
|
|
454
|
|
|
|
|
|
455
|
@classmethod
|
|
|
|
|
456
|
def get_version(cls, settings, path):
|
|
|
|
|
457
|
"""Generate the version string to be used in static URLs.
|
|
|
|
|
458
|
|
|
|
|
|
459
|
This method may be overridden in subclasses (but note that it
|
|
|
|
|
460
|
is a class method rather than a static method). The default
|
|
|
|
|
461
|
implementation uses a hash of the file's contents.
|
|
|
|
|
462
|
|
|
337
|
|
|
463
|
``settings`` is the `Application.settings` dictionary and ``path``
|
|
|
|
|
464
|
is the relative location of the requested asset on the filesystem.
|
|
|
|
|
465
|
The returned value should be a string, or ``None`` if no version
|
|
|
|
|
466
|
could be determined.
|
|
|
|
|
467
|
"""
|
|
|
|
|
468
|
# begin subclass override:
|
|
|
|
|
469
|
static_paths = settings['static_path']
|
|
|
|
|
470
|
if isinstance(static_paths, basestring):
|
|
|
|
|
471
|
static_paths = [static_paths]
|
|
|
|
|
472
|
roots = tuple(
|
|
|
|
|
473
|
os.path.abspath(os.path.expanduser(p)) + os.sep for p in static_paths
|
|
|
|
|
474
|
)
|
|
|
|
|
475
|
|
|
|
|
|
476
|
try:
|
|
|
|
|
477
|
abs_path = filefind(path, roots)
|
|
|
|
|
478
|
except IOError:
|
|
|
|
|
479
|
app_log.error("Could not find static file %r", path)
|
|
|
|
|
480
|
return None
|
|
|
|
|
481
|
|
|
|
|
|
482
|
# end subclass override
|
|
|
|
|
483
|
|
|
|
|
|
484
|
with cls._lock:
|
|
|
|
|
485
|
hashes = cls._static_hashes
|
|
|
|
|
486
|
if abs_path not in hashes:
|
|
|
|
|
487
|
try:
|
|
|
|
|
488
|
f = open(abs_path, "rb")
|
|
|
|
|
489
|
hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
|
|
|
|
|
490
|
f.close()
|
|
|
|
|
491
|
except Exception:
|
|
|
|
|
492
|
app_log.error("Could not open static file %r", path)
|
|
|
|
|
493
|
hashes[abs_path] = None
|
|
|
|
|
494
|
hsh = hashes.get(abs_path)
|
|
|
|
|
495
|
if hsh:
|
|
|
|
|
496
|
return hsh[:5]
|
|
|
|
|
497
|
return None
|
|
|
|
|
498
|
|
|
|
|
|
499
|
|
|
|
|
|
500
|
def parse_url_path(self, url_path):
|
|
|
|
|
501
|
"""Converts a static URL path into a filesystem path.
|
|
|
|
|
502
|
|
|
|
|
|
503
|
``url_path`` is the path component of the URL with
|
|
|
|
|
504
|
``static_url_prefix`` removed. The return value should be
|
|
|
|
|
505
|
filesystem path relative to ``static_path``.
|
|
|
|
|
506
|
"""
|
|
|
|
|
507
|
if os.sep != "/":
|
|
|
|
|
508
|
url_path = url_path.replace("/", os.sep)
|
|
|
|
|
509
|
return url_path
|
|
|
|
|
510
|
|
|
338
|
|
|
511
|
class TrailingSlashHandler(web.RequestHandler):
|
|
339
|
class TrailingSlashHandler(web.RequestHandler):
|
|
512
|
"""Simple redirect handler that strips trailing slashes
|
|
340
|
"""Simple redirect handler that strips trailing slashes
|