Fix paths and URLs generated by API, and add note about config for proxying

This commit is contained in:
Paul Bienkowski 2021-11-27 22:40:06 +01:00
parent 9a13631097
commit 8dec4c8262
3 changed files with 68 additions and 10 deletions

View file

@ -8,7 +8,12 @@ from os.path import dirname, join, normpath, abspath, exists, isfile
from datetime import datetime, date from datetime import datetime, date
from sanic import Sanic, Blueprint 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.exceptions import Unauthorized, NotFound
from sanic_session import Session, InMemorySessionInterface 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) 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") @app.middleware("request")
async def inject_session(req): async def inject_session(req):
req.ctx._session_ctx = make_session() req.ctx._session_ctx = make_session()
@ -156,16 +198,16 @@ if INDEX_HTML and exists(INDEX_HTML):
@app.get("/config.json") @app.get("/config.json")
def get_frontend_config(req): 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 = { result = {
"basename": req.ctx.frontend_base_path,
**req.app.config.FRONTEND_CONFIG, **req.app.config.FRONTEND_CONFIG,
"apiUrl": f"{scheme}://{req.host}{base_path}api", "apiUrl": f"{req.ctx.api_url}/api",
"loginUrl": f"{scheme}://{req.host}{base_path}login", "loginUrl": f"{req.ctx.api_url}/login",
"obsMapSource": { "obsMapSource": {
"type": "vector", "type": "vector",
"tiles": [ "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("000", "{z}")
.replace("111", "{x}") .replace("111", "{x}")
.replace("222", "{y}") .replace("222", "{y}")
@ -177,14 +219,21 @@ if INDEX_HTML and exists(INDEX_HTML):
return json_response(result) return json_response(result)
with open(INDEX_HTML, "rt") as f:
index_file_contents = f.read()
@app.get("/<path:path>") @app.get("/<path:path>")
def get_frontend_static(req, path): def get_frontend_static(req, path):
print("++++++++++++++++++++++++++++++++++++++++++++++++", path)
if path.startswith("api/"): if path.startswith("api/"):
raise NotFound() raise NotFound()
file = join(app.config.FRONTEND_DIR, path) file = join(app.config.FRONTEND_DIR, path)
if not exists(file) or not path or not isfile(file): 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) return file_response(file)

View file

@ -38,13 +38,12 @@ async def login(req, next: str = None):
session["state"] = rndstr() session["state"] = rndstr()
session["nonce"] = rndstr() session["nonce"] = rndstr()
session["next"] = next session["next"] = next
scheme = 'https' if req.app.config.FRONTEND_HTTPS else req.scheme
args = { args = {
"client_id": client.client_id, "client_id": client.client_id,
"response_type": "code", "response_type": "code",
"scope": ["openid"], "scope": ["openid"],
"nonce": session["nonce"], "nonce": session["nonce"],
"redirect_uri": scheme + "://" + req.host + "/login/redirect", "redirect_uri": req.ctx.frontend_url + "/login/redirect",
"state": session["state"], "state": session["state"],
} }
@ -81,7 +80,9 @@ async def login_redirect(req):
email = userinfo.get("email") email = userinfo.get("email")
if email is None: 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() user = (await req.ctx.db.execute(select(User).where(User.sub == sub))).scalar()

View file

@ -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 configuration of the keycloak). Do not forget to generate a secure secret
string. 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 ### Build container and run them
```bash ```bash