From 8dec4c82628e33bab650fbdb58eead717302ddeb Mon Sep 17 00:00:00 2001 From: Paul Bienkowski Date: Sat, 27 Nov 2021 22:40:06 +0100 Subject: [PATCH] Fix paths and URLs generated by API, and add note about config for proxying --- api/obs/api/app.py | 63 ++++++++++++++++++++++++++++++++----- api/obs/api/routes/login.py | 7 +++-- deployment/README.md | 8 +++++ 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/api/obs/api/app.py b/api/obs/api/app.py index 08901a7..a4193ce 100644 --- a/api/obs/api/app.py +++ b/api/obs/api/app.py @@ -8,7 +8,12 @@ from os.path import dirname, join, normpath, abspath, exists, isfile from datetime import datetime, date from sanic import Sanic, Blueprint -from sanic.response import text, json as json_response, file as file_response +from sanic.response import ( + text, + json as json_response, + file as file_response, + html as html_response, +) from sanic.exceptions import Unauthorized, NotFound from sanic_session import Session, InMemorySessionInterface @@ -96,6 +101,43 @@ async def app_disconnect_db(app, loop): await app.ctx._db_engine_ctx.__aexit__(None, None, None) +def remove_right(l, r): + if l.endswith(r): + return l[: -len(r)] + return l + + +@app.middleware("request") +async def inject_urls(req): + if req.app.config.FRONTEND_HTTPS: + req.ctx.frontend_scheme = "https" + elif req.app.config.FRONTEND_URL: + req.ctx.frontend_scheme = ( + "http" if req.app.config.FRONTEND_URL.startswith("http://") else "https" + ) + else: + req.ctx.frontend_scheme = req.scheme + + req.ctx.api_scheme = req.ctx.frontend_scheme # just use the same for now + req.ctx.api_base_path = remove_right(req.server_path, req.path) + req.ctx.api_url = f"{req.ctx.frontend_scheme}://{req.host}{req.ctx.api_base_path}" + + if req.app.config.FRONTEND_URL: + req.ctx.frontend_base_path = "/" + urlparse( + req.app.config.FRONTEND_URL + ).path.strip("/") + req.ctx.frontend_url = req.app.config.FRONTEND_URL.rstrip("/") + elif app.config.FRONTEND_DIR: + req.ctx.frontend_base_path = req.ctx.api_base_path + req.ctx.frontend_url = req.ctx.api_url + + else: + req.ctx.frontend_base_path = "/" + req.ctx.frontend_url = ( + f"{req.ctx.frontend_scheme}://{req.host}{req.ctx.frontend_base_path}" + ) + + @app.middleware("request") async def inject_session(req): req.ctx._session_ctx = make_session() @@ -156,16 +198,16 @@ if INDEX_HTML and exists(INDEX_HTML): @app.get("/config.json") def get_frontend_config(req): - base_path = req.server_path.replace("config.json", "") - scheme = "https" if req.app.config.FRONTEND_HTTPS else req.scheme result = { + "basename": req.ctx.frontend_base_path, **req.app.config.FRONTEND_CONFIG, - "apiUrl": f"{scheme}://{req.host}{base_path}api", - "loginUrl": f"{scheme}://{req.host}{base_path}login", + "apiUrl": f"{req.ctx.api_url}/api", + "loginUrl": f"{req.ctx.api_url}/login", "obsMapSource": { "type": "vector", "tiles": [ - req.app.url_for("tiles", zoom="000", x="111", y="222.pbf") + req.ctx.api_url + + req.app.url_for("tiles", zoom="000", x="111", y="222.pbf") .replace("000", "{z}") .replace("111", "{x}") .replace("222", "{y}") @@ -177,14 +219,21 @@ if INDEX_HTML and exists(INDEX_HTML): return json_response(result) + with open(INDEX_HTML, "rt") as f: + index_file_contents = f.read() + @app.get("/") def get_frontend_static(req, path): + print("++++++++++++++++++++++++++++++++++++++++++++++++", path) if path.startswith("api/"): raise NotFound() file = join(app.config.FRONTEND_DIR, path) if not exists(file) or not path or not isfile(file): - file = INDEX_HTML + return html_response( + index_file_contents.replace("__BASE_HREF__", req.ctx.frontend_url + "/") + ) + return file_response(file) diff --git a/api/obs/api/routes/login.py b/api/obs/api/routes/login.py index 07d6682..dbc5463 100644 --- a/api/obs/api/routes/login.py +++ b/api/obs/api/routes/login.py @@ -38,13 +38,12 @@ async def login(req, next: str = None): session["state"] = rndstr() session["nonce"] = rndstr() session["next"] = next - scheme = 'https' if req.app.config.FRONTEND_HTTPS else req.scheme args = { "client_id": client.client_id, "response_type": "code", "scope": ["openid"], "nonce": session["nonce"], - "redirect_uri": scheme + "://" + req.host + "/login/redirect", + "redirect_uri": req.ctx.frontend_url + "/login/redirect", "state": session["state"], } @@ -81,7 +80,9 @@ async def login_redirect(req): email = userinfo.get("email") if email is None: - raise ValueError("user has no email set, please configure keycloak to require emails") + raise ValueError( + "user has no email set, please configure keycloak to require emails" + ) user = (await req.ctx.db.execute(select(User).where(User.sub == sub))).scalar() diff --git a/deployment/README.md b/deployment/README.md index 666d10e..442b7d0 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -78,6 +78,14 @@ Then edit `config/config.py` to your heart's content (and matching the configuration of the keycloak). Do not forget to generate a secure secret string. +Also set `PROXIES_COUNT = 1` in your config, even if that option is not +included in the example file. Read the [sanic +docs](https://sanic.readthedocs.io/en/v20.12.3/sanic/config.html) for why this +needs to be done. If your reverse proxy supports it, you can also use a +forwarded secret to secure your proxy target from spoofing. This is not +required if your application server does not listen on a public interface, but +it is recommended anyway, if possible. + ### Build container and run them ```bash