diff --git a/jupyterhub/app.py b/jupyterhub/app.py index bf321f23..0138d6ff 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -50,7 +50,7 @@ from ._data import DATA_FILES_PATH from .log import CoroutineLogFormatter, log_request from .traitlets import URLPrefix, Command from .utils import ( - url_path_join, + url_path_join, localhost, ISO8601_ms, ISO8601_s, ) # classes for config @@ -260,7 +260,7 @@ class JupyterHub(Application): token = orm.new_token() return token - proxy_api_ip = Unicode('localhost', config=True, + proxy_api_ip = Unicode(localhost(), config=True, help="The ip for the proxy API handlers" ) proxy_api_port = Integer(config=True, @@ -272,7 +272,7 @@ class JupyterHub(Application): hub_port = Integer(8081, config=True, help="The port for this process" ) - hub_ip = Unicode('localhost', config=True, + hub_ip = Unicode(localhost(), config=True, help="The ip for this process" ) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index ca11e1da..fb7e656c 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -26,7 +26,7 @@ from sqlalchemy import create_engine from .utils import ( random_port, url_path_join, wait_for_server, wait_for_http_server, - new_token, hash_token, compare_token, + new_token, hash_token, compare_token, localhost, ) @@ -78,7 +78,7 @@ class Server(Base): ip = self.ip if ip in {'', '0.0.0.0'}: # when listening on all interfaces, connect to localhost - ip = 'localhost' + ip = localhost() return "{proto}://{ip}:{port}".format( proto=self.proto, ip=ip, @@ -109,12 +109,12 @@ class Server(Base): if http: yield wait_for_http_server(self.url, timeout=timeout) else: - yield wait_for_server(self.ip or 'localhost', self.port, timeout=timeout) + yield wait_for_server(self.ip or localhost(), self.port, timeout=timeout) def is_up(self): """Is the server accepting connections?""" try: - socket.create_connection((self.ip or 'localhost', self.port)) + socket.create_connection((self.ip or localhost(), self.port)) except socket.error as e: if e.errno == errno.ENETUNREACH: try: diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index f5c7e2b1..a481eb83 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -22,7 +22,7 @@ from traitlets import ( ) from .traitlets import Command -from .utils import random_port +from .utils import random_port, localhost class Spawner(LoggingConfigurable): """Base class for spawning single-user notebook servers. @@ -41,7 +41,7 @@ class Spawner(LoggingConfigurable): hub = Any() authenticator = Any() api_token = Unicode() - ip = Unicode('localhost', config=True, + ip = Unicode(localhost(), config=True, help="The IP address (or hostname) the single-user server should listen on" ) start_timeout = Integer(60, config=True, diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index 9491e1f9..e1b49b43 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -17,6 +17,7 @@ from ..spawner import LocalProcessSpawner from ..app import JupyterHub from ..auth import PAMAuthenticator from .. import orm +from ..utils import localhost from pamela import PAMError @@ -108,7 +109,7 @@ class MockHub(JupyterHub): db_file = None def _ip_default(self): - return 'localhost' + return localhost() def _authenticator_class_default(self): return MockPAMAuthenticator diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index e9f8223d..dcf01335 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -6,10 +6,12 @@ from binascii import b2a_hex import errno import hashlib +from hmac import compare_digest import os import socket +from threading import Thread import uuid -from hmac import compare_digest +import warnings from tornado import web, gen, ioloop from tornado.httpclient import AsyncHTTPClient, HTTPError @@ -192,3 +194,36 @@ def url_path_join(*pieces): result = '/' return result + +def localhost(): + """Return localhost or 127.0.0.1""" + if hasattr(localhost, '_localhost'): + return localhost._localhost + binder = connector = None + try: + binder = socket.socket() + binder.bind(('localhost', 0)) + binder.listen(1) + port = binder.getsockname()[1] + def accept(): + try: + conn, addr = binder.accept() + except ConnectionAbortedError: + pass + else: + conn.close() + t = Thread(target=accept) + t.start() + connector = socket.create_connection(('localhost', port), timeout=10) + t.join(timeout=10) + except (socket.error, socket.gaierror) as e: + warnings.warn("localhost doesn't appear to work, using 127.0.0.1\n%s" % e, RuntimeWarning) + localhost._localhost = '127.0.0.1' + else: + localhost._localhost = 'localhost' + finally: + if binder: + binder.close() + if connector: + connector.close() + return localhost._localhost