-
Notifications
You must be signed in to change notification settings - Fork 114
/
server.py
133 lines (112 loc) · 5.44 KB
/
server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import hashlib
import hmac
import json
import platform
import sys
from time import time
from flask import Flask, request, make_response, Blueprint
from werkzeug.local import LocalProxy
from .version import __version__
class SlackServer(Flask):
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()
# If a server is passed in, bind the event handler routes to it,
# otherwise create a new Flask instance.
if server:
if isinstance(server, (Flask, Blueprint, LocalProxy)):
self.bind_route(server)
else:
raise TypeError("Server must be an instance of Flask, Blueprint, or LocalProxy")
else:
Flask.__init__(self, __name__)
self.bind_route(self)
def get_package_info(self):
client_name = __name__.split('.')[0]
client_version = __version__ # Version is returned from version.py
# Collect the package info, Python version and OS version.
package_info = {
"client": "{0}/{1}".format(client_name, client_version),
"python": "Python/{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info),
"system": "{0}/{1}".format(platform.system(), platform.release())
}
# Concatenate and format the user-agent string to be passed into request headers
ua_string = []
for key, val in package_info.items():
ua_string.append(val)
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
req = str.encode('v0:' + str(timestamp) + ':') + request.get_data()
request_hash = 'v0=' + hmac.new(
str.encode(self.signing_secret),
req, hashlib.sha256
).hexdigest()
if hasattr(hmac, "compare_digest"):
# 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:
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 req_timestamp is None or 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 req_signature is None or 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 back to Slack
if "challenge" in event_data:
return make_response(
{"challenge": event_data.get("challenge")}, 200,
{"content_type": "application/json"}
)
# 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)