|
2 | 2 | import json |
3 | 3 | import platform |
4 | 4 | import sys |
| 5 | +import hmac |
| 6 | +import hashlib |
| 7 | +from time import time |
5 | 8 | from .version import __version__ |
6 | 9 |
|
7 | 10 |
|
8 | 11 | class SlackServer(Flask): |
9 | | - def __init__(self, verification_token, endpoint, emitter, server): |
10 | | - self.verification_token = verification_token |
| 12 | + def __init__(self, signing_secret, endpoint, emitter, server): |
| 13 | + self.signing_secret = signing_secret |
11 | 14 | self.emitter = emitter |
12 | 15 | self.endpoint = endpoint |
13 | 16 | self.package_info = self.get_package_info() |
@@ -41,32 +44,91 @@ def get_package_info(self): |
41 | 44 |
|
42 | 45 | return " ".join(ua_string) |
43 | 46 |
|
| 47 | + def verify_signature(self, timestamp, signature): |
| 48 | + # Verify the request signature of the request sent from Slack |
| 49 | + # Generate a new hash using the app's signing secret and request data |
| 50 | + |
| 51 | + # Compare the generated hash and incoming request signature |
| 52 | + # Python 2.7.6 doesn't support compare_digest |
| 53 | + # It's recommended to use Python 2.7.7+ |
| 54 | + # noqa See https://docs.python.org/2/whatsnew/2.7.html#pep-466-network-security-enhancements-for-python-2-7 |
| 55 | + if hasattr(hmac, "compare_digest"): |
| 56 | + req = str.encode('v0:' + str(timestamp) + ':') + request.data |
| 57 | + request_hash = 'v0=' + hmac.new( |
| 58 | + str.encode(self.signing_secret), |
| 59 | + req, hashlib.sha256 |
| 60 | + ).hexdigest() |
| 61 | + # Compare byte strings for Python 2 |
| 62 | + if (sys.version_info[0] == 2): |
| 63 | + return hmac.compare_digest(bytes(request_hash), bytes(signature)) |
| 64 | + else: |
| 65 | + return hmac.compare_digest(request_hash, signature) |
| 66 | + else: |
| 67 | + # So, we'll compare the signatures explicitly |
| 68 | + req = str.encode('v0:' + str(timestamp) + ':') + request.data |
| 69 | + request_hash = 'v0=' + hmac.new( |
| 70 | + str.encode(self.signing_secret), |
| 71 | + req, hashlib.sha256 |
| 72 | + ).hexdigest() |
| 73 | + |
| 74 | + if len(request_hash) != len(signature): |
| 75 | + return False |
| 76 | + result = 0 |
| 77 | + if isinstance(request_hash, bytes) and isinstance(signature, bytes): |
| 78 | + for x, y in zip(request_hash, signature): |
| 79 | + result |= x ^ y |
| 80 | + else: |
| 81 | + for x, y in zip(request_hash, signature): |
| 82 | + result |= ord(x) ^ ord(y) |
| 83 | + return result == 0 |
| 84 | + |
44 | 85 | def bind_route(self, server): |
45 | 86 | @server.route(self.endpoint, methods=['GET', 'POST']) |
46 | 87 | def event(): |
47 | 88 | # If a GET request is made, return 404. |
48 | 89 | if request.method == 'GET': |
49 | 90 | return make_response("These are not the slackbots you're looking for.", 404) |
50 | 91 |
|
| 92 | + # Each request comes with request timestamp and request signature |
| 93 | + # emit an error if the timestamp is out of range |
| 94 | + req_timestamp = request.headers.get('X-Slack-Request-Timestamp') |
| 95 | + if abs(time() - int(req_timestamp)) > 60 * 5: |
| 96 | + slack_exception = SlackEventAdapterException('Invalid request timestamp') |
| 97 | + self.emitter.emit('error', slack_exception) |
| 98 | + return make_response("", 403) |
| 99 | + |
| 100 | + # Verify the request signature using the app's signing secret |
| 101 | + # emit an error if the signature can't be verified |
| 102 | + req_signature = request.headers.get('X-Slack-Signature') |
| 103 | + if not self.verify_signature(req_timestamp, req_signature): |
| 104 | + slack_exception = SlackEventAdapterException('Invalid request signature') |
| 105 | + self.emitter.emit('error', slack_exception) |
| 106 | + return make_response("", 403) |
| 107 | + |
51 | 108 | # Parse the request payload into JSON |
52 | 109 | event_data = json.loads(request.data.decode('utf-8')) |
53 | 110 |
|
54 | | - # Echo the URL verification challenge code |
| 111 | + # Echo the URL verification challenge code back to Slack |
55 | 112 | if "challenge" in event_data: |
56 | 113 | return make_response( |
57 | 114 | event_data.get("challenge"), 200, {"content_type": "application/json"} |
58 | 115 | ) |
59 | 116 |
|
60 | | - # Verify the request token |
61 | | - request_token = event_data.get("token") |
62 | | - if self.verification_token != request_token: |
63 | | - self.emitter.emit('error', Exception('invalid verification token')) |
64 | | - return make_response("Request contains invalid Slack verification token", 403) |
65 | | - |
66 | 117 | # Parse the Event payload and emit the event to the event listener |
67 | 118 | if "event" in event_data: |
68 | 119 | event_type = event_data["event"]["type"] |
69 | 120 | self.emitter.emit(event_type, event_data) |
70 | 121 | response = make_response("", 200) |
71 | 122 | response.headers['X-Slack-Powered-By'] = self.package_info |
72 | 123 | return response |
| 124 | + |
| 125 | + |
| 126 | +class SlackEventAdapterException(Exception): |
| 127 | + """ |
| 128 | + Base exception for all errors raised by the SlackClient library |
| 129 | + """ |
| 130 | + def __init__(self, msg=None): |
| 131 | + if msg is None: |
| 132 | + # default error message |
| 133 | + msg = "An error occurred in the SlackEventsApiAdapter library" |
| 134 | + super(SlackEventAdapterException, self).__init__(msg) |
0 commit comments