Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport PR #4260 on branch 5.7.x (Launch the browser with a redirect file) #4265

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions notebook/auth/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions notebook/base/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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,
Expand Down
110 changes: 76 additions & 34 deletions notebook/notebookapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import signal
import socket
import sys
import tempfile
import threading
import time
import warnings
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -1178,6 +1173,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=_("""
Expand Down Expand Up @@ -1357,9 +1359,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):
Expand Down Expand Up @@ -1689,6 +1688,67 @@ def remove_server_info_file(self):
if e.errno != errno.ENOENT:
raise

def write_browser_open_file(self):
"""Write an nbserver-<pid>-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 io.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-<pid>-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

Expand Down Expand Up @@ -1718,38 +1778,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,
]))

Expand All @@ -1765,6 +1806,7 @@ def start(self):
info(_("Interrupted..."))
finally:
self.remove_server_info_file()
self.remove_browser_open_file()
self.cleanup_kernels()

def stop(self):
Expand Down
18 changes: 18 additions & 0 deletions notebook/templates/browser-open.html
Original file line number Diff line number Diff line change
@@ -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. #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="1;url={{ open_url }}" />
<title>Opening Jupyter Notebook</title>
</head>
<body>

<p>
This page should redirect you to Jupyter Notebook. If it doesn't,
<a href="{{ open_url }}">click here to go to Jupyter</a>.
</p>

</body>
</html>
7 changes: 4 additions & 3 deletions notebook/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down