From 17d3d9deb7c5f401c47d35c008bea77c4cd2ba9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 7 Aug 2014 13:34:58 +0200 Subject: [PATCH] Reverse proxy wrapper now supports configuration of the http headers to utilize for determining prefix and scheme Introduced two new configuration settings, server.reverseProxy.prefixHeader and server.reverseProxy.schemeHeader to define the headers to evaluate for prefix and scheme to use respectively, also moved server.baseUrl and server.scheme to server.reverseProxy.fallbackPrefix and server.reverseProxy.fallbackScheme. Also fixed SockJS URI as generated in index.jinja to include BASE_URL. Should do what PR #507 intended, but with less code duplication. --- src/octoprint/server/__init__.py | 8 ++++- src/octoprint/server/util/__init__.py | 42 ++++++++++++++++--------- src/octoprint/settings.py | 44 ++++++++++++++++++++++++--- src/octoprint/templates/index.jinja2 | 2 +- 4 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 57e9cfa9..16e48321 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -153,7 +153,13 @@ class Server(): except AttributeError, e: logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName) - app.wsgi_app = util.ReverseProxied(app.wsgi_app) + app.wsgi_app = util.ReverseProxied( + app.wsgi_app, + settings().get(["server", "reverseProxy", "prefixHeader"]), + settings().get(["server", "reverseProxy", "schemeHeader"]), + settings().get(["server", "reverseProxy", "prefixFallback"]), + settings().get(["server", "reverseProxy", "prefixScheme"]) + ) app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV" loginManager = LoginManager() diff --git a/src/octoprint/server/util/__init__.py b/src/octoprint/server/util/__init__.py index 66e67301..4e964e3f 100644 --- a/src/octoprint/server/util/__init__.py +++ b/src/octoprint/server/util/__init__.py @@ -72,29 +72,43 @@ class ReverseProxied(object): :param app: the WSGI application :param header_script_name: the HTTP header in the wsgi environment from which to determine the prefix :param header_scheme: the HTTP header in the wsgi environment from which to determine the scheme + :param base_url: the prefix to use as fallback if headers are not set """ - def __init__(self, app, header_script_name="HTTP_X_SCRIPT_NAME", header_scheme="HTTP_X_SCHEME"): + def __init__(self, app, header_prefix="x-script-name", header_scheme="x-scheme", base_url="", scheme=""): self.app = app - self._header_script_name = header_script_name - self._header_scheme = header_scheme + + # headers for prefix & scheme, converted to conform to WSGI format + to_wsgi_format = lambda header: "HTTP_" + header.upper().replace("-", "_") + self._header_prefix = to_wsgi_format(header_prefix) + self._header_scheme = to_wsgi_format(header_scheme) + + # fallback prefix & scheme from config + self._fallback_prefix = base_url + self._fallback_scheme = scheme def __call__(self, environ, start_response): - script_name = environ.get(self._header_script_name, '') - if not script_name: - script_name = settings().get(["server", "baseUrl"]) + # determine prefix + prefix = environ.get(self._header_prefix, "") + if not prefix: + prefix = self._fallback_prefix - if script_name: - environ['SCRIPT_NAME'] = script_name - path_info = environ['PATH_INFO'] - if path_info.startswith(script_name): - environ['PATH_INFO'] = path_info[len(script_name):] + # rewrite SCRIPT_NAME and if necessary also PATH_INFO based on prefix + if prefix: + environ["SCRIPT_NAME"] = prefix + path_info = environ["PATH_INFO"] + if path_info.startswith(prefix): + environ["PATH_INFO"] = path_info[len(prefix):] - scheme = environ.get(self._header_scheme, '') + # determine scheme + scheme = environ.get(self._header_scheme, "") if not scheme: - scheme = settings().get(["server", "scheme"]) + scheme = self._fallback_scheme + # rewrite wsgi.url_scheme based on scheme if scheme: - environ['wsgi.url_scheme'] = scheme + environ["wsgi.url_scheme"] = scheme + + # call wrapped app with rewritten environment return self.app(environ, start_response) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index a31b0780..b607f97e 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -41,8 +41,12 @@ default_settings = { "host": "0.0.0.0", "port": 5000, "firstRun": True, - "baseUrl": "", - "scheme": "", + "reverseProxy": { + "prefixHeader": "X-Script-Name", + "schemeHeader": "X-Scheme", + "prefixFallback": "", + "schemeFallback": "" + }, "uploads": { "maxSize": 1 * 1024 * 1024 * 1024, # 1GB "nameSuffix": ".name", @@ -198,7 +202,7 @@ class Settings(object): if os.path.exists(self._configfile) and os.path.isfile(self._configfile): with open(self._configfile, "r") as f: self._config = yaml.safe_load(f) - # chamged from else to handle cases where the file exists, but is empty / 0 bytes + # changed from else to handle cases where the file exists, but is empty / 0 bytes if not self._config: self._config = {} @@ -209,6 +213,36 @@ class Settings(object): if not self._config: return + dirty = False + for migrate in (self._migrate_event_config, self._migrate_reverse_proxy_config): + dirty = migrate() or dirty + if dirty: + self.save(force=True) + + def _migrate_reverse_proxy_config(self): + if "server" in self._config.keys() and ("baseUrl" in self._config["server"] or "scheme" in self._config["server"]): + prefix = "" + if "baseUrl" in self._config["server"]: + prefix = self._config["server"]["baseUrl"] + del self._config["server"]["baseUrl"] + + scheme = "" + if "scheme" in self._config["server"]: + scheme = self._config["server"]["scheme"] + del self._config["server"]["scheme"] + + if not "reverseProxy" in self._config["server"] or not isinstance(self._config["server"]["reverseProxy"], dict): + self._config["server"]["reverseProxy"] = dict() + if prefix: + self._config["server"]["reverseProxy"]["prefixFallback"] = prefix + if scheme: + self._config["server"]["reverseProxy"]["schemeFallback"] = scheme + self._logger.info("Migrated reverse proxy configuration to new structure") + return True + else: + return False + + def _migrate_event_config(self): if "events" in self._config.keys() and ("gcodeCommandTrigger" in self._config["events"] or "systemCommandTrigger" in self._config["events"]): self._logger.info("Migrating config (event subscriptions)...") @@ -290,8 +324,10 @@ class Settings(object): newEvents["subscriptions"].append(newTrigger) self._config["events"] = newEvents - self.save(force=True) self._logger.info("Migrated %d event subscriptions to new format and structure" % len(newEvents["subscriptions"])) + return True + else: + return False def save(self, force=False): if not self._dirty and not force: diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index a7508680..54b75b0d 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -37,7 +37,7 @@ var CONFIG_GCODE_SIZE_THRESHOLD = {{ gcodeThreshold }}; var CONFIG_GCODE_MOBILE_SIZE_THRESHOLD = {{ gcodeMobileThreshold }}; - var SOCKJS_URI = window.location.protocol.slice(0, -1) + "://" + (window.document ? window.document.domain : window.location.hostname) + ":" + window.location.port + "/sockjs"; + var SOCKJS_URI = window.location.protocol.slice(0, -1) + "://" + (window.document ? window.document.domain : window.location.hostname) + ":" + window.location.port + BASEURL + "sockjs"; var SOCKJS_DEBUG = {% if debug -%} true; {% else %} false; {%- endif %} var UI_API_KEY = "{{ uiApiKey }}";