diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index 9ca0d25ff..42a5826c2 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -17,21 +17,15 @@ Authors: #----------------------------------------------------------------------------- -import datetime -import email.utils import functools -import hashlib import json import logging -import mimetypes import os import stat import sys -import threading import traceback from tornado import web -from tornado import websocket try: from tornado.log import app_log @@ -39,65 +33,12 @@ except ImportError: app_log = logging.getLogger() from IPython.config import Application -from IPython.external.decorator import decorator from IPython.utils.path import filefind -from IPython.utils.jsonutil import date_default # UF_HIDDEN is a stat flag not defined in the stat module. # It is used by BSD to indicate hidden files. UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768) -#----------------------------------------------------------------------------- -# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary! -#----------------------------------------------------------------------------- - -# Google Chrome, as of release 16, changed its websocket protocol number. The -# parts tornado cares about haven't really changed, so it's OK to continue -# accepting Chrome connections, but as of Tornado 2.1.1 (the currently released -# version as of Oct 30/2011) the version check fails, see the issue report: - -# https://github.com/facebook/tornado/issues/385 - -# This issue has been fixed in Tornado post 2.1.1: - -# https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710 - -# Here we manually apply the same patch as above so that users of IPython can -# continue to work with an officially released Tornado. We make the -# monkeypatch version check as narrow as possible to limit its effects; once -# Tornado 2.1.1 is no longer found in the wild we'll delete this code. - -import tornado - -if tornado.version_info <= (2,1,1): - - def _execute(self, transforms, *args, **kwargs): - from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76 - - self.open_args = args - self.open_kwargs = kwargs - - # The difference between version 8 and 13 is that in 8 the - # client sends a "Sec-Websocket-Origin" header and in 13 it's - # simply "Origin". - if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"): - self.ws_connection = WebSocketProtocol8(self) - self.ws_connection.accept_connection() - - elif self.request.headers.get("Sec-WebSocket-Version"): - self.stream.write(tornado.escape.utf8( - "HTTP/1.1 426 Upgrade Required\r\n" - "Sec-WebSocket-Version: 8\r\n\r\n")) - self.stream.close() - - else: - self.ws_connection = WebSocketProtocol76(self) - self.ws_connection.accept_connection() - - websocket.WebSocketHandler._execute = _execute - del _execute - - #----------------------------------------------------------------------------- # Top-level handlers #----------------------------------------------------------------------------- @@ -359,20 +300,20 @@ HTTPError = web.HTTPError class FileFindHandler(web.StaticFileHandler): """subclass of StaticFileHandler for serving files from a search path""" + # cache search results, don't search for files more than once _static_paths = {} - # _lock is needed for tornado < 2.2.0 compat - _lock = threading.Lock() # protects _static_hashes def initialize(self, path, default_filename=None): if isinstance(path, basestring): path = [path] - self.roots = tuple( + + self.root = tuple( os.path.abspath(os.path.expanduser(p)) + os.sep for p in path ) self.default_filename = default_filename @classmethod - def locate_file(cls, path, roots): + def get_absolute_path(cls, roots, path): """locate a file to serve on our static file search path""" with cls._lock: if path in cls._static_paths: @@ -382,131 +323,18 @@ class FileFindHandler(web.StaticFileHandler): except IOError: # empty string should always give exists=False return '' - - # os.path.abspath strips a trailing / - # it needs to be temporarily added back for requests to root/ - if not (abspath + os.sep).startswith(roots): - raise HTTPError(403, "%s is not in root static directory", path) - + cls._static_paths[path] = abspath return abspath - def get(self, path, include_body=True): - path = self.parse_url_path(path) - - # begin subclass override - abspath = self.locate_file(path, self.roots) - # end subclass override - - if os.path.isdir(abspath) and self.default_filename is not None: - # need to look at the request.path here for when path is empty - # but there is some prefix to the path that was already - # trimmed by the routing - if not self.request.path.endswith("/"): - self.redirect(self.request.path + "/") - return - abspath = os.path.join(abspath, self.default_filename) - if not os.path.exists(abspath): - raise HTTPError(404) - if not os.path.isfile(abspath): - raise HTTPError(403, "%s is not a file", path) - - stat_result = os.stat(abspath) - modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME]) - - self.set_header("Last-Modified", modified) - - mime_type, encoding = mimetypes.guess_type(abspath) - if mime_type: - self.set_header("Content-Type", mime_type) - - cache_time = self.get_cache_time(path, modified, mime_type) - - if cache_time > 0: - self.set_header("Expires", datetime.datetime.utcnow() + \ - datetime.timedelta(seconds=cache_time)) - self.set_header("Cache-Control", "max-age=" + str(cache_time)) - else: - self.set_header("Cache-Control", "public") - - self.set_extra_headers(path) - - # Check the If-Modified-Since, and don't send the result if the - # content has not been modified - ims_value = self.request.headers.get("If-Modified-Since") - if ims_value is not None: - date_tuple = email.utils.parsedate(ims_value) - if_since = datetime.datetime(*date_tuple[:6]) - if if_since >= modified: - self.set_status(304) - return + def validate_absolute_path(self, root, absolute_path): + """check if the file should be served (raises 404, 403, etc.)""" + for root in self.root: + if (absolute_path + os.sep).startswith(root): + break - with open(abspath, "rb") as file: - data = file.read() - hasher = hashlib.sha1() - hasher.update(data) - self.set_header("Etag", '"%s"' % hasher.hexdigest()) - if include_body: - self.write(data) - else: - assert self.request.method == "HEAD" - self.set_header("Content-Length", len(data)) - - @classmethod - def get_version(cls, settings, path): - """Generate the version string to be used in static URLs. - - This method may be overridden in subclasses (but note that it - is a class method rather than a static method). The default - implementation uses a hash of the file's contents. + return super(FileFindHandler, self).validate_absolute_path(root, absolute_path) - ``settings`` is the `Application.settings` dictionary and ``path`` - is the relative location of the requested asset on the filesystem. - The returned value should be a string, or ``None`` if no version - could be determined. - """ - # begin subclass override: - static_paths = settings['static_path'] - if isinstance(static_paths, basestring): - static_paths = [static_paths] - roots = tuple( - os.path.abspath(os.path.expanduser(p)) + os.sep for p in static_paths - ) - - try: - abs_path = filefind(path, roots) - except IOError: - app_log.error("Could not find static file %r", path) - return None - - # end subclass override - - with cls._lock: - hashes = cls._static_hashes - if abs_path not in hashes: - try: - f = open(abs_path, "rb") - hashes[abs_path] = hashlib.md5(f.read()).hexdigest() - f.close() - except Exception: - app_log.error("Could not open static file %r", path) - hashes[abs_path] = None - hsh = hashes.get(abs_path) - if hsh: - return hsh[:5] - return None - - - def parse_url_path(self, url_path): - """Converts a static URL path into a filesystem path. - - ``url_path`` is the path component of the URL with - ``static_url_prefix`` removed. The return value should be - filesystem path relative to ``static_path``. - """ - if os.sep != "/": - url_path = url_path.replace("/", os.sep) - return url_path class TrailingSlashHandler(web.RequestHandler): """Simple redirect handler that strips trailing slashes diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index e523cf4aa..f86e55897 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -42,8 +42,8 @@ from jinja2 import Environment, FileSystemLoader from zmq.eventloop import ioloop ioloop.install() -# check for tornado 2.1.0 -msg = "The IPython Notebook requires tornado >= 2.1.0" +# check for tornado 3.1.0 +msg = "The IPython Notebook requires tornado >= 3.1.0" try: import tornado except ImportError: @@ -52,7 +52,7 @@ try: version_info = tornado.version_info except AttributeError: raise ImportError(msg + ", but you have < 1.1.0") -if version_info < (2,1,0): +if version_info < (3,1,0): raise ImportError(msg + ", but you have %s" % tornado.version) from tornado import httpserver @@ -542,9 +542,8 @@ class NotebookApp(BaseIPythonApplication): # hook up tornado 3's loggers to our app handlers for name in ('access', 'application', 'general'): logger = logging.getLogger('tornado.%s' % name) - logger.propagate = False + logger.parent = self.log logger.setLevel(self.log.level) - logger.handlers = self.log.handlers def init_webapp(self): """initialize tornado webapp and httpserver""" @@ -692,8 +691,8 @@ class NotebookApp(BaseIPythonApplication): @catch_config_error def initialize(self, argv=None): - self.init_logging() super(NotebookApp, self).initialize(argv) + self.init_logging() self.init_kernel_argv() self.init_configurables() self.init_components() diff --git a/setup.py b/setup.py index 6498bff88..204804785 100755 --- a/setup.py +++ b/setup.py @@ -273,7 +273,7 @@ if 'setuptools' in sys.modules: zmq = 'pyzmq>=2.1.11', doc = 'Sphinx>=0.3', test = 'nose>=0.10.1', - notebook = ['tornado>=2.0', 'pyzmq>=2.1.11', 'jinja2'], + notebook = ['tornado>=3.1', 'pyzmq>=2.1.11', 'jinja2'], nbconvert = ['pygments', 'jinja2', 'Sphinx>=0.3'] ) everything = set()