Skip to content

Commit

Permalink
use DASH_PROXY env var to fix startup message
Browse files Browse the repository at this point in the history
and verify the proxy will see your app!
  • Loading branch information
alexcjohnson committed May 31, 2020
1 parent 316b662 commit c0c8c7f
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 20 deletions.
73 changes: 53 additions & 20 deletions dash/dash.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from __future__ import print_function

import itertools
import os
import random
import sys
import collections
import importlib
Expand All @@ -14,6 +12,7 @@
import mimetypes

from functools import wraps
from future.moves.urllib.parse import urlparse

import flask
from flask_compress import Compress
Expand All @@ -25,7 +24,7 @@
from .fingerprint import build_fingerprint, check_fingerprint
from .resources import Scripts, Css
from .development.base_component import ComponentRegistry
from .exceptions import PreventUpdate, InvalidResourceError
from .exceptions import PreventUpdate, InvalidResourceError, ProxyError
from .version import __version__
from ._configs import get_combined_config, pathname_configs
from ._utils import (
Expand Down Expand Up @@ -1332,7 +1331,8 @@ def enable_dev_tools(

if dev_tools.silence_routes_logging:
logging.getLogger("werkzeug").setLevel(logging.ERROR)
self.logger.setLevel(logging.INFO)

self.logger.setLevel(logging.INFO)

if dev_tools.hot_reload:
_reload = self._hot_reload
Expand Down Expand Up @@ -1444,6 +1444,7 @@ def run_server(
self,
host=os.getenv("HOST", "127.0.0.1"),
port=os.getenv("PORT", "8050"),
proxy=os.getenv("DASH_PROXY", None),
debug=False,
dev_tools_ui=None,
dev_tools_props_check=None,
Expand All @@ -1470,6 +1471,14 @@ def run_server(
env: ``PORT``
:type port: int
:param proxy: If this application will be served to a different URL
via a proxy configured outside of Python, you can list it here
as a string of the form ``"{input}::{output}"``, for example:
``"http://0.0.0.0:8050::https://my.domain.com"``
so that the startup message will display an accurate URL.
env: ``DASH_PROXY``
:type proxy: string
:param debug: Set Flask debug mode and enable dev tools.
env: ``DASH_DEBUG``
:type debug: bool
Expand Down Expand Up @@ -1550,25 +1559,49 @@ def run_server(
]
raise

if self._dev_tools.silence_routes_logging:
# Since it's silenced, the address doesn't show anymore.
# so we only see the "Running on" message once with hot reloading
# https://stackoverflow.com/a/57231282/9188800
if os.getenv("WERKZEUG_RUN_MAIN") != "true":
ssl_context = flask_run_options.get("ssl_context")
self.logger.info(
"Running on %s://%s:%s%s",
"https" if ssl_context else "http",
host,
port,
self.config.requests_pathname_prefix,
)
protocol = "https" if ssl_context else "http"
path = self.config.requests_pathname_prefix

if proxy:
served_url, proxied_url = map(urlparse, proxy.split("::"))

def verify_url_part(served_part, url_part, part_name):
if served_part != url_part:
raise ProxyError(
"""
{0}: {1} is incompatible with the proxy:
{3}
To see your app at {4},
you must use {0}: {2}
""".format(
part_name,
url_part,
served_part,
proxy,
proxied_url.geturl(),
)
)

verify_url_part(served_url.scheme, protocol, "protocol")
verify_url_part(served_url.hostname, host, "host")
verify_url_part(served_url.port, port, "port")

# Generate a debugger pin and log it to the screen.
debugger_pin = os.environ["WERKZEUG_DEBUG_PIN"] = "-".join(
itertools.chain(
"".join([str(random.randint(0, 9)) for _ in range(3)])
for _ in range(3)
display_url = (
proxied_url.scheme,
proxied_url.hostname,
(":{}".format(proxied_url.port) if proxied_url.port else ""),
path,
)
)
else:
display_url = (protocol, host, ":{}".format(port), path)

self.logger.info("Running on {}://{}{}{}".format(*display_url))

self.logger.info("Debugger PIN: %s", debugger_pin)
if not os.environ.get("FLASK_ENV"):
os.environ["FLASK_ENV"] = "development"

self.server.run(host=host, port=port, debug=debug, **flask_run_options)
4 changes: 4 additions & 0 deletions dash/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,7 @@ class MissingCallbackContextException(CallbackException):

class UnsupportedRelativePath(CallbackException):
pass


class ProxyError(DashException):
pass
60 changes: 60 additions & 0 deletions tests/unit/test_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,63 @@ def test_port_env_fail_range(empty_environ):
excinfo.exconly()
== "AssertionError: Expecting an integer from 1 to 65535, found port=65536"
)


def test_no_proxy_success(mocker, caplog, empty_environ):
app = Dash()

# mock out the run method so we don't actually start listening forever
mocker.patch.object(app.server, "run")

app.run_server(port=8787)

assert "Running on http://127.0.0.1:8787/\n" in caplog.text


@pytest.mark.parametrize(
"proxy, host, port, path",
[
("https://daash.plot.ly", "127.0.0.1", 8050, "/"),
("https://daaash.plot.ly", "0.0.0.0", 8050, "/a/b/c/"),
("https://daaaash.plot.ly", "127.0.0.1", 1234, "/"),
("http://go.away", "127.0.0.1", 8050, "/now/"),
("http://my.server.tv:8765", "0.0.0.0", 80, "/"),
],
)
def test_proxy_success(mocker, caplog, empty_environ, proxy, host, port, path):
proxystr = "http://{}:{}::{}".format(host, port, proxy)
app = Dash(url_base_pathname=path)
mocker.patch.object(app.server, "run")

app.run_server(proxy=proxystr, host=host, port=port)

assert "Running on {}{}\n".format(proxy, path) in caplog.text


def test_proxy_failure(mocker, empty_environ):
app = Dash()

# if the tests work we'll never get to server.run, but keep the mock
# in case something is amiss and we don't get an exception.
mocker.patch.object(app.server, "run")

with pytest.raises(_exc.ProxyError) as excinfo:
app.run_server(
proxy="https://127.0.0.1:8055::http://plot.ly", host="127.0.0.1", port=8055
)
assert "protocol: http is incompatible with the proxy" in excinfo.exconly()
assert "you must use protocol: https" in excinfo.exconly()

with pytest.raises(_exc.ProxyError) as excinfo:
app.run_server(
proxy="http://0.0.0.0:8055::http://plot.ly", host="127.0.0.1", port=8055
)
assert "host: 127.0.0.1 is incompatible with the proxy" in excinfo.exconly()
assert "you must use host: 0.0.0.0" in excinfo.exconly()

with pytest.raises(_exc.ProxyError) as excinfo:
app.run_server(
proxy="http://0.0.0.0:8155::http://plot.ly", host="0.0.0.0", port=8055
)
assert "port: 8055 is incompatible with the proxy" in excinfo.exconly()
assert "you must use port: 8155" in excinfo.exconly()

0 comments on commit c0c8c7f

Please sign in to comment.