Skip to content

Commit

Permalink
Merge pull request #8 from iurisilvio/webhook
Browse files Browse the repository at this point in the history
Webhooks support
  • Loading branch information
iurisilvio committed Dec 11, 2014
2 parents 5d3a1fe + ed9f361 commit 2703680
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 6 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,43 @@ The `@pusher.channel_data` gives you a way to set other values. If a `user_id`
key is returned, it overrides the default `user_id`.

```python
from flask.ext.login import current_user

@pusher.channel_data
def pusher_channel_data(channel_name, socket_id):
return {
"name": current_user.name
}
```

Pusher webhooks
---------------

Pusher has webhooks to send websocket events to your server.

Flask-Pusher create the routes to handle these webhooks and validate the headers `X-Pusher-Key` and `X-Pusher-Signature`.

```python
from flask import request

@pusher.webhooks.channel_existence
def channel_existence_webhook():
print request.json

@pusher.webhooks.presence
def presence_webhook():
print request.json

@pusher.webhooks.client
def client_webhook():
print request.json
```

The JSON request is documented in Pusher docs: http://pusher.com/docs/webhooks

These webhooks routes are mounted in `/pusher/events/channel_existence`, `/pusher/events/presence` and `/pusher/events/client`. Configure your Pusher app to send webhooks to these routes.


Disclaimer
----------
This project is not affiliated with Pusher or Flask.
69 changes: 64 additions & 5 deletions flask_pusher.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import hashlib
import hmac

from flask import Blueprint, current_app, request, jsonify, abort

import pusher as _pusher
Expand All @@ -9,6 +12,8 @@ def __init__(self, app=None):
self.app = app
self._auth_handler = None
self._channel_data_handler = None
self._blueprint = Blueprint('pusher', __name__, url_prefix="/pusher")
self.webhooks = Webhooks(self)

if app is not None:
self.init_app(app)
Expand All @@ -29,8 +34,8 @@ def init_app(self, app):
port=app.config["PUSHER_PORT"],
encoder=getattr(app, "json_encoder", None))

bp = self._make_blueprint()
app.register_blueprint(bp)
self._make_blueprint()
app.register_blueprint(self._blueprint)

if not hasattr(app, "extensions"):
app.extensions = {}
Expand All @@ -49,9 +54,9 @@ def channel_data(self, handler):
return handler

def _make_blueprint(self):
bp = Blueprint('pusher', __name__)
bp = self._blueprint

@bp.route("/pusher/auth", methods=["POST"])
@bp.route("/auth", methods=["POST"])
def auth():
if not self._auth_handler:
abort(403)
Expand Down Expand Up @@ -82,4 +87,58 @@ def pusher_data():
"PUSHER_KEY": self.client.key
}

return bp
def _sign(self, message):
return hmac.new(self.client.secret, message,
hashlib.sha256).hexdigest()


class Webhooks(object):

CHANNEL_EXISTENCE_EVENT = "channel_existence"
PRESENCE_EVENT = "presence"
CLIENT_EVENT = "client"

def __init__(self, pusher):
self.pusher = pusher
self._handlers = {}
self._register(self.CHANNEL_EXISTENCE_EVENT)
self._register(self.PRESENCE_EVENT)
self._register(self.CLIENT_EVENT)

def channel_existence(self, func):
self._handlers[self.CHANNEL_EXISTENCE_EVENT] = func
return func

def presence(self, func):
self._handlers[self.PRESENCE_EVENT] = func
return func

def client(self, func):
self._handlers[self.CLIENT_EVENT] = func
return func

def _register(self, event):
def route():
func = self._handlers.get(event)
if not func:
abort(404)
self._validate()
func()
return "OK", 200

rule = "/events/%s" % event
name = "%s_event" % event
self.pusher._blueprint.add_url_rule(rule, name, route,
methods=["POST"])

def _validate(self):
pusher_key = request.headers.get("X-Pusher-Key")
if pusher_key != self.pusher.client.key:
# invalid pusher key
abort(403)

webhook_signature = request.headers.get("X-Pusher-Signature")
expected_signature = self.pusher._sign(request.data)
if webhook_signature != expected_signature:
# invalid signature
abort(403)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='Flask-Pusher',
version='0.2',
version='0.3dev',
url='https://www.github.com/iurisilvio/Flask-Pusher',
license='MIT',
author='Iuri de Silvio',
Expand Down
72 changes: 72 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,77 @@ def test_invalid_channel(self):
self.assertEqual(404, response.status_code)


class PusherWebhookTest(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
self.app.debug = True
self.app.config["PUSHER_KEY"] = "KEY"
self.app.config["PUSHER_SECRET"] = "SUPERSECRET"
self.pusher = Pusher(self.app)
self.client = self.app.test_client()
self._called = False

@self.pusher.webhooks.client
def c():
self._called = True

def test_no_webhook(self):
with self.app.test_request_context():
url = url_for("pusher.presence_event")
response = self.client.post(url)
self.assertEqual(404, response.status_code)
self.assertFalse(self._called)

def test_without_key_forbidden(self):
with self.app.test_request_context():
url = url_for("pusher.client_event")
response = self.client.post(url)
self.assertEqual(403, response.status_code)
self.assertFalse(self._called)

def test_invalid_key_forbidden(self):
with self.app.test_request_context():
url = url_for("pusher.client_event")
response = self.client.post(url, headers={
"Content-Type": "application/json",
"X-Pusher-Key": "meh"
})
self.assertEqual(403, response.status_code)
self.assertFalse(self._called)

def test_valid_key_forbidden_without_signature(self):
with self.app.test_request_context():
url = url_for("pusher.client_event")
response = self.client.post(url, headers={
"Content-Type": "application/json",
"X-Pusher-Key": "KEY"
})
self.assertEqual(403, response.status_code)
self.assertFalse(self._called)

def test_invalid_signature(self):
with self.app.test_request_context():
url = url_for("pusher.client_event")
response = self.client.post(url, headers={
"Content-Type": "application/json",
"X-Pusher-Key": "KEY",
"X-Pusher-Signature": "x"
})
self.assertEqual(403, response.status_code)
self.assertFalse(self._called)

def test_valid_signature(self):
data = '{"a": "b"}'
with self.app.test_request_context():
url = url_for("pusher.client_event")
signature = self.pusher._sign(data)
response = self.client.post(url, data=data, headers={
"Content-Type": "application/json",
"X-Pusher-Key": "KEY",
"X-Pusher-Signature": signature
})
self.assertEqual(200, response.status_code)
self.assertTrue(self._called)

if __name__ == '__main__':
unittest.main()

0 comments on commit 2703680

Please sign in to comment.