Repository: bokeh/bokeh

Issue:
Server should optionally enable xheader support 
Needed e.g. to run bokeh server behind SSL terminated reverse proxy. 



Discussion:

Resolving patch:
diff --git a/bokeh/command/subcommands/serve.py b/bokeh/command/subcommands/serve.py
--- a/bokeh/command/subcommands/serve.py
+++ b/bokeh/command/subcommands/serve.py
@@ -103,7 +103,7 @@
     http://localhost:{DEFAULT_PORT}/foobar/app_script
 
 If needed, Bokeh server can send keep-alive pings at a fixed interval.
-To configure this feature, set the --keep-alive option:
+To configure this feature, set the ``--keep-alive`` option:
 
 .. code-block:: sh
 
@@ -112,6 +112,22 @@
 The value is specified in milliseconds. The default keep-alive interval
 is 37 seconds. Give a value of 0 to disable keep-alive pings.
 
+To have the Bokeh server override the remote IP and URI scheme/protocol for
+all requests with ``X-Real-Ip``, ``X-Forwarded-For``, ``X-Scheme``,
+``X-Forwarded-Proto``  headers (if they are provided), set the
+``--use-xheaders`` option:
+
+.. code-block:: sh
+
+    bokeh serve app_script.py --use-xheaders
+
+This is typically needed when running a Bokeh server behind a reverse proxy
+that is SSL-terminated.
+
+.. warning::
+    It is not advised to set this option on a Bokeh server directly facing
+    the Internet.
+
 Session ID Options
 ~~~~~~~~~~~~~~~~~~
 
@@ -296,6 +312,11 @@ class Serve(Subcommand):
             default=None,
         )),
 
+        ('--use-xheaders', dict(
+            action='store_true',
+            help="Prefer X-headers for IP/protocol information",
+        )),
+
         ('--log-level', dict(
             metavar='LOG-LEVEL',
             action  = 'store',
@@ -338,7 +359,9 @@ def invoke(self, args):
                                                               'host',
                                                               'prefix',
                                                               'develop',
-                                                              'keep_alive_milliseconds']
+                                                              'keep_alive_milliseconds',
+                                                              'use_xheaders',
+                                                            ]
                           if getattr(args, key, None) is not None }
 
         server_kwargs['sign_sessions'] = settings.sign_sessions()
diff --git a/bokeh/server/server.py b/bokeh/server/server.py
--- a/bokeh/server/server.py
+++ b/bokeh/server/server.py
@@ -83,7 +83,7 @@ def __init__(self, applications, **kwargs):
         tornado_kwargs['extra_websocket_origins'] = _create_hosts_whitelist(kwargs.get('allow_websocket_origin', None), self._port)
 
         self._tornado = BokehTornado(self._applications, self.prefix, **tornado_kwargs)
-        self._http = HTTPServer(self._tornado)
+        self._http = HTTPServer(self._tornado, xheaders=kwargs.get('use_xheaders', False))
         self._address = None
         if 'address' in kwargs:
             self._address = kwargs['address']


Test patch:
diff --git a/bokeh/command/subcommands/tests/test_serve.py b/bokeh/command/subcommands/tests/test_serve.py
--- a/bokeh/command/subcommands/tests/test_serve.py
+++ b/bokeh/command/subcommands/tests/test_serve.py
@@ -82,6 +82,11 @@ def test_args():
             default=None,
         )),
 
+        ('--use-xheaders', dict(
+            action='store_true',
+            help="Prefer X-headers for IP/protocol information",
+        )),
+
         ('--log-level', dict(
             metavar='LOG-LEVEL',
             action  = 'store',
diff --git a/bokeh/server/tests/test_server.py b/bokeh/server/tests/test_server.py
--- a/bokeh/server/tests/test_server.py
+++ b/bokeh/server/tests/test_server.py
@@ -240,6 +240,11 @@ def autoload_url(server):
     return url(server) + \
         "autoload.js?bokeh-protocol-version=1.0&bokeh-autoload-element=foo"
 
+def test_use_xheaders():
+    application = Application()
+    with ManagedServerLoop(application, use_xheaders=True) as server:
+        assert server._http.xheaders == True
+
 def test__autocreate_session_autoload():
     application = Application()
     with ManagedServerLoop(application) as server: