Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ sudo: false
dist: trusty
language: python
python:
- "2.7.6"
- "2.7"
- "3.3"
- "3.4"
Expand Down
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ user has authorized your app.

.. code:: python

SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"]
SLACK_SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"]

Create a Slack Event Adapter for receiving actions via the Events API
-----------------------------------------------------------------------
Expand All @@ -83,7 +83,7 @@ Create a Slack Event Adapter for receiving actions via the Events API
from slackeventsapi import SlackEventAdapter


slack_events_adapter = SlackEventAdapter(SLACK_VERIFICATION_TOKEN, endpoint="/slack/events")
slack_events_adapter = SlackEventAdapter(SLACK_SIGNING_SECRET, endpoint="/slack/events")


# Create an event listener for "reaction_added" events and print the emoji name
Expand Down Expand Up @@ -118,7 +118,7 @@ Create a Slack Event Adapter for receiving actions via the Events API

# Bind the Events API route to your existing Flask app by passing the server
# instance as the last param, or with `server=app`.
slack_events_adapter = SlackEventAdapter(SLACK_VERIFICATION_TOKEN, "/slack/events", app)
slack_events_adapter = SlackEventAdapter(SLACK_SIGNING_SECRET, "/slack/events", app)


# Create an event listener for "reaction_added" events and print the emoji name
Expand Down
6 changes: 3 additions & 3 deletions example/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ Copy your app's **Bot User OAuth Access Token** and add it to your python enviro

Next, go back to your app's **Basic Information** page

.. image:: https://cloud.githubusercontent.com/assets/32463/24877833/950dd53c-1de5-11e7-984f-deb26e8b9482.png
.. image:: https://user-images.githubusercontent.com/32463/43932347-63b21eca-9bf8-11e8-8b30-0a848c263bb1.png

Add your app's **Verification Token** to your python environmental variables
Add your app's **Signing Secret** to your python environmental variables

.. code::

export SLACK_VERIFICATION_TOKEN=xxxxxxxxXxxXxxXxXXXxxXxxx
export SLACK_SIGNING_SECRET=xxxxxxxxXxxXxxXxXXXxxXxxx


**🤖 Start ngrok**
Expand Down
17 changes: 11 additions & 6 deletions example/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import os

# Our app's Slack Event Adapter for receiving actions via the Events API
SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"]
slack_events_adapter = SlackEventAdapter(SLACK_VERIFICATION_TOKEN, "/slack/events")
slack_signing_secret = os.environ["SLACK_SIGNING_SECRET"]
slack_events_adapter = SlackEventAdapter(slack_signing_secret, "/slack/events")

# Create a SlackClient for your bot to use for Web API requests
SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
CLIENT = SlackClient(SLACK_BOT_TOKEN)
slack_bot_token = os.environ["SLACK_BOT_TOKEN"]
slack_client = SlackClient(slack_bot_token)

# Example responder to greetings
@slack_events_adapter.on("message")
Expand All @@ -18,7 +18,7 @@ def handle_message(event_data):
if message.get("subtype") is None and "hi" in message.get('text'):
channel = message["channel"]
message = "Hello <@%s>! :tada:" % message["user"]
CLIENT.api_call("chat.postMessage", channel=channel, text=message)
slack_client.api_call("chat.postMessage", channel=channel, text=message)


# Example reaction emoji echo
Expand All @@ -28,7 +28,12 @@ def reaction_added(event_data):
emoji = event["reaction"]
channel = event["item"]["channel"]
text = ":%s:" % emoji
CLIENT.api_call("chat.postMessage", channel=channel, text=text)
slack_client.api_call("chat.postMessage", channel=channel, text=text)

# Error events
@slack_events_adapter.on("error")
def error_handler(err):
print("ERROR: " + str(err))

# Once we have our event listeners configured, we can start the
# Flask server with the default `/events` endpoint on port 3000
Expand Down
6 changes: 3 additions & 3 deletions slackeventsapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
class SlackEventAdapter(EventEmitter):
# Initialize the Slack event server
# If no endpoint is provided, default to listening on '/slack/events'
def __init__(self, verification_token, endpoint="/slack/events", server=None):
def __init__(self, signing_secret, endpoint="/slack/events", server=None, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this **kwargs used now that we aren't using any optional flags?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. Removed.

EventEmitter.__init__(self)
self.verification_token = verification_token
self.server = SlackServer(verification_token, endpoint, self, server)
self.signing_secret = signing_secret
self.server = SlackServer(signing_secret, endpoint, self, server, **kwargs)

def start(self, host='127.0.0.1', port=None, debug=False, **kwargs):
"""
Expand Down
80 changes: 71 additions & 9 deletions slackeventsapi/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
import json
import platform
import sys
import hmac
import hashlib
from time import time
from .version import __version__


class SlackServer(Flask):
def __init__(self, verification_token, endpoint, emitter, server):
self.verification_token = verification_token
def __init__(self, signing_secret, endpoint, emitter, server):
self.signing_secret = signing_secret
self.emitter = emitter
self.endpoint = endpoint
self.package_info = self.get_package_info()
Expand Down Expand Up @@ -41,32 +44,91 @@ def get_package_info(self):

return " ".join(ua_string)

def verify_signature(self, timestamp, signature):
# Verify the request signature of the request sent from Slack
# Generate a new hash using the app's signing secret and request data

# Compare the generated hash and incoming request signature
# Python 2.7.6 doesn't support compare_digest
# It's recommended to use Python 2.7.7+
# noqa See https://docs.python.org/2/whatsnew/2.7.html#pep-466-network-security-enhancements-for-python-2-7
if hasattr(hmac, "compare_digest"):
req = str.encode('v0:' + str(timestamp) + ':') + request.data
request_hash = 'v0=' + hmac.new(
str.encode(self.signing_secret),
req, hashlib.sha256
).hexdigest()
# Compare byte strings for Python 2
if (sys.version_info[0] == 2):
return hmac.compare_digest(bytes(request_hash), bytes(signature))
else:
return hmac.compare_digest(request_hash, signature)
else:
# So, we'll compare the signatures explicitly
req = str.encode('v0:' + str(timestamp) + ':') + request.data
request_hash = 'v0=' + hmac.new(
str.encode(self.signing_secret),
req, hashlib.sha256
).hexdigest()

if len(request_hash) != len(signature):
return False
result = 0
if isinstance(request_hash, bytes) and isinstance(signature, bytes):
for x, y in zip(request_hash, signature):
result |= x ^ y
else:
for x, y in zip(request_hash, signature):
result |= ord(x) ^ ord(y)
return result == 0

def bind_route(self, server):
@server.route(self.endpoint, methods=['GET', 'POST'])
def event():
# If a GET request is made, return 404.
if request.method == 'GET':
return make_response("These are not the slackbots you're looking for.", 404)

# Each request comes with request timestamp and request signature
# emit an error if the timestamp is out of range
req_timestamp = request.headers.get('X-Slack-Request-Timestamp')
if abs(time() - int(req_timestamp)) > 60 * 5:
slack_exception = SlackEventAdapterException('Invalid request timestamp')
self.emitter.emit('error', slack_exception)
return make_response("", 403)

# Verify the request signature using the app's signing secret
# emit an error if the signature can't be verified
req_signature = request.headers.get('X-Slack-Signature')
if not self.verify_signature(req_timestamp, req_signature):
slack_exception = SlackEventAdapterException('Invalid request signature')
self.emitter.emit('error', slack_exception)
return make_response("", 403)

# Parse the request payload into JSON
event_data = json.loads(request.data.decode('utf-8'))

# Echo the URL verification challenge code
# Echo the URL verification challenge code back to Slack
if "challenge" in event_data:
return make_response(
event_data.get("challenge"), 200, {"content_type": "application/json"}
)

# Verify the request token
request_token = event_data.get("token")
if self.verification_token != request_token:
self.emitter.emit('error', Exception('invalid verification token'))
return make_response("Request contains invalid Slack verification token", 403)

# Parse the Event payload and emit the event to the event listener
if "event" in event_data:
event_type = event_data["event"]["type"]
self.emitter.emit(event_type, event_data)
response = make_response("", 200)
response.headers['X-Slack-Powered-By'] = self.package_info
return response


class SlackEventAdapterException(Exception):
"""
Base exception for all errors raised by the SlackClient library
"""
def __init__(self, msg=None):
if msg is None:
# default error message
msg = "An error occurred in the SlackEventsApiAdapter library"
super(SlackEventAdapterException, self).__init__(msg)
19 changes: 16 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import pytest
import json
import hashlib
import hmac
import pytest
from slackeventsapi import SlackEventAdapter


def create_signature(secret, timestamp, data):
req = str.encode('v0:' + str(timestamp) + ':') + str.encode(data)
request_signature= 'v0='+hmac.new(
str.encode(secret),
req, hashlib.sha256
).hexdigest()
return request_signature


def load_event_fixture(event, as_string=True):
filename = "tests/data/{}.json".format(event)
with open(filename) as json_data:
Expand All @@ -23,12 +34,14 @@ def pytest_namespace():
return {
'reaction_event_fixture': load_event_fixture('reaction_added'),
'url_challenge_fixture': load_event_fixture('url_challenge'),
'bad_token_fixture': event_with_bad_token()
'bad_token_fixture': event_with_bad_token(),
'create_signature': create_signature
}


@pytest.fixture
def app():
adapter = SlackEventAdapter("vFO9LARnLI7GflLR8tGqHgdy")
adapter = SlackEventAdapter("SIGNING_SECRET")
app = adapter.server
app.testing = True
return app
16 changes: 11 additions & 5 deletions tests/test_events.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import time
import pytest
from slackeventsapi import SlackEventAdapter

ADAPTER = SlackEventAdapter('vFO9LARnLI7GflLR8tGqHgdy')

ADAPTER = SlackEventAdapter('SIGNING_SECRET')

def test_event_emission(client):
# Events should trigger an event
data = pytest.reaction_event_fixture

@ADAPTER.on('reaction_added')
def event_handler(event):
assert event["reaction"] == 'grinning'

data = pytest.reaction_event_fixture
timestamp = int(time.time())
signature = pytest.create_signature(ADAPTER.signing_secret, timestamp, data)

res = client.post(
'/slack/events',
data=data,
content_type='application/json'
content_type='application/json',
headers={
'X-Slack-Request-Timestamp': timestamp,
'X-Slack-Signature': signature
}
)

assert res.status_code == 200
Loading