Skip to content

Commit fa25768

Browse files
committed
URL Redirections (from 3xx responses) can now be disabled
1 parent 962b9e2 commit fa25768

4 files changed

Lines changed: 131 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ The use of environment variables allow you to provide overrides to default setti
516516
| `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.
517517
| `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`.
518518
| `APPRISE_INTERPRET_EMOJIS` | Override the Apprise `interpret-emojis` setting. This defaults to `none` (not set), but can be enforced to `no` or `yes`.
519+
| `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`.
519520
| `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)
520521
| `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.
521522
| `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.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Copyright (C) 2026 Chris Caron <lead2gold@gmail.com>
2+
# All rights reserved.
3+
#
4+
# This code is licensed under the MIT License.
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files(the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions :
12+
#
13+
# The above copyright notice and this permission notice shall be included in
14+
# all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
# THE SOFTWARE.
23+
import json
24+
from unittest import mock
25+
26+
import apprise
27+
from django.test import SimpleTestCase, override_settings
28+
29+
from ..forms import NotifyForm
30+
31+
# Grant access to our Notification Manager Singleton
32+
N_MGR = apprise.manager_plugins.NotificationManager()
33+
34+
35+
class NotifyWithRedirectTests(SimpleTestCase):
36+
"""
37+
Test that APPRISE_HTTP_REDIRECTS is wired correctly to AppriseAsset.
38+
39+
The apprise package installed in the test environment may pre-date the
40+
http_redirects attribute, so these tests mock AppriseAsset directly to
41+
verify that views.py passes the correct value rather than running the
42+
full notification stack.
43+
"""
44+
45+
def _make_asset_spy(self):
46+
"""Return a mock that patches AppriseAsset and captures its kwargs."""
47+
real_asset_cls = apprise.AppriseAsset
48+
49+
captured = {}
50+
51+
class SpyAsset(real_asset_cls):
52+
def __init__(self, **kwargs):
53+
# Capture what was passed before forwarding
54+
captured.update(kwargs)
55+
# Remove our new kwarg if the installed apprise doesn't
56+
# know about it yet so the real __init__ doesn't raise
57+
kwargs.pop("http_redirects", None)
58+
super().__init__(**kwargs)
59+
60+
return SpyAsset, captured
61+
62+
def test_stateful_notify_passes_redirects_to_asset(self):
63+
"""
64+
Stateful /notify/{key} passes APPRISE_HTTP_REDIRECTS to AppriseAsset.
65+
"""
66+
spy_cls, captured = self._make_asset_spy()
67+
68+
key = "test_redirect_stateful"
69+
70+
# Seed stateful config
71+
with mock.patch("apprise.AppriseAsset", spy_cls):
72+
self.client.post("/add/{}".format(key), {"urls": "json://user:pass@localhost"})
73+
74+
form_data = {"title": "Test", "body": "Body"}
75+
form = NotifyForm(data=form_data)
76+
assert form.is_valid()
77+
del form.cleaned_data["attachment"]
78+
79+
# Redirects disabled (default)
80+
spy_cls, captured = self._make_asset_spy()
81+
with mock.patch("apprise.AppriseAsset", spy_cls), override_settings(APPRISE_HTTP_REDIRECTS=False):
82+
self.client.post("/notify/{}".format(key), form.cleaned_data)
83+
assert captured.get("http_redirects") is False
84+
85+
# Redirects enabled
86+
spy_cls, captured = self._make_asset_spy()
87+
with mock.patch("apprise.AppriseAsset", spy_cls), override_settings(APPRISE_HTTP_REDIRECTS=True):
88+
self.client.post("/notify/{}".format(key), form.cleaned_data)
89+
assert captured.get("http_redirects") is True
90+
91+
def test_stateless_notify_passes_redirects_to_asset(self):
92+
"""
93+
Stateless /notify passes APPRISE_HTTP_REDIRECTS to AppriseAsset.
94+
"""
95+
json_data = {
96+
"urls": "json://user:pass@localhost",
97+
"title": "Test",
98+
"body": "Body",
99+
}
100+
101+
# Redirects disabled (default)
102+
spy_cls, captured = self._make_asset_spy()
103+
with mock.patch("apprise.AppriseAsset", spy_cls), override_settings(APPRISE_HTTP_REDIRECTS=False):
104+
self.client.post(
105+
"/notify",
106+
data=json.dumps(json_data),
107+
content_type="application/json",
108+
)
109+
assert captured.get("http_redirects") is False
110+
111+
# Redirects enabled
112+
spy_cls, captured = self._make_asset_spy()
113+
with mock.patch("apprise.AppriseAsset", spy_cls), override_settings(APPRISE_HTTP_REDIRECTS=True):
114+
self.client.post(
115+
"/notify",
116+
data=json.dumps(json_data),
117+
content_type="application/json",
118+
)
119+
assert captured.get("http_redirects") is True

apprise_api/api/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,8 @@ def post(self, request, key):
14821482
"storage_mode": settings.APPRISE_STORAGE_MODE,
14831483
# Emoji configuration (values are None, True, or False)
14841484
"interpret_emojis": settings.APPRISE_INTERPRET_EMOJIS,
1485+
# HTTP redirect behaviour (values are None, True, or False)
1486+
"http_redirects": settings.APPRISE_HTTP_REDIRECTS,
14851487
}
14861488

14871489
if body_format:
@@ -2048,6 +2050,8 @@ def post(self, request):
20482050
"plugin_paths": settings.APPRISE_PLUGIN_PATHS,
20492051
# Emoji configuration (values are None, True, or False)
20502052
"interpret_emojis": settings.APPRISE_INTERPRET_EMOJIS,
2053+
# HTTP redirect behaviour (values are None, True, or False)
2054+
"http_redirects": settings.APPRISE_HTTP_REDIRECTS,
20512055
}
20522056
if settings.APPRISE_STATELESS_STORAGE:
20532057
# Persistent Storage is allowed with Stateless queries

apprise_api/core/settings/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,10 @@
371371
"+",
372372
)
373373
)
374+
375+
# Allow HTTP Redirects override
376+
# By default Apprise follows HTTP 3xx redirects, matching the behaviour of
377+
# the underlying requests library. Set APPRISE_HTTP_REDIRECTS=no to disable
378+
# redirect following globally across all plugins without touching individual
379+
# URLs.
380+
APPRISE_HTTP_REDIRECTS = os.environ.get("APPRISE_HTTP_REDIRECTS", "yes")[0].lower() in ("a", "y", "1", "t", "e", "+")

0 commit comments

Comments
 (0)