Compare commits

..

No commits in common. "master" and "v3.8.2" have entirely different histories.

10 changed files with 69 additions and 123 deletions

View file

@ -1,14 +1,6 @@
# Changelog
All notable changes to this project will be documented in this file.
## v3.9 - 2024-12-20
### Improvements
- Change the default value of ALLOWED_HOSTS from "*" to the domain part of SITE_ROOT
### Bug Fixes
- Fix fetchstatus.py (again) to handle SITE_ROOT with a path (#1108)
## v3.8.2 - 2024-12-19
### Improvements

View file

@ -1,7 +1,5 @@
FROM python:3.13.1-slim-bookworm AS builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
COPY requirements.txt /tmp
RUN \
apt-get update && \
@ -45,5 +43,5 @@ RUN mkdir /data && chown hc /data
USER hc
ENV USE_GZIP_MIDDLEWARE=True
HEALTHCHECK --start-period=20s --start-interval=5s --interval=60s --retries=1 CMD ./fetchstatus.py
HEALTHCHECK --interval=60s --retries=1 CMD ./fetchstatus.py
CMD [ "uwsgi", "/opt/healthchecks/docker/uwsgi.ini"]

View file

@ -20,8 +20,7 @@ settings.py uses for reading SITE_ROOT:
from __future__ import annotations
import os
from urllib.parse import urlparse
from urllib.request import Request, urlopen
from urllib.request import urlopen
# Read SITE_ROOT from environment, same as settings.py would do:
SITE_ROOT = os.getenv("SITE_ROOT", "http://localhost:8000")
@ -31,10 +30,8 @@ if os.path.exists("hc/local_settings.py"):
SITE_ROOT = getattr(local_settings, "SITE_ROOT", SITE_ROOT)
parsed_site_root = urlparse(SITE_ROOT.removesuffix("/"))
url = f"http://localhost:8000{parsed_site_root.path}/api/v3/status/"
headers = {"Host": parsed_site_root.netloc}
with urlopen(Request(url, headers=headers)) as response:
SITE_ROOT = SITE_ROOT.removesuffix("/")
with urlopen(f"{SITE_ROOT}/api/v3/status/") as response:
assert response.status == 200
print("Status OK")

View file

@ -5,46 +5,39 @@ from django.urls import path
from hc.accounts import views
urlpatterns = [
path("projects/add/", views.add_project, name="hc-add-project"),
path("projects/<uuid:code>/settings/", views.project, name="hc-project-settings"),
path("login/", views.login, name="hc-login"),
path("login/two_factor/", views.login_webauthn, name="hc-login-webauthn"),
path("login/two_factor/totp/", views.login_totp, name="hc-login-totp"),
path("logout/", views.logout, name="hc-logout"),
path("signup/csrf/", views.signup_csrf),
path("signup/", views.signup, name="hc-signup"),
path("login_link_sent/", views.login_link_sent, name="hc-login-link-sent"),
path(
"projects/<uuid:code>/remove/", views.remove_project, name="hc-remove-project"
),
path("accounts/login/", views.login, name="hc-login"),
path("accounts/login/two_factor/", views.login_webauthn, name="hc-login-webauthn"),
path("accounts/login/two_factor/totp/", views.login_totp, name="hc-login-totp"),
path("accounts/logout/", views.logout, name="hc-logout"),
path("accounts/signup/csrf/", views.signup_csrf),
path("accounts/signup/", views.signup, name="hc-signup"),
path("accounts/login_link_sent/", views.login_link_sent, name="hc-login-link-sent"),
path(
"accounts/check_token/<str:username>/<str:token>/",
"check_token/<str:username>/<str:token>/",
views.check_token,
name="hc-check-token",
),
path("accounts/profile/", views.profile, name="hc-profile"),
path("accounts/profile/appearance/", views.appearance, name="hc-appearance"),
path("profile/", views.profile, name="hc-profile"),
path("profile/appearance/", views.appearance, name="hc-appearance"),
path("profile/notifications/", views.notifications, name="hc-notifications"),
path("close/", views.close, name="hc-close"),
path(
"accounts/profile/notifications/", views.notifications, name="hc-notifications"
),
path("accounts/close/", views.close, name="hc-close"),
path(
"accounts/unsubscribe_reports/<str:signed_username>/",
"unsubscribe_reports/<str:signed_username>/",
views.unsubscribe_reports,
name="hc-unsubscribe-reports",
),
path("accounts/set_password/", views.set_password, name="hc-set-password"),
path("accounts/change_email/", views.change_email, name="hc-change-email"),
path("set_password/", views.set_password, name="hc-set-password"),
path("change_email/", views.change_email, name="hc-change-email"),
path(
"accounts/change_email/<str:signed_payload>/",
"change_email/<str:signed_payload>/",
views.change_email_verify,
name="hc-change-email-verify",
),
path("accounts/two_factor/webauthn/", views.add_webauthn, name="hc-add-webauthn"),
path("accounts/two_factor/totp/", views.add_totp, name="hc-add-totp"),
path("accounts/two_factor/totp/remove/", views.remove_totp, name="hc-remove-totp"),
path("two_factor/webauthn/", views.add_webauthn, name="hc-add-webauthn"),
path("two_factor/totp/", views.add_totp, name="hc-add-totp"),
path("two_factor/totp/remove/", views.remove_totp, name="hc-remove-totp"),
path(
"accounts/two_factor/<uuid:code>/remove/",
"two_factor/<uuid:code>/remove/",
views.remove_credential,
name="hc-remove-credential",
),

View file

@ -2,28 +2,26 @@ from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from urllib.parse import urlsplit
from django.apps import AppConfig
from django.conf import settings
from django.core.checks import Error, Warning, register
from django.http.request import split_domain_port, validate_host
class ApiConfig(AppConfig):
name = "hc.api"
@register() # W001, W002, W005, E002
@register() # W001, W002
def settings_check(
app_configs: Sequence[AppConfig] | None,
databases: Sequence[str] | None,
**kwargs: dict[str, Any],
) -> list[Error | Warning]:
items: list[Error | Warning] = []
) -> list[Warning]:
items = []
site_root_parts = urlsplit(settings.SITE_ROOT)
if not site_root_parts.scheme:
site_root_parts = settings.SITE_ROOT.split("://")
if site_root_parts[0] not in ("http", "https"):
items.append(
Warning(
"Invalid settings.SITE_ROOT value",
@ -32,16 +30,6 @@ def settings_check(
)
)
host, _ = split_domain_port(site_root_parts.netloc)
if site_root_parts.scheme and not validate_host(host, settings.ALLOWED_HOSTS):
items.append(
Error(
"The hostname in settings.SITE_ROOT is not found in settings.ALLOWED_HOSTS",
hint=f"Add '{host}' to settings.ALLOWED_HOSTS",
id="hc.api.E002",
)
)
if not settings.EMAIL_HOST:
items.append(
Warning(
@ -57,7 +45,7 @@ def settings_check(
Warning(
"settings.SECURE_PROXY_SSL_HEADER is not 2-element tuple",
hint="See https://healthchecks.io/docs/self_hosted_configuration/#SECURE_PROXY_SSL_HEADER",
id="hc.api.W005",
id="hc.api.W003",
)
)

View file

@ -9,15 +9,10 @@ from hc.test import BaseTestCase
@override_settings(EMAIL_HOST="localhost")
class SystemChecksCase(BaseTestCase):
@override_settings(SITE_ROOT="example.com")
def test_it_validates_site_root_syntax(self) -> None:
def test_it_validates_site_root(self) -> None:
ids = [item.id for item in settings_check(None, None)]
self.assertEqual(ids, ["hc.api.W001"])
@override_settings(SITE_ROOT="http://surprise.example.com")
def test_it_checks_site_root_host_is_present_in_allowed_hosts(self) -> None:
ids = [item.id for item in settings_check(None, None)]
self.assertEqual(ids, ["hc.api.E002"])
@override_settings(EMAIL_HOST=None)
def test_it_warns_about_missing_smtp_credentials(self) -> None:
ids = [item.id for item in settings_check(None, None)]
@ -26,4 +21,4 @@ class SystemChecksCase(BaseTestCase):
@override_settings(SECURE_PROXY_SSL_HEADER="abc")
def test_it_checks_secure_proxy_ssl_header_tupleness(self) -> None:
ids = [item.id for item in settings_check(None, None)]
self.assertEqual(ids, ["hc.api.W005"])
self.assertEqual(ids, ["hc.api.W003"])

View file

@ -13,7 +13,6 @@ from pathlib import Path
from typing import Any
from urllib.parse import urlparse
from django.http.request import split_domain_port
import django_stubs_ext
django_stubs_ext.monkeypatch()
@ -39,6 +38,7 @@ def envint(s: str, default: str) -> int | None:
SECRET_KEY = os.getenv("SECRET_KEY", "---")
METRICS_KEY = os.getenv("METRICS_KEY")
DEBUG = envbool("DEBUG", "True")
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",")
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "healthchecks@example.org")
SUPPORT_EMAIL = os.getenv("SUPPORT_EMAIL")
USE_PAYMENTS = envbool("USE_PAYMENTS", "False")
@ -210,14 +210,6 @@ if PING_BODY_LIMIT and PING_BODY_LIMIT > 2621440:
_site_root_parts = urlparse(SITE_ROOT)
LOGIN_URL = f"{_site_root_parts.path}/accounts/login/"
STATIC_URL = f"{_site_root_parts.path}/static/"
if v := os.getenv("ALLOWED_HOSTS"):
# If ALLOWED_HOSTS is set in environment, use it
ALLOWED_HOSTS = v.split(",")
else:
# Otherwise, populate it with the domain from SITE_ROOT
domain, _ = split_domain_port(_site_root_parts.netloc)
ALLOWED_HOSTS = [domain]
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "static-collected"
STATICFILES_FINDERS = (

View file

@ -14,7 +14,18 @@ if _path := urlparse(settings.SITE_ROOT).path.lstrip("/"):
urlpatterns = [
path(f"{prefix}admin/login/", accounts_views.login),
path(f"{prefix}admin/", admin.site.urls),
path(prefix, include("hc.accounts.urls")),
path(f"{prefix}accounts/", include("hc.accounts.urls")),
path(f"{prefix}projects/add/", accounts_views.add_project, name="hc-add-project"),
path(
f"{prefix}projects/<uuid:code>/settings/",
accounts_views.project,
name="hc-project-settings",
),
path(
f"{prefix}projects/<uuid:code>/remove/",
accounts_views.remove_project,
name="hc-remove-project",
),
path(prefix, include("hc.api.urls")),
path(prefix, include("hc.front.urls")),
path(prefix, include("hc.payments.urls")),

View file

@ -89,7 +89,7 @@ from environment variables. Below is a list of environment variables it reads an
<h2 id="ADMINS"><code>ADMINS</code></h2>
<p>Default: <code>""</code> (empty string)</p>
<p>A comma-separated list of email addresses to send code error notifications to.
<p>A comma-sepparated list of email addresses to send code error notifications to.
When <code>DEBUG=False</code>, Healthchecks will send the details of exceptions raised in the
request/response cycle to the listed addresses. Example:</p>
<div class="highlight"><pre><span></span><code><span class="na">ADMINS</span><span class="o">=</span><span class="s">alice@example.org,bob@example.org</span>
@ -98,16 +98,13 @@ request/response cycle to the listed addresses. Example:</p>
<p>Note: for error notifications to work, make sure you have also specified working
SMTP credentials in the <code>EMAIL_...</code> environment variables.</p>
<h2 id="ALLOWED_HOSTS"><code>ALLOWED_HOSTS</code></h2>
<p>Default: the domain part of <code>SITE_ROOT</code></p>
<p>The host/domain names that this site can serve. Healthchecks populates this setting
automatically with the domain part of <a href="#SITE_ROOT">SITE_ROOT</a>. You do not need
to set it unless you serve Healthchecks on more than one domain.</p>
<p>If you do serve the same Healthchecks instance on more than one domain, specify
them all in <code>ALLOWED_HOSTS</code>, separated by commas:</p>
<div class="highlight"><pre><span></span><code><span class="na">ALLOWED_HOSTS</span><span class="o">=</span><span class="s">first.example.org,second.example.org</span>
<p>Default: <code>*</code></p>
<p>The host/domain names that this site can serve. You can specify multiple domain names
by separating them with commas:</p>
<div class="highlight"><pre><span></span><code><span class="na">ALLOWED_HOSTS</span><span class="o">=</span><span class="s">my-hc.example.org,alternative-name.example.org</span>
</code></pre></div>
<p>Aside from the comma-separated syntax, this is a standard Django setting.
<p>Apart from the comma-separated syntax, this is a standard Django setting.
Read more about it in the
<a href="https://docs.djangoproject.com/en/5.1/ref/settings/#allowed-hosts">Django documentation</a>.</p>
<h2 id="APPRISE_ENABLED"><code>APPRISE_ENABLED</code></h2>
@ -518,8 +515,8 @@ other guarantee that it sets/strips this header appropriately.</p>
<p><strong>Note on using <code>local_settings.py</code>:</strong>
When Healthchecks reads settings from environment variables, it expects
<code>SECURE_PROXY_SSL_HEADER</code> to contain header name and value, separated with comma.
If you set <code>SECURE_PROXY_SSL_HEADER</code> in <code>local_settings.py</code>, it should be a tuple
with two elements instead:</p>
If you set <code>SECURE_PROXY_SSL_HEADER</code> in <code>local_settings.py</code>, it should be a
a tuple with two elements instead:</p>
<div class="highlight"><pre><span></span><code><span class="c1"># in local_settings.py</span>
<span class="na">SECURE_PROXY_SSL_HEADER</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">(&quot;HTTP_X_FORWARDED_PROTO&quot;, &quot;https&quot;)</span>
</code></pre></div>
@ -548,7 +545,7 @@ TCP socket.</p>
</code></pre></div>
<p>Healthchecks uses <a href="https://github.com/AsamK/signal-cli">signal-cli</a> to send Signal
notifications. Healthchecks interacts with signal-cli over UNIX or TCP socket (requires
notifications. Healthcecks interacts with signal-cli over UNIX or TCP socket (requires
signal-cli 0.10.0 or later).</p>
<p>To enable the Signal integration:</p>
<ul>
@ -595,14 +592,9 @@ its web UI and documentation.</p>
<h2 id="SITE_ROOT"><code>SITE_ROOT</code></h2>
<p>Default: <code>http://localhost:8000</code></p>
<p>The base URL of this Healthchecks instance. Healthchecks uses <code>SITE_ROOT</code> whenever
it needs to construct absolute URLs. Healthchecks also uses <code>SITE_ROOT</code> to set
several other settings, detailed below.</p>
<p>If the <a href="#ALLOWED_HOSTS">ALLOWED_HOSTS</a> setting is not set, Healthchecks
automatically populates it with the domain part of <code>SITE_ROOT</code>. Under typical scenarios
you can use the automatically populated value and do not need to set
<code>ALLOWED_HOSTS</code> yourself.</p>
it needs to construct absolute URLs.</p>
<p>If the SITE_ROOT contains a path (for example, <code>http://localhost:8000<b>/prefix</b></code>),
then Healthchecks automatically sets the following additional Django settings:</p>
then Healthchecks will automatically set the following additional Django settings:</p>
<ul>
<li><code>LOGIN_URL=<b>/prefix</b>/accounts/login/</code>. Required
for correct redirection to a log-in page when an unauthenticated user requests a
@ -616,8 +608,7 @@ setting, read more about it in
</ul>
<p><strong>On using <code>local_settings.py</code>:</strong> Healthchecks only sets the above additional settings
if you specify <code>SITE_ROOT</code> via an environment variable. If you instead specify it in
<code>local_settings.py</code>, you will also need to set <code>ALLOWED_HOSTS</code>, <code>LOGIN_URL</code>, and
<code>STATIC_URL</code> there.</p>
<code>local_settings.py</code>, you will also need to set <code>LOGIN_URL</code>and <code>STATIC_URL</code> there.</p>
<h2 id="SLACK_CLIENT_ID"><code>SLACK_CLIENT_ID</code></h2>
<p>Default: <code>None</code></p>
<p>The Slack Client ID, used by the Healthchecks integration for Slack.</p>

View file

@ -93,7 +93,7 @@ from environment variables. Below is a list of environment variables it reads an
Default: `""` (empty string)
A comma-separated list of email addresses to send code error notifications to.
A comma-sepparated list of email addresses to send code error notifications to.
When `DEBUG=False`, Healthchecks will send the details of exceptions raised in the
request/response cycle to the listed addresses. Example:
@ -106,20 +106,16 @@ SMTP credentials in the `EMAIL_...` environment variables.
## `ALLOWED_HOSTS` {: #ALLOWED_HOSTS }
Default: the domain part of `SITE_ROOT`
Default: `*`
The host/domain names that this site can serve. Healthchecks populates this setting
automatically with the domain part of [SITE_ROOT](#SITE_ROOT). You do not need
to set it unless you serve Healthchecks on more than one domain.
If you do serve the same Healthchecks instance on more than one domain, specify
them all in `ALLOWED_HOSTS`, separated by commas:
The host/domain names that this site can serve. You can specify multiple domain names
by separating them with commas:
```ini
ALLOWED_HOSTS=first.example.org,second.example.org
ALLOWED_HOSTS=my-hc.example.org,alternative-name.example.org
```
Aside from the comma-separated syntax, this is a standard Django setting.
Apart from the comma-separated syntax, this is a standard Django setting.
Read more about it in the
[Django documentation](https://docs.djangoproject.com/en/5.1/ref/settings/#allowed-hosts).
@ -738,8 +734,8 @@ other guarantee that it sets/strips this header appropriately.
**Note on using `local_settings.py`:**
When Healthchecks reads settings from environment variables, it expects
`SECURE_PROXY_SSL_HEADER` to contain header name and value, separated with comma.
If you set `SECURE_PROXY_SSL_HEADER` in `local_settings.py`, it should be a tuple
with two elements instead:
If you set `SECURE_PROXY_SSL_HEADER` in `local_settings.py`, it should be a
a tuple with two elements instead:
```ini
# in local_settings.py
@ -783,7 +779,7 @@ SIGNAL_CLI_SOCKET=example.org:7583
```
Healthchecks uses [signal-cli](https://github.com/AsamK/signal-cli) to send Signal
notifications. Healthchecks interacts with signal-cli over UNIX or TCP socket (requires
notifications. Healthcecks interacts with signal-cli over UNIX or TCP socket (requires
signal-cli 0.10.0 or later).
To enable the Signal integration:
@ -848,16 +844,10 @@ its web UI and documentation.
Default: `http://localhost:8000`
The base URL of this Healthchecks instance. Healthchecks uses `SITE_ROOT` whenever
it needs to construct absolute URLs. Healthchecks also uses `SITE_ROOT` to set
several other settings, detailed below.
If the [ALLOWED_HOSTS](#ALLOWED_HOSTS) setting is not set, Healthchecks
automatically populates it with the domain part of `SITE_ROOT`. Under typical scenarios
you can use the automatically populated value and do not need to set
`ALLOWED_HOSTS` yourself.
it needs to construct absolute URLs.
If the SITE_ROOT contains a path (for example, <code>http://localhost:8000<b>/prefix</b></code>),
then Healthchecks automatically sets the following additional Django settings:
then Healthchecks will automatically set the following additional Django settings:
* <code>LOGIN_URL=<b>/prefix</b>/accounts/login/</code>. Required
for correct redirection to a log-in page when an unauthenticated user requests a
@ -871,8 +861,7 @@ setting, read more about it in
**On using `local_settings.py`:** Healthchecks only sets the above additional settings
if you specify `SITE_ROOT` via an environment variable. If you instead specify it in
`local_settings.py`, you will also need to set `ALLOWED_HOSTS`, `LOGIN_URL`, and
`STATIC_URL` there.
`local_settings.py`, you will also need to set `LOGIN_URL`and `STATIC_URL` there.
## `SLACK_CLIENT_ID` {: #SLACK_CLIENT_ID }