diff --git a/README.md b/README.md index e0e83a9..0f56e1e 100644 --- a/README.md +++ b/README.md @@ -516,6 +516,7 @@ The use of environment variables allow you to provide overrides to default setti | `APPRISE_CONFIG_LOCK` | Locks down your API hosting so that you can no longer delete/update/access stateful information. Your configuration is still referenced when stateful calls are made to `/notify`. The idea of this switch is to allow someone to set their (Apprise) configuration up and then as an added security tactic, they may choose to lock their configuration down (in a read-only state). Those who use the Apprise CLI tool may still do it, however the `--config` (`-c`) switch will not successfully reference this access point anymore. You can however use the `apprise://` plugin without any problem ([see here for more details](https://appriseit.com/services/apprise_api/)). This defaults to `no` and can however be set to `yes` by simply defining the global variable as such. | `APPRISE_ADMIN` | Enables admin mode. This removes the distinction between users and admins and allows listing stored configuration keys (when `STATEFUL_MODE` is set to `simple`). This defaults to `no` and can be set to `yes`. | `APPRISE_INTERPRET_EMOJIS` | Override the Apprise `interpret-emojis` setting. This defaults to `none` (not set), but can be enforced to `no` or `yes`. +| `APPRISE_HTTP_REDIRECTS` | By default, Apprise follows HTTP 3xx redirects, matching the behaviour of the underlying requests library. Set to `no` to disable redirect following globally across all plugins without having to add `redirect=no` to every individual URL. Individual URLs can always override this with `?redirect=yes` or `?redirect=no` regardless of this setting. This defaults to `yes`. | `APPRISE_DENY_SERVICES` | A comma separated set of entries identifying what plugins to deny access to. You only need to identify one schema entry associated with a plugin to in turn disable all of it. Hence, if you wanted to disable the `glib` plugin, you do not need to additionally include `qt` as well since it's included as part of the (`dbus`) package; consequently specifying `qt` would in turn disable the `glib` module as well (another way to accomplish the same task). To exclude/disable more the one upstream service, simply specify additional entries separated by a `,` (comma) or ` ` (space). The `APPRISE_DENY_SERVICES` entries are ignored if the `APPRISE_ALLOW_SERVICES` is identified. By default, this is initialized to `windows, dbus, gnome, macosx, syslog` (blocking local actions from being issued inside of the docker container) | `APPRISE_ALLOW_SERVICES` | A comma separated set of entries identifying what plugins to allow access to. You may only use alpha-numeric characters as is the restriction of Apprise Schemas (schema://) anyway. To exclusively include more the one upstream service, simply specify additional entries separated by a `,` (comma) or ` ` (space). The `APPRISE_DENY_SERVICES` entries are ignored if the `APPRISE_ALLOW_SERVICES` is identified. | `APPRISE_API_ONLY` | This option will disable the entire access to web administration interface and only the API will be available for use. By default, this option will be set to `no`, or you can omit this variable if you wish. diff --git a/apprise_api/api/tests/test_json_urls.py b/apprise_api/api/tests/test_json_urls.py index 1d69946..bb70d45 100644 --- a/apprise_api/api/tests/test_json_urls.py +++ b/apprise_api/api/tests/test_json_urls.py @@ -182,6 +182,7 @@ def test_priority_tag_detail_shape(self): plain = tag_detail(_Tag("family")) prioritized = tag_detail(_Tag("family", priority=1, has_priority=True)) prioritized_string = tag_detail("2:email") + plain_string = tag_detail("alerts") assert plain == { "name": "family", @@ -201,6 +202,12 @@ def test_priority_tag_detail_shape(self): "token": "2:email", "exact": "2:email", } + assert plain_string == { + "name": "alerts", + "priority": 0, + "token": "alerts", + "exact": "0:alerts", + } assert tag_names(["2:email", _Tag("family")]) == {"email", "family"} def test_service_retry_falls_back_to_url_parameter(self): diff --git a/apprise_api/api/tests/test_redirects.py b/apprise_api/api/tests/test_redirects.py new file mode 100644 index 0000000..cafe9e9 --- /dev/null +++ b/apprise_api/api/tests/test_redirects.py @@ -0,0 +1,119 @@ +# Copyright (C) 2026 Chris Caron +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import json +from unittest import mock + +import apprise +from django.test import SimpleTestCase, override_settings + +from ..forms import NotifyForm + +# Grant access to our Notification Manager Singleton +N_MGR = apprise.manager_plugins.NotificationManager() + + +class NotifyWithRedirectTests(SimpleTestCase): + """ + Test that APPRISE_HTTP_REDIRECTS is wired correctly to AppriseAsset. + + The apprise package installed in the test environment may pre-date the + http_redirects attribute, so these tests mock AppriseAsset directly to + verify that views.py passes the correct value rather than running the + full notification stack. + """ + + def _make_asset_spy(self): + """Return a mock that patches AppriseAsset and captures its kwargs.""" + real_asset_cls = apprise.AppriseAsset + + captured = {} + + class SpyAsset(real_asset_cls): + def __init__(self, **kwargs): + # Capture what was passed before forwarding + captured.update(kwargs) + # Remove our new kwarg if the installed apprise doesn't + # know about it yet so the real __init__ doesn't raise + kwargs.pop("http_redirects", None) + super().__init__(**kwargs) + + return SpyAsset, captured + + def test_stateful_notify_passes_redirects_to_asset(self): + """ + Stateful /notify/{key} passes APPRISE_HTTP_REDIRECTS to AppriseAsset. + """ + spy_cls, captured = self._make_asset_spy() + + key = "test_redirect_stateful" + + # Seed stateful config + with mock.patch("apprise.AppriseAsset", spy_cls): + self.client.post("/add/{}".format(key), {"urls": "json://user:pass@localhost"}) + + form_data = {"title": "Test", "body": "Body"} + form = NotifyForm(data=form_data) + assert form.is_valid() + del form.cleaned_data["attachment"] + + # Redirects disabled (default) + spy_cls, captured = self._make_asset_spy() + with mock.patch("apprise.AppriseAsset", spy_cls), override_settings(APPRISE_HTTP_REDIRECTS=False): + self.client.post("/notify/{}".format(key), form.cleaned_data) + assert captured.get("http_redirects") is False + + # Redirects enabled + spy_cls, captured = self._make_asset_spy() + with mock.patch("apprise.AppriseAsset", spy_cls), override_settings(APPRISE_HTTP_REDIRECTS=True): + self.client.post("/notify/{}".format(key), form.cleaned_data) + assert captured.get("http_redirects") is True + + def test_stateless_notify_passes_redirects_to_asset(self): + """ + Stateless /notify passes APPRISE_HTTP_REDIRECTS to AppriseAsset. + """ + json_data = { + "urls": "json://user:pass@localhost", + "title": "Test", + "body": "Body", + } + + # Redirects disabled (default) + spy_cls, captured = self._make_asset_spy() + with mock.patch("apprise.AppriseAsset", spy_cls), override_settings(APPRISE_HTTP_REDIRECTS=False): + self.client.post( + "/notify", + data=json.dumps(json_data), + content_type="application/json", + ) + assert captured.get("http_redirects") is False + + # Redirects enabled + spy_cls, captured = self._make_asset_spy() + with mock.patch("apprise.AppriseAsset", spy_cls), override_settings(APPRISE_HTTP_REDIRECTS=True): + self.client.post( + "/notify", + data=json.dumps(json_data), + content_type="application/json", + ) + assert captured.get("http_redirects") is True diff --git a/apprise_api/api/views.py b/apprise_api/api/views.py index 265402f..4627fa5 100644 --- a/apprise_api/api/views.py +++ b/apprise_api/api/views.py @@ -1489,6 +1489,8 @@ def post(self, request, key): "storage_mode": settings.APPRISE_STORAGE_MODE, # Emoji configuration (values are None, True, or False) "interpret_emojis": settings.APPRISE_INTERPRET_EMOJIS, + # HTTP redirect behaviour (values are None, True, or False) + "http_redirects": settings.APPRISE_HTTP_REDIRECTS, } if body_format: @@ -2111,6 +2113,8 @@ def post(self, request): "plugin_paths": settings.APPRISE_PLUGIN_PATHS, # Emoji configuration (values are None, True, or False) "interpret_emojis": settings.APPRISE_INTERPRET_EMOJIS, + # HTTP redirect behaviour (values are None, True, or False) + "http_redirects": settings.APPRISE_HTTP_REDIRECTS, } if settings.APPRISE_STATELESS_STORAGE: # Persistent Storage is allowed with Stateless queries diff --git a/apprise_api/core/settings/__init__.py b/apprise_api/core/settings/__init__.py index 5341948..f3eaaac 100644 --- a/apprise_api/core/settings/__init__.py +++ b/apprise_api/core/settings/__init__.py @@ -378,3 +378,10 @@ "+", ) ) + +# Allow HTTP Redirects override +# By default Apprise follows HTTP 3xx redirects, matching the behaviour of +# the underlying requests library. Set APPRISE_HTTP_REDIRECTS=no to disable +# redirect following globally across all plugins without touching individual +# URLs. +APPRISE_HTTP_REDIRECTS = os.environ.get("APPRISE_HTTP_REDIRECTS", "yes")[0].lower() in ("a", "y", "1", "t", "e", "+") diff --git a/pyproject.toml b/pyproject.toml index f6663d0..6e71346 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ version = "1.4.1" authors = [{ name = "Chris Caron", email = "lead2gold@gmail.com" }] license = "MIT" dependencies = [ - "apprise == 1.10.0", + "apprise == 1.11.0", "django", "gevent", "gunicorn", diff --git a/requirements.txt b/requirements.txt index ac64679..37def51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ # apprise @ git+https://github.com/caronc/apprise@custom-tag-or-version ## 3. The below grabs our stable version (generally the best choice): -apprise == 1.10.0 +apprise == 1.11.0 ## Apprise API Minimum Requirements django