Browse Source

Add rate limiting for Pushover notifications

pull/474/head
Pēteris Caune 4 years ago
parent
commit
c2bb4b31b5
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
5 changed files with 85 additions and 35 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +7
    -0
      hc/api/models.py
  3. +1
    -28
      hc/api/tests/test_notify.py
  4. +65
    -0
      hc/api/tests/test_notify_pushover.py
  5. +11
    -7
      hc/api/transports.py

+ 1
- 0
CHANGELOG.md View File

@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- Change Zulip onboarding, ask for the zuliprc file (#202)
- Add a section in Docs about running self-hosted instances
- Add experimental Dockerfile and docker-compose.yml
- Add rate limiting for Pushover notifications (6 notifications / user / minute)
## Bug Fixes
- Fix unwanted HTML escaping in SMS and WhatsApp notifications


+ 7
- 0
hc/api/models.py View File

@ -931,6 +931,13 @@ class TokenBucket(models.Model):
# 6 messages for a single recipient per minute:
return TokenBucket.authorize(value, 6, 60)
@staticmethod
def authorize_pushover(user_key):
salted_encoded = (user_key + settings.SECRET_KEY).encode()
value = "po-%s" % hashlib.sha1(salted_encoded).hexdigest()
# 6 messages for a single user key per minute:
return TokenBucket.authorize(value, 6, 60)
@staticmethod
def authorize_sudo_code(user):
value = "sudo-%d" % user.id


+ 1
- 28
hc/api/tests/test_notify.py View File

@ -2,7 +2,7 @@
from datetime import timedelta as td
import json
from unittest.mock import patch, Mock
from unittest.mock import patch
from django.core import mail
from django.utils.timezone import now
@ -492,33 +492,6 @@ class NotifyTestCase(BaseTestCase):
n = Notification.objects.first()
self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"')
@patch("hc.api.transports.requests.request")
def test_pushover(self, mock_post):
self._setup_data("po", "123|0")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertIn("DOWN", payload["title"])
@patch("hc.api.transports.requests.request")
def test_pushover_up_priority(self, mock_post):
self._setup_data("po", "123|0|2", status="up")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertIn("UP", payload["title"])
self.assertEqual(payload["priority"], 2)
self.assertIn("retry", payload)
self.assertIn("expire", payload)
@patch("hc.api.transports.requests.request")
def test_victorops(self, mock_post):
self._setup_data("victorops", "123")


+ 65
- 0
hc/api/tests/test_notify_pushover.py View File

@ -0,0 +1,65 @@
# coding: utf-8
from datetime import timedelta as td
from unittest.mock import patch
from django.test.utils import override_settings
from django.utils.timezone import now
from hc.api.models import Channel, Check, Notification, TokenBucket
from hc.test import BaseTestCase
class NotifyTestCase(BaseTestCase):
def _setup_data(self, value, status="down", email_verified=True):
self.check = Check(project=self.project)
self.check.status = status
self.check.last_ping = now() - td(minutes=61)
self.check.save()
self.channel = Channel(project=self.project)
self.channel.kind = "po"
self.channel.value = value
self.channel.email_verified = email_verified
self.channel.save()
self.channel.checks.add(self.check)
@patch("hc.api.transports.requests.request")
def test_pushover(self, mock_post):
self._setup_data("123|0")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertIn("DOWN", payload["title"])
@patch("hc.api.transports.requests.request")
def test_pushover_up_priority(self, mock_post):
self._setup_data("123|0|2", status="up")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertIn("UP", payload["title"])
self.assertEqual(payload["priority"], 2)
self.assertIn("retry", payload)
self.assertIn("expire", payload)
@override_settings(SECRET_KEY="test-secret")
@patch("hc.api.transports.requests.request")
def test_it_obeys_rate_limit(self, mock_post):
self._setup_data("123|0")
# "c0ca..." is sha1("123test-secret")
obj = TokenBucket(value="po-c0ca2a9774952af32cabf86453f69e442c4ed0eb")
obj.tokens = 0
obj.save()
self.channel.notify(self.check)
n = Notification.objects.first()
self.assertEqual(n.error, "Rate limit exceeded")

+ 11
- 7
hc/api/transports.py View File

@ -358,20 +358,24 @@ class Pushover(HttpTransport):
URL = "https://api.pushover.net/1/messages.json"
def notify(self, check):
others = self.checks().filter(status="down").exclude(code=check.code)
pieces = self.channel.value.split("|")
user_key, prio = pieces[0], pieces[1]
# The third element, if present, is the priority for "up" events
if len(pieces) == 3 and check.status == "up":
prio = pieces[2]
from hc.api.models import TokenBucket
if not TokenBucket.authorize_pushover(user_key):
return "Rate limit exceeded"
others = self.checks().filter(status="down").exclude(code=check.code)
# list() executes the query, to avoid DB access while
# rendering a template
ctx = {"check": check, "down_checks": list(others)}
text = tmpl("pushover_message.html", **ctx)
title = tmpl("pushover_title.html", **ctx)
pieces = self.channel.value.split("|")
user_key, prio = pieces[0], pieces[1]
# The third element, if present, is the priority for "up" events
if len(pieces) == 3 and check.status == "up":
prio = pieces[2]
payload = {
"token": settings.PUSHOVER_API_TOKEN,
"user": user_key,


Loading…
Cancel
Save