Repository: ipython/ipython

Issue:
add base_url to notebook configuration options
Hi,

(Apologies if this is already possible - I didn't find it anywhere.)

I'd like to serve my notebook behind a proxy server. For example, comsider nginx running on http://somewhere.com and serving all URLS matching the regex http://somewhere.com/ipython/astraw/(.*) by proxying them to http://localhost:8888/ipython/astraw/$1. In particular, this is proxying without URL rewriting.

To do this, I think two changes need to be made to the source: 1) change the handlers specified to tornado.web.Application so that they specify a base_url before their current name. 2) have all URL emitters (e.g. "/static/blah/blah") also have the base_url (e.g. emit base_url+"/static/blah/blah" instead).

Does this make sense? Is there a way to do it already? If so, I missed it after looking for some time, but apologies nevertheless.

Thanks for the notebook interface. I am dreaming big with this...



Discussion:

Resolving patch:
diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py
--- a/IPython/frontend/html/notebook/handlers.py
+++ b/IPython/frontend/html/notebook/handlers.py
@@ -168,12 +168,15 @@ def read_only(self):
     @property
     def ws_url(self):
         """websocket url matching the current request
-        
+
         turns http[s]://host[:port] into
                 ws[s]://host[:port]
         """
         proto = self.request.protocol.replace('http', 'ws')
-        return "%s://%s" % (proto, self.request.host)
+        host = self.application.ipython_app.websocket_host # default to config value
+        if host == '':
+            host = self.request.host # get from request
+        return "%s://%s" % (proto, host)
 
 
 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
@@ -192,7 +195,8 @@ def get(self):
         project = nbm.notebook_dir
         self.render(
             'projectdashboard.html', project=project,
-            base_project_url=u'/', base_kernel_url=u'/',
+            base_project_url=self.application.ipython_app.base_project_url,
+            base_kernel_url=self.application.ipython_app.base_kernel_url,
             read_only=self.read_only,
             logged_in=self.logged_in,
             login_available=self.login_available
@@ -255,7 +259,8 @@ def get(self):
         self.render(
             'notebook.html', project=project,
             notebook_id=notebook_id,
-            base_project_url=u'/', base_kernel_url=u'/',
+            base_project_url=self.application.ipython_app.base_project_url,
+            base_kernel_url=self.application.ipython_app.base_kernel_url,
             kill_kernel=False,
             read_only=False,
             logged_in=self.logged_in,
@@ -276,7 +281,8 @@ def get(self, notebook_id):
         self.render(
             'notebook.html', project=project,
             notebook_id=notebook_id,
-            base_project_url=u'/', base_kernel_url=u'/',
+            base_project_url=self.application.ipython_app.base_project_url,
+            base_kernel_url=self.application.ipython_app.base_kernel_url,
             kill_kernel=False,
             read_only=self.read_only,
             logged_in=self.logged_in,
@@ -297,7 +303,8 @@ def get(self, notebook_id):
         self.render(
             'printnotebook.html', project=project,
             notebook_id=notebook_id,
-            base_project_url=u'/', base_kernel_url=u'/',
+            base_project_url=self.application.ipython_app.base_project_url,
+            base_kernel_url=self.application.ipython_app.base_kernel_url,
             kill_kernel=False,
             read_only=self.read_only,
             logged_in=self.logged_in,
@@ -637,7 +644,8 @@ def get(self, notebook_id):
         self.render(
             'notebook.html', project=project,
             notebook_id=notebook_id,
-            base_project_url=u'/', base_kernel_url=u'/',
+            base_project_url=self.application.ipython_app.base_project_url,
+            base_kernel_url=self.application.ipython_app.base_kernel_url,
             kill_kernel=False,
             read_only=False,
             logged_in=self.logged_in,
diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py
--- a/IPython/frontend/html/notebook/notebookapp.py
+++ b/IPython/frontend/html/notebook/notebookapp.py
@@ -84,13 +84,24 @@
 ipython notebook --port=5555 --ip=*    # Listen on port 5555, all interfaces
 """
 
+#-----------------------------------------------------------------------------
+# Helper functions
+#-----------------------------------------------------------------------------
+
+def url_path_join(a,b):
+    if a.endswith('/') and b.startswith('/'):
+        return a[:-1]+b
+    else:
+        return a+b
+
 #-----------------------------------------------------------------------------
 # The Tornado web application
 #-----------------------------------------------------------------------------
 
 class NotebookWebApplication(web.Application):
 
-    def __init__(self, ipython_app, kernel_manager, notebook_manager, log, settings_overrides):
+    def __init__(self, ipython_app, kernel_manager, notebook_manager, log,
+                 base_project_url, settings_overrides):
         handlers = [
             (r"/", ProjectDashboardHandler),
             (r"/login", LoginHandler),
@@ -119,7 +130,14 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log, settings_
         # allow custom overrides for the tornado web app.
         settings.update(settings_overrides)
 
-        super(NotebookWebApplication, self).__init__(handlers, **settings)
+        # prepend base_project_url onto the patterns that we match
+        new_handlers = []
+        for handler in handlers:
+            pattern = url_path_join(base_project_url, handler[0])
+            new_handler = tuple([pattern]+list(handler[1:]))
+            new_handlers.append( new_handler )
+
+        super(NotebookWebApplication, self).__init__(new_handlers, **settings)
 
         self.kernel_manager = kernel_manager
         self.log = log
@@ -277,7 +295,15 @@ def _enable_mathjax_changed(self, name, old, new):
         """set mathjax url to empty if mathjax is disabled"""
         if not new:
             self.mathjax_url = u''
-    
+
+    base_project_url = Unicode('/', config=True,
+                               help='''The base URL for the notebook server''')
+    base_kernel_url = Unicode('/', config=True,
+                               help='''The base URL for the kernel server''')
+    websocket_host = Unicode("", config=True,
+        help="""The hostname for the websocket server."""
+    )
+
     mathjax_url = Unicode("", config=True,
         help="""The url for MathJax.js."""
     )
@@ -285,9 +311,11 @@ def _mathjax_url_default(self):
         if not self.enable_mathjax:
             return u''
         static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
+        static_url_prefix = self.webapp_settings.get("static_url_prefix",
+                                                     "/static/")
         if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
             self.log.info("Using local MathJax")
-            return u"/static/mathjax/MathJax.js"
+            return static_url_prefix+u"mathjax/MathJax.js"
         else:
             self.log.info("Using MathJax from CDN")
             return u"http://cdn.mathjax.org/mathjax/latest/MathJax.js"
@@ -331,7 +359,7 @@ def init_webapp(self):
         """initialize tornado webapp and httpserver"""
         self.web_app = NotebookWebApplication(
             self, self.kernel_manager, self.notebook_manager, self.log,
-            self.webapp_settings
+            self.base_project_url, self.webapp_settings
         )
         if self.certfile:
             ssl_options = dict(certfile=self.certfile)


Test patch: