Skip to content

Commit 51dae23

Browse files
authored
Merge pull request #4260 from takluyver/browser-open-file
Launch the browser with a redirect file
2 parents f759e4d + 56d7a2d commit 51dae23

File tree

5 files changed

+99
-49
lines changed

5 files changed

+99
-49
lines changed

notebook/auth/login.py

-6
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,11 @@ def get_user_token(cls, handler):
204204
return
205205
# check login token from URL argument or Authorization header
206206
user_token = cls.get_token(handler)
207-
one_time_token = handler.one_time_token
208207
authenticated = False
209208
if user_token == token:
210209
# token-authenticated, set the login cookie
211210
handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip)
212211
authenticated = True
213-
elif one_time_token and user_token == one_time_token:
214-
# one-time-token-authenticated, only allow this token once
215-
handler.settings.pop('one_time_token', None)
216-
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
217-
authenticated = True
218212

219213
if authenticated:
220214
return uuid.uuid4().hex

notebook/base/handlers.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,6 @@ def token(self):
180180
"""Return the login token for this application, if any."""
181181
return self.settings.get('token', None)
182182

183-
@property
184-
def one_time_token(self):
185-
"""Return the one-time-use token for this application, if any."""
186-
return self.settings.get('one_time_token', None)
187-
188183
@property
189184
def login_available(self):
190185
"""May a user proceed to log in?
@@ -475,7 +470,7 @@ def template_namespace(self):
475470
logged_in=self.logged_in,
476471
allow_password_change=self.settings.get('allow_password_change'),
477472
login_available=self.login_available,
478-
token_available=bool(self.token or self.one_time_token),
473+
token_available=bool(self.token),
479474
static_url=self.static_url,
480475
sys_info=json_sys_info(),
481476
contents_js_source=self.contents_js_source,

notebook/notebookapp.py

+76-34
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
@@ -754,12 +755,6 @@ def _write_cookie_secret_file(self, secret):
754755
""")
755756
).tag(config=True)
756757

757-
one_time_token = Unicode(
758-
help=_("""One-time token used for opening a browser.
759-
Once used, this token cannot be used again.
760-
""")
761-
)
762-
763758
_token_generated = True
764759

765760
@default('token')
@@ -1184,6 +1179,13 @@ def _update_mathjax_config(self, change):
11841179
def _default_info_file(self):
11851180
info_file = "nbserver-%s.json" % os.getpid()
11861181
return os.path.join(self.runtime_dir, info_file)
1182+
1183+
browser_open_file = Unicode()
1184+
1185+
@default('browser_open_file')
1186+
def _default_browser_open_file(self):
1187+
basename = "nbserver-%s-open.html" % os.getpid()
1188+
return os.path.join(self.runtime_dir, basename)
11871189

11881190
pylab = Unicode('disabled', config=True,
11891191
help=_("""
@@ -1363,9 +1365,6 @@ def init_webapp(self):
13631365
self.tornado_settings['cookie_options'] = self.cookie_options
13641366
self.tornado_settings['get_secure_cookie_kwargs'] = self.get_secure_cookie_kwargs
13651367
self.tornado_settings['token'] = self.token
1366-
if (self.open_browser or self.file_to_run) and not self.password:
1367-
self.one_time_token = binascii.hexlify(os.urandom(24)).decode('ascii')
1368-
self.tornado_settings['one_time_token'] = self.one_time_token
13691368

13701369
# ensure default_url starts with base_url
13711370
if not self.default_url.startswith(self.base_url):
@@ -1697,6 +1696,67 @@ def remove_server_info_file(self):
16971696
if e.errno != errno.ENOENT:
16981697
raise
16991698

1699+
def write_browser_open_file(self):
1700+
"""Write an nbserver-<pid>-open.html file
1701+
1702+
This can be used to open the notebook in a browser
1703+
"""
1704+
# default_url contains base_url, but so does connection_url
1705+
open_url = self.default_url[len(self.base_url):]
1706+
1707+
with open(self.browser_open_file, 'w', encoding='utf-8') as f:
1708+
self._write_browser_open_file(open_url, f)
1709+
1710+
def _write_browser_open_file(self, url, fh):
1711+
if self.token:
1712+
url = url_concat(url, {'token': self.token})
1713+
url = url_path_join(self.connection_url, url)
1714+
1715+
jinja2_env = self.web_app.settings['jinja2_env']
1716+
template = jinja2_env.get_template('browser-open.html')
1717+
fh.write(template.render(open_url=url))
1718+
1719+
def remove_browser_open_file(self):
1720+
"""Remove the nbserver-<pid>-open.html file created for this server.
1721+
1722+
Ignores the error raised when the file has already been removed.
1723+
"""
1724+
try:
1725+
os.unlink(self.browser_open_file)
1726+
except OSError as e:
1727+
if e.errno != errno.ENOENT:
1728+
raise
1729+
1730+
def launch_browser(self):
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 not browser:
1738+
return
1739+
1740+
if self.file_to_run:
1741+
if not os.path.exists(self.file_to_run):
1742+
self.log.critical(_("%s does not exist") % self.file_to_run)
1743+
self.exit(1)
1744+
1745+
relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1746+
uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep)))
1747+
1748+
# Write a temporary file to open in the browser
1749+
fd, open_file = tempfile.mkstemp(suffix='.html')
1750+
with open(fd, 'w', encoding='utf-8') as fh:
1751+
self._write_browser_open_file(uri, fh)
1752+
else:
1753+
open_file = self.browser_open_file
1754+
1755+
b = lambda: browser.open(
1756+
urljoin('file:', pathname2url(open_file)),
1757+
new=self.webbrowser_open_new)
1758+
threading.Thread(target=b).start()
1759+
17001760
def start(self):
17011761
""" Start the Notebook server app, after initialization
17021762
@@ -1726,38 +1786,19 @@ def start(self):
17261786
"resources section at https://jupyter.org/community.html."))
17271787

17281788
self.write_server_info_file()
1789+
self.write_browser_open_file()
17291790

17301791
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()
1792+
self.launch_browser()
17531793

17541794
if self.token and self._token_generated:
17551795
# log full URL with generated token, so there's a copy/pasteable link
17561796
# with auth info.
17571797
self.log.critical('\n'.join([
17581798
'\n',
1759-
'Copy/paste this URL into your browser when you connect for the first time,',
1760-
'to login with a token:',
1799+
'To access the notebook, open this file in a browser:',
1800+
' %s' % urljoin('file:', pathname2url(self.browser_open_file)),
1801+
'Or copy and paste one of these URLs:',
17611802
' %s' % self.display_url,
17621803
]))
17631804

@@ -1773,6 +1814,7 @@ def start(self):
17731814
info(_("Interrupted..."))
17741815
finally:
17751816
self.remove_server_info_file()
1817+
self.remove_browser_open_file()
17761818
self.cleanup_kernels()
17771819

17781820
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)