Skip to content

Commit 270c0f9

Browse files
committed
Launch the browser with a redirect file
This avoids putting the authentication token into a command-line argument to launch the browser, where it's visible to other users. Filesystem permissions should ensure that only the user who started the notebook can use this route to authenticate. Thanks to Dr Owain Kenway for suggesting this technique.
1 parent f759e4d commit 270c0f9

File tree

3 files changed

+95
-26
lines changed

3 files changed

+95
-26
lines changed

notebook/notebookapp.py

+73-23
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import signal
2727
import socket
2828
import sys
29+
import tempfile
2930
import threading
3031
import time
3132
import warnings
@@ -107,7 +108,7 @@
107108
from notebook._sysinfo import get_sys_info
108109

109110
from ._tz import utcnow, utcfromtimestamp
110-
from .utils import url_path_join, check_pid, url_escape
111+
from .utils import url_path_join, check_pid, url_escape, urljoin, pathname2url
111112

112113
#-----------------------------------------------------------------------------
113114
# Module globals
@@ -1184,6 +1185,13 @@ def _update_mathjax_config(self, change):
11841185
def _default_info_file(self):
11851186
info_file = "nbserver-%s.json" % os.getpid()
11861187
return os.path.join(self.runtime_dir, info_file)
1188+
1189+
browser_open_file = Unicode()
1190+
1191+
@default('browser_open_file')
1192+
def _default_browser_open_file(self):
1193+
basename = "nbserver-%s-open.html" % os.getpid()
1194+
return os.path.join(self.runtime_dir, basename)
11871195

11881196
pylab = Unicode('disabled', config=True,
11891197
help=_("""
@@ -1697,6 +1705,67 @@ def remove_server_info_file(self):
16971705
if e.errno != errno.ENOENT:
16981706
raise
16991707

1708+
def write_browser_open_file(self):
1709+
"""Write an nbserver-<pid>-open.html file
1710+
1711+
This can be used to open the notebook in a browser
1712+
"""
1713+
# default_url contains base_url, but so does connection_url
1714+
open_url = self.default_url[len(self.base_url):]
1715+
1716+
with open(self.browser_open_file, 'w', encoding='utf-8') as f:
1717+
self._write_browser_open_file(open_url, f)
1718+
1719+
def _write_browser_open_file(self, url, fh):
1720+
if self.token:
1721+
url = url_concat(url, {'token': self.one_time_token})
1722+
url = url_path_join(self.connection_url, url)
1723+
1724+
jinja2_env = self.web_app.settings['jinja2_env']
1725+
template = jinja2_env.get_template('browser-open.html')
1726+
fh.write(template.render(open_url=url))
1727+
1728+
def remove_browser_open_file(self):
1729+
"""Remove the nbserver-<pid>-open.html file created for this server.
1730+
1731+
Ignores the error raised when the file has already been removed.
1732+
"""
1733+
try:
1734+
os.unlink(self.browser_open_file)
1735+
except OSError as e:
1736+
if e.errno != errno.ENOENT:
1737+
raise
1738+
1739+
def launch_browser(self):
1740+
try:
1741+
browser = webbrowser.get(self.browser or None)
1742+
except webbrowser.Error as e:
1743+
self.log.warning(_('No web browser found: %s.') % e)
1744+
browser = None
1745+
1746+
if not browser:
1747+
return
1748+
1749+
if self.file_to_run:
1750+
if not os.path.exists(self.file_to_run):
1751+
self.log.critical(_("%s does not exist") % self.file_to_run)
1752+
self.exit(1)
1753+
1754+
relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1755+
uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep)))
1756+
1757+
# Write a temporary file to open in the browser
1758+
fd, open_file = tempfile.mkstemp(suffix='.html')
1759+
with open(fd, 'w', encoding='utf-8') as fh:
1760+
self._write_browser_open_file(uri, fh)
1761+
else:
1762+
open_file = self.browser_open_file
1763+
1764+
b = lambda: browser.open(
1765+
urljoin('file:', pathname2url(open_file)),
1766+
new=self.webbrowser_open_new)
1767+
threading.Thread(target=b).start()
1768+
17001769
def start(self):
17011770
""" Start the Notebook server app, after initialization
17021771
@@ -1726,30 +1795,10 @@ def start(self):
17261795
"resources section at https://jupyter.org/community.html."))
17271796

17281797
self.write_server_info_file()
1798+
self.write_browser_open_file()
17291799

17301800
if self.open_browser or self.file_to_run:
1731-
try:
1732-
browser = webbrowser.get(self.browser or None)
1733-
except webbrowser.Error as e:
1734-
self.log.warning(_('No web browser found: %s.') % e)
1735-
browser = None
1736-
1737-
if self.file_to_run:
1738-
if not os.path.exists(self.file_to_run):
1739-
self.log.critical(_("%s does not exist") % self.file_to_run)
1740-
self.exit(1)
1741-
1742-
relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1743-
uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep)))
1744-
else:
1745-
# default_url contains base_url, but so does connection_url
1746-
uri = self.default_url[len(self.base_url):]
1747-
if self.one_time_token:
1748-
uri = url_concat(uri, {'token': self.one_time_token})
1749-
if browser:
1750-
b = lambda : browser.open(url_path_join(self.connection_url, uri),
1751-
new=self.webbrowser_open_new)
1752-
threading.Thread(target=b).start()
1801+
self.launch_browser()
17531802

17541803
if self.token and self._token_generated:
17551804
# log full URL with generated token, so there's a copy/pasteable link
@@ -1773,6 +1822,7 @@ def start(self):
17731822
info(_("Interrupted..."))
17741823
finally:
17751824
self.remove_server_info_file()
1825+
self.remove_browser_open_file()
17761826
self.cleanup_kernels()
17771827

17781828
def stop(self):

notebook/templates/browser-open.html

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{# This template is not served, but written as a file to open in the browser,
2+
passing the token without putting it in a command-line argument. #}
3+
<!DOCTYPE html>
4+
<html lang="en">
5+
<head>
6+
<meta charset="UTF-8">
7+
<meta http-equiv="refresh" content="1;url={{ open_url }}" />
8+
<title>Opening Jupyter Notebook</title>
9+
</head>
10+
<body>
11+
12+
<p>
13+
This page should redirect you to Jupyter Notebook. If it doesn't,
14+
<a href="{{ open_url }}">click here to go to Jupyter</a>.
15+
</p>
16+
17+
</body>
18+
</html>

notebook/utils.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
from distutils.version import LooseVersion
1414

1515
try:
16-
from urllib.parse import quote, unquote, urlparse
16+
from urllib.parse import quote, unquote, urlparse, urljoin
17+
from urllib.request import pathname2url
1718
except ImportError:
18-
from urllib import quote, unquote
19-
from urlparse import urlparse
19+
from urllib import quote, unquote, pathname2url
20+
from urlparse import urlparse, urljoin
2021

2122
from ipython_genutils import py3compat
2223

0 commit comments

Comments
 (0)