mirror of
https://github.com/caronc/apprise-api.git
synced 2024-10-05 01:31:58 +02:00
Server-side handling of new apprise:// schema (#56)
This commit is contained in:
parent
2943bce981
commit
a6fe49e1e1
@ -202,6 +202,7 @@ The use of environment variables allow you to provide over-rides to default sett
|
||||
|
||||
| `SECRET_KEY` | A Django variable acting as a *salt* for most things that require security. This API uses it for the hash sequences when writing the configuration files to disk (`hash` mode only).
|
||||
| `ALLOWED_HOSTS` | A list of strings representing the host/domain names that this API can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. By default this is set to `*` allowing any host. Use space to delimit more than one host.
|
||||
| `APPRISE_RECURSION_MAX` | This defines the number of times one Apprise API Server can (recursively) call another. This is to both support and mitigate abuse through [the `apprise://` schema](https://github.com/caronc/apprise/wiki/Notify_apprise_api) for those who choose to use it. When leveraged properly, you can increase this (recursion max) value and successfully load balance the handling of many notification requests through many additional API Servers. By default this value is set to `1` (one).
|
||||
| `BASE_URL` | Those who are hosting the API behind a proxy that requires a subpath to gain access to this API should specify this path here as well. By default this is not set at all.
|
||||
| `LOG_LEVEL` | Adjust the log level to the console. Possible values are `CRITICAL`, `ERROR`, `WARNING`, `INFO`, and `DEBUG`.
|
||||
| `DEBUG` | This defaults to `no` and can however be set to `yes` by simply defining the global variable as such.
|
||||
|
@ -492,3 +492,99 @@ class NotifyTests(SimpleTestCase):
|
||||
|
||||
# nothing was changed
|
||||
assert apprise.plugins.SCHEMA_MAP['mailto'].enabled is True
|
||||
|
||||
@override_settings(APPRISE_RECURSION_MAX=1)
|
||||
@patch('apprise.Apprise.notify')
|
||||
def test_stateful_notify_recursion(self, mock_notify):
|
||||
"""
|
||||
Test recursion an id header details as part of post
|
||||
"""
|
||||
|
||||
# Set our return value
|
||||
mock_notify.return_value = True
|
||||
|
||||
# our key to use
|
||||
key = 'test_stateful_notify_recursion'
|
||||
|
||||
# Add some content
|
||||
response = self.client.post(
|
||||
'/add/{}'.format(key),
|
||||
{'urls': 'mailto://user:pass@yahoo.ca'})
|
||||
assert response.status_code == 200
|
||||
|
||||
# Form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
}
|
||||
|
||||
# Define our headers we plan to pass along with our request
|
||||
headers = {
|
||||
'HTTP_X-APPRISE-ID': 'abc123',
|
||||
'HTTP_X-APPRISE-RECURSION-COUNT': str(1),
|
||||
}
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), data=form_data, **headers)
|
||||
assert response.status_code == 200
|
||||
assert mock_notify.call_count == 1
|
||||
|
||||
headers = {
|
||||
# Header specified but with whitespace
|
||||
'HTTP_X-APPRISE-ID': ' ',
|
||||
# No Recursion value specified
|
||||
}
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Recursion limit reached
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), data=form_data, **headers)
|
||||
assert response.status_code == 200
|
||||
assert mock_notify.call_count == 1
|
||||
|
||||
headers = {
|
||||
'HTTP_X-APPRISE-ID': 'abc123',
|
||||
# Recursion Limit hit
|
||||
'HTTP_X-APPRISE-RECURSION-COUNT': str(2),
|
||||
}
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Recursion limit reached
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), data=form_data, **headers)
|
||||
assert response.status_code == 406
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
headers = {
|
||||
'HTTP_X-APPRISE-ID': 'abc123',
|
||||
# Negative recursion value (bad request)
|
||||
'HTTP_X-APPRISE-RECURSION-COUNT': str(-1),
|
||||
}
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# invalid recursion specified
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), data=form_data, **headers)
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
headers = {
|
||||
'HTTP_X-APPRISE-ID': 'abc123',
|
||||
# Invalid recursion value (bad request)
|
||||
'HTTP_X-APPRISE-RECURSION-COUNT': 'invalid',
|
||||
}
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# invalid recursion specified
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), data=form_data, **headers)
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
@ -113,6 +113,93 @@ class StatelessNotifyTests(SimpleTestCase):
|
||||
assert response.status_code == 424
|
||||
assert mock_notify.call_count == 2
|
||||
|
||||
@override_settings(APPRISE_RECURSION_MAX=1)
|
||||
@patch('apprise.Apprise.notify')
|
||||
def test_stateless_notify_recursion(self, mock_notify):
|
||||
"""
|
||||
Test recursion an id header details as part of post
|
||||
"""
|
||||
|
||||
# Set our return value
|
||||
mock_notify.return_value = True
|
||||
|
||||
headers = {
|
||||
'HTTP_X-APPRISE-ID': 'abc123',
|
||||
'HTTP_X-APPRISE-RECURSION-COUNT': str(1),
|
||||
}
|
||||
|
||||
# Preare our form data (without url specified)
|
||||
# content will fall back to default configuration
|
||||
form_data = {
|
||||
'urls': 'mailto://user:pass@hotmail.com',
|
||||
'body': 'test notifiction',
|
||||
}
|
||||
|
||||
# At a minimum 'body' is requred
|
||||
form = NotifyByUrlForm(data=form_data)
|
||||
assert form.is_valid()
|
||||
|
||||
# recursion value is within correct limits
|
||||
response = self.client.post('/notify', form.cleaned_data, **headers)
|
||||
assert response.status_code == 200
|
||||
assert mock_notify.call_count == 1
|
||||
|
||||
headers = {
|
||||
# Header specified but with whitespace
|
||||
'HTTP_X-APPRISE-ID': ' ',
|
||||
# No Recursion value specified
|
||||
}
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Recursion limit reached
|
||||
response = self.client.post('/notify', form.cleaned_data, **headers)
|
||||
assert response.status_code == 200
|
||||
assert mock_notify.call_count == 1
|
||||
|
||||
headers = {
|
||||
'HTTP_X-APPRISE-ID': 'abc123',
|
||||
# Recursion Limit hit
|
||||
'HTTP_X-APPRISE-RECURSION-COUNT': str(2),
|
||||
}
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Recursion limit reached
|
||||
response = self.client.post('/notify', form.cleaned_data, **headers)
|
||||
assert response.status_code == 406
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
headers = {
|
||||
'HTTP_X-APPRISE-ID': 'abc123',
|
||||
# Negative recursion value (bad request)
|
||||
'HTTP_X-APPRISE-RECURSION-COUNT': str(-1),
|
||||
}
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# invalid recursion specified
|
||||
response = self.client.post('/notify', form.cleaned_data, **headers)
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
headers = {
|
||||
'HTTP_X-APPRISE-ID': 'abc123',
|
||||
# Invalid recursion value (bad request)
|
||||
'HTTP_X-APPRISE-RECURSION-COUNT': 'invalid',
|
||||
}
|
||||
|
||||
# Reset our count
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# invalid recursion specified
|
||||
response = self.client.post('/notify', form.cleaned_data, **headers)
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
@override_settings(APPRISE_STATELESS_URLS="mailto://user:pass@localhost")
|
||||
@patch('apprise.Apprise.notify')
|
||||
def test_notify_default_urls(self, mock_notify):
|
||||
|
@ -89,6 +89,7 @@ class ResponseCode(object):
|
||||
no_access = 403
|
||||
not_found = 404
|
||||
method_not_allowed = 405
|
||||
method_not_accepted = 406
|
||||
failed_dependency = 424
|
||||
internal_server_error = 500
|
||||
|
||||
@ -623,6 +624,41 @@ class NotifyView(View):
|
||||
'APPRISE_DENY_SERVICES plugin %s:// was not found -'
|
||||
' ignoring.', name)
|
||||
|
||||
# Prepare our keyword arguments (to be passed into an AppriseAsset
|
||||
# object)
|
||||
kwargs = {}
|
||||
|
||||
if body_format:
|
||||
# Store our defined body format
|
||||
kwargs['body_format'] = body_format
|
||||
|
||||
# Acquire our recursion count (if defined)
|
||||
try:
|
||||
recursion = \
|
||||
int(request.headers.get('X-Apprise-Recursion-Count', 0))
|
||||
|
||||
if recursion < 0:
|
||||
# We do not accept negative numbers
|
||||
raise TypeError("Invalid Recursion Value")
|
||||
|
||||
if recursion > settings.APPRISE_RECURSION_MAX:
|
||||
return HttpResponse(
|
||||
_('The recursion limit has been reached.'),
|
||||
status=ResponseCode.method_not_accepted)
|
||||
|
||||
# Store our recursion value for our AppriseAsset() initialization
|
||||
kwargs['_recursion'] = recursion
|
||||
|
||||
except (TypeError, ValueError):
|
||||
return HttpResponse(
|
||||
_('An invalid recursion value was specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
# Acquire our unique identifier (if defined)
|
||||
uid = request.headers.get('X-Apprise-ID', '').strip()
|
||||
if uid:
|
||||
kwargs['_uid'] = uid
|
||||
|
||||
# Prepare ourselves a default Asset
|
||||
asset = None if not body_format else \
|
||||
apprise.AppriseAsset(body_format=body_format)
|
||||
@ -785,6 +821,41 @@ class StatelessNotifyView(View):
|
||||
_('An invalid (body) format was specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
# Prepare our keyword arguments (to be passed into an AppriseAsset
|
||||
# object)
|
||||
kwargs = {}
|
||||
|
||||
if body_format:
|
||||
# Store our defined body format
|
||||
kwargs['body_format'] = body_format
|
||||
|
||||
# Acquire our recursion count (if defined)
|
||||
try:
|
||||
recursion = \
|
||||
int(request.headers.get('X-Apprise-Recursion-Count', 0))
|
||||
|
||||
if recursion < 0:
|
||||
# We do not accept negative numbers
|
||||
raise TypeError("Invalid Recursion Value")
|
||||
|
||||
if recursion > settings.APPRISE_RECURSION_MAX:
|
||||
return HttpResponse(
|
||||
_('The recursion limit has been reached.'),
|
||||
status=ResponseCode.method_not_accepted)
|
||||
|
||||
# Store our recursion value for our AppriseAsset() initialization
|
||||
kwargs['_recursion'] = recursion
|
||||
|
||||
except (TypeError, ValueError):
|
||||
return HttpResponse(
|
||||
_('An invalid recursion value was specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
# Acquire our unique identifier (if defined)
|
||||
uid = request.headers.get('X-Apprise-ID', '').strip()
|
||||
if uid:
|
||||
kwargs['_uid'] = uid
|
||||
|
||||
# Prepare ourselves a default Asset
|
||||
asset = None if not body_format else \
|
||||
apprise.AppriseAsset(body_format=body_format)
|
||||
|
@ -163,3 +163,9 @@ APPRISE_DENY_SERVICES = os.environ.get('APPRISE_DENY_SERVICES', ','.join((
|
||||
# - anything not identified here is denied/disabled)
|
||||
# - this list trumps the APPRISE_DENY_SERVICES identified above
|
||||
APPRISE_ALLOW_SERVICES = os.environ.get('APPRISE_ALLOW_SERVICES', '')
|
||||
|
||||
# Define the number of recursive calls your system will allow users to make
|
||||
# The idea here is to prevent people from defining apprise:// URL's triggering
|
||||
# a call to the same server again, and again and again. By default we allow
|
||||
# 1 level of recursion
|
||||
APPRISE_RECURSION_MAX = int(os.environ.get('APPRISE_RECURSION_MAX', 1))
|
||||
|
@ -1,5 +1,4 @@
|
||||
django
|
||||
cryptography == 3.3.2
|
||||
apprise == 0.9.6
|
||||
|
||||
# 3rd party service support
|
||||
|
Loading…
Reference in New Issue
Block a user