diff --git a/bugsink/conf_templates/docker.py.template b/bugsink/conf_templates/docker.py.template index b5d93db..398e607 100644 --- a/bugsink/conf_templates/docker.py.template +++ b/bugsink/conf_templates/docker.py.template @@ -1,7 +1,7 @@ import os from urllib.parse import urlparse -from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood +from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name from bugsink.settings.default import * # noqa from bugsink.settings.default import DATABASES @@ -195,3 +195,11 @@ if os.getenv("FILE_EVENT_STORAGE_PATH"): "USE_FOR_WRITE": os.getenv("FILE_EVENT_STORAGE_USE_FOR_WRITE", "false").lower() in ("true", "1", "yes"), }, } + + +FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"]) +if FORCE_SCRIPT_NAME: + # "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME + # automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028 + # so we'll just be explicit about it. + STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/" diff --git a/bugsink/conf_templates/singleserver.py.template b/bugsink/conf_templates/singleserver.py.template index 4ec6f7b..dbcef0e 100644 --- a/bugsink/conf_templates/singleserver.py.template +++ b/bugsink/conf_templates/singleserver.py.template @@ -2,7 +2,7 @@ # This is the configuration for the singleserver setup for Bugsink in production. from bugsink.settings.default import * # noqa -from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood +from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "{{ secret_key }}" @@ -126,3 +126,10 @@ ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"]) # Alternatively, you can set the ALLOWED_HOSTS manually: # ALLOWED_HOSTS = ["{{ host }}"] + +FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"]) +if FORCE_SCRIPT_NAME: + # "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME + # automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028 + # so we'll just be explicit about it. + STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/" diff --git a/bugsink/context_processors.py b/bugsink/context_processors.py index 172560e..f6a9818 100644 --- a/bugsink/context_processors.py +++ b/bugsink/context_processors.py @@ -9,6 +9,7 @@ from django.urls import reverse from django.contrib.auth.models import AnonymousUser from django.db.utils import OperationalError from django.db.models import Sum +from django.urls import get_script_prefix from bugsink.app_settings import get_settings, CB_ANYBODY from bugsink.transaction import durable_atomic @@ -136,6 +137,7 @@ def useful_settings_processor(request): 'registration_enabled': get_settings().USER_REGISTRATION == CB_ANYBODY, 'app_settings': get_settings(), 'system_warnings': get_system_warnings, + 'script_prefix': get_script_prefix().rstrip("/"), # TODO why } diff --git a/bugsink/middleware.py b/bugsink/middleware.py index 7ebe665..926d242 100644 --- a/bugsink/middleware.py +++ b/bugsink/middleware.py @@ -8,6 +8,7 @@ from django.core.exceptions import SuspiciousOperation from django.utils.translation import get_supported_language_variant from django.utils.translation.trans_real import parse_accept_lang_header from django.utils import translation +from django.urls import get_script_prefix performance_logger = logging.getLogger("bugsink.performance.views") @@ -48,7 +49,7 @@ class LoginRequiredMiddleware: # we explicitly ignore the admin and accounts paths, and the api; we can always push this to a setting later for path in ["/admin", "/accounts", "/api"]: - if request.path.startswith(path): + if request.path.startswith(get_script_prefix().rstrip("/") + path): return None if getattr(view_func, 'login_exempt', False): diff --git a/bugsink/settings/default.py b/bugsink/settings/default.py index f4881fe..0e907a5 100644 --- a/bugsink/settings/default.py +++ b/bugsink/settings/default.py @@ -221,7 +221,8 @@ DATABASE_ROUTERS = ("bugsink.dbrouters.SeparateSnappeaDBRouter",) CONN_MAX_AGE = 0 -LOGIN_REDIRECT_URL = "/" +LOGIN_REDIRECT_URL = "home" +LOGIN_URL = "login" # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators diff --git a/bugsink/settings/development.py b/bugsink/settings/development.py index 36b1702..922483b 100644 --- a/bugsink/settings/development.py +++ b/bugsink/settings/development.py @@ -6,7 +6,7 @@ import os from django.utils._os import safe_join from sentry_sdk_extensions.transport import MoreLoudlyFailingTransport -from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood +from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name # no_bandit_expl: _development_ settings, we know that this is insecure; would fail to deploy in prod if (as configured) @@ -86,7 +86,7 @@ BUGSINK = { # "MAX_ENVELOPE_SIZE": 100 * _MEBIBYTE, # "MAX_ENVELOPE_COMPRESSED_SIZE": 20 * _MEBIBYTE, - "BASE_URL": "http://bugsink:8000", # no trailing slash + "BASE_URL": "http://bugsink:8000/foobar", # no trailing slash "SITE_TITLE": "Bugsink", # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company" # undocumented feature: this enables links to the admin interface in the header/footer. I'm not sure where the admin @@ -151,3 +151,11 @@ ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"]) # django-tailwind setting; the below allows for environment-variable overriding of the npm binary path. NPM_BIN_PATH = os.getenv("NPM_BIN_PATH", "npm") + + +FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"]) +if FORCE_SCRIPT_NAME: + # "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME + # automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028 + # so we'll just be explicit about it. + STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/" diff --git a/bugsink/utils.py b/bugsink/utils.py index 6a64e05..2aeb3ab 100644 --- a/bugsink/utils.py +++ b/bugsink/utils.py @@ -77,6 +77,27 @@ def deduce_allowed_hosts(base_url): return [url.hostname] + ["localhost", "127.0.0.1"] +def deduce_script_name(base_url): + """Extract the path prefix from BASE_URL for subpath hosting support.""" + + # On the matter of leading an trailing slashes: + # https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.13 (the CGI spec) -> SCRIPT_NAME must start with a / + # trailing slash: doesn't matter https://github.com/django/django/commit/a15a3e9148e9 (but normalized away) + # So: leading-but-no-trailing slash is what we want. + # Our usage in STATIC_URL is made consistent with that. + # Because BASE_URL is documented to be "no trailing slash", the below produces exactly what we want. + + try: + parsed_url = urlparse(base_url) + path = parsed_url.path + except Exception: + # maximize robustness here: one broken setting shouldn't break the deduction for others (the brokenness of + # BASE_URL will be manifested elsewhere more explicitly anyway) + return None + + return path if path not in (None, "", "/") else None + + # Note: the excessive string-matching in the below is intentional: # I'd rather have our error-handling code as simple as possible # instead of relying on all kinds of imports of Exception classes. diff --git a/issues/templates/issues/base.html b/issues/templates/issues/base.html index e4d1704..9fbd9db 100644 --- a/issues/templates/issues/base.html +++ b/issues/templates/issues/base.html @@ -109,13 +109,13 @@ {# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#}