diff --git a/notebook/auth/login.py b/notebook/auth/login.py index 14b510ccac..b32123faf5 100644 --- a/notebook/auth/login.py +++ b/notebook/auth/login.py @@ -204,17 +204,11 @@ def get_user_token(cls, handler): return # check login token from URL argument or Authorization header user_token = cls.get_token(handler) - one_time_token = handler.one_time_token authenticated = False if user_token == token: # token-authenticated, set the login cookie handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip) authenticated = True - elif one_time_token and user_token == one_time_token: - # one-time-token-authenticated, only allow this token once - handler.settings.pop('one_time_token', None) - handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip) - authenticated = True if authenticated: return uuid.uuid4().hex diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 8f7c3b25a0..c5fd8be583 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -180,11 +180,6 @@ def token(self): """Return the login token for this application, if any.""" return self.settings.get('token', None) - @property - def one_time_token(self): - """Return the one-time-use token for this application, if any.""" - return self.settings.get('one_time_token', None) - @property def login_available(self): """May a user proceed to log in? @@ -475,7 +470,7 @@ def template_namespace(self): logged_in=self.logged_in, allow_password_change=self.settings.get('allow_password_change'), login_available=self.login_available, - token_available=bool(self.token or self.one_time_token), + token_available=bool(self.token), static_url=self.static_url, sys_info=json_sys_info(), contents_js_source=self.contents_js_source, diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 46c881bee1..c180bea4d2 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -26,6 +26,7 @@ import signal import socket import sys +import tempfile import threading import time import warnings @@ -107,7 +108,7 @@ from notebook._sysinfo import get_sys_info from ._tz import utcnow, utcfromtimestamp -from .utils import url_path_join, check_pid, url_escape +from .utils import url_path_join, check_pid, url_escape, urljoin, pathname2url #----------------------------------------------------------------------------- # Module globals @@ -754,12 +755,6 @@ def _write_cookie_secret_file(self, secret): """) ).tag(config=True) - one_time_token = Unicode( - help=_("""One-time token used for opening a browser. - Once used, this token cannot be used again. - """) - ) - _token_generated = True @default('token') @@ -1184,6 +1179,13 @@ def _update_mathjax_config(self, change): def _default_info_file(self): info_file = "nbserver-%s.json" % os.getpid() return os.path.join(self.runtime_dir, info_file) + + browser_open_file = Unicode() + + @default('browser_open_file') + def _default_browser_open_file(self): + basename = "nbserver-%s-open.html" % os.getpid() + return os.path.join(self.runtime_dir, basename) pylab = Unicode('disabled', config=True, help=_(""" @@ -1363,9 +1365,6 @@ def init_webapp(self): self.tornado_settings['cookie_options'] = self.cookie_options self.tornado_settings['get_secure_cookie_kwargs'] = self.get_secure_cookie_kwargs self.tornado_settings['token'] = self.token - if (self.open_browser or self.file_to_run) and not self.password: - self.one_time_token = binascii.hexlify(os.urandom(24)).decode('ascii') - self.tornado_settings['one_time_token'] = self.one_time_token # ensure default_url starts with base_url if not self.default_url.startswith(self.base_url): @@ -1697,6 +1696,67 @@ def remove_server_info_file(self): if e.errno != errno.ENOENT: raise + def write_browser_open_file(self): + """Write an nbserver--open.html file + + This can be used to open the notebook in a browser + """ + # default_url contains base_url, but so does connection_url + open_url = self.default_url[len(self.base_url):] + + with open(self.browser_open_file, 'w', encoding='utf-8') as f: + self._write_browser_open_file(open_url, f) + + def _write_browser_open_file(self, url, fh): + if self.token: + url = url_concat(url, {'token': self.token}) + url = url_path_join(self.connection_url, url) + + jinja2_env = self.web_app.settings['jinja2_env'] + template = jinja2_env.get_template('browser-open.html') + fh.write(template.render(open_url=url)) + + def remove_browser_open_file(self): + """Remove the nbserver--open.html file created for this server. + + Ignores the error raised when the file has already been removed. + """ + try: + os.unlink(self.browser_open_file) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + def launch_browser(self): + try: + browser = webbrowser.get(self.browser or None) + except webbrowser.Error as e: + self.log.warning(_('No web browser found: %s.') % e) + browser = None + + if not browser: + return + + if self.file_to_run: + if not os.path.exists(self.file_to_run): + self.log.critical(_("%s does not exist") % self.file_to_run) + self.exit(1) + + relpath = os.path.relpath(self.file_to_run, self.notebook_dir) + uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep))) + + # Write a temporary file to open in the browser + fd, open_file = tempfile.mkstemp(suffix='.html') + with open(fd, 'w', encoding='utf-8') as fh: + self._write_browser_open_file(uri, fh) + else: + open_file = self.browser_open_file + + b = lambda: browser.open( + urljoin('file:', pathname2url(open_file)), + new=self.webbrowser_open_new) + threading.Thread(target=b).start() + def start(self): """ Start the Notebook server app, after initialization @@ -1726,38 +1786,19 @@ def start(self): "resources section at https://jupyter.org/community.html.")) self.write_server_info_file() + self.write_browser_open_file() if self.open_browser or self.file_to_run: - try: - browser = webbrowser.get(self.browser or None) - except webbrowser.Error as e: - self.log.warning(_('No web browser found: %s.') % e) - browser = None - - if self.file_to_run: - if not os.path.exists(self.file_to_run): - self.log.critical(_("%s does not exist") % self.file_to_run) - self.exit(1) - - relpath = os.path.relpath(self.file_to_run, self.notebook_dir) - uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep))) - else: - # default_url contains base_url, but so does connection_url - uri = self.default_url[len(self.base_url):] - if self.one_time_token: - uri = url_concat(uri, {'token': self.one_time_token}) - if browser: - b = lambda : browser.open(url_path_join(self.connection_url, uri), - new=self.webbrowser_open_new) - threading.Thread(target=b).start() + self.launch_browser() if self.token and self._token_generated: # log full URL with generated token, so there's a copy/pasteable link # with auth info. self.log.critical('\n'.join([ '\n', - 'Copy/paste this URL into your browser when you connect for the first time,', - 'to login with a token:', + 'To access the notebook, open this file in a browser:', + ' %s' % urljoin('file:', pathname2url(self.browser_open_file)), + 'Or copy and paste one of these URLs:', ' %s' % self.display_url, ])) @@ -1773,6 +1814,7 @@ def start(self): info(_("Interrupted...")) finally: self.remove_server_info_file() + self.remove_browser_open_file() self.cleanup_kernels() def stop(self): diff --git a/notebook/templates/browser-open.html b/notebook/templates/browser-open.html new file mode 100644 index 0000000000..6f277967fc --- /dev/null +++ b/notebook/templates/browser-open.html @@ -0,0 +1,18 @@ +{# This template is not served, but written as a file to open in the browser, + passing the token without putting it in a command-line argument. #} + + + + + + Opening Jupyter Notebook + + + +

+ This page should redirect you to Jupyter Notebook. If it doesn't, + click here to go to Jupyter. +

+ + + diff --git a/notebook/utils.py b/notebook/utils.py index de1b3ea6f6..5b3d9dff9a 100644 --- a/notebook/utils.py +++ b/notebook/utils.py @@ -13,10 +13,11 @@ from distutils.version import LooseVersion try: - from urllib.parse import quote, unquote, urlparse + from urllib.parse import quote, unquote, urlparse, urljoin + from urllib.request import pathname2url except ImportError: - from urllib import quote, unquote - from urlparse import urlparse + from urllib import quote, unquote, pathname2url + from urlparse import urlparse, urljoin from ipython_genutils import py3compat