diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 3982957ea..ddd82426d 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -3,6 +3,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +import datetime import functools import json import mimetypes @@ -15,15 +16,17 @@ import warnings try: # py3 from http.client import responses + from http.cookies import Morsel except ImportError: from httplib import responses + from Cookie import Morsel try: from urllib.parse import urlparse # Py 3 except ImportError: from urlparse import urlparse # Py 2 from jinja2 import TemplateNotFound -from tornado import web, gen, escape +from tornado import web, gen, escape, httputil from tornado.log import app_log from notebook._sysinfo import get_sys_info @@ -91,14 +94,41 @@ class AuthenticatedHandler(web.RequestHandler): # for example, so just ignore) self.log.debug(e) + def force_clear_cookie(self, name, path="/", domain=None): + """Deletes the cookie with the given name. + + Tornado's cookie handling currently (Jan 2018) stores cookies in a dict + keyed by name, so it can only modify one cookie with a given name per + response. The browser can store multiple cookies with the same name + but different domains and/or paths. This method lets us clear multiple + cookies with the same name. + + Due to limitations of the cookie protocol, you must pass the same + path and domain to clear a cookie as were used when that cookie + was set (but there is no way to find out on the server side + which values were used for a given cookie). + """ + name = escape.native_str(name) + expires = datetime.datetime.utcnow() - datetime.timedelta(days=365) + + morsel = Morsel() + morsel.set(name, '', '""') + morsel['expires'] = httputil.format_timestamp(expires) + morsel['path'] = path + if domain: + morsel['domain'] = domain + self.add_header("Set-Cookie", morsel.OutputString()) + def clear_login_cookie(self): cookie_options = self.settings.get('cookie_options', {}) path = cookie_options.setdefault('path', self.base_url) self.clear_cookie(self.cookie_name, path=path) if path and path != '/': - # also clear cookie on / to ensure old cookies - # are cleared after the change in path behavior. - self.clear_cookie(self.cookie_name) + # also clear cookie on / to ensure old cookies are cleared + # after the change in path behavior (changed in notebook 5.2.2). + # N.B. This bypasses the normal cookie handling, which can't update + # two cookies with the same name. See the method above. + self.force_clear_cookie(self.cookie_name) def get_current_user(self): if self.login_handler is None: