OLD | NEW |
---|---|
(Empty) | |
1 import hashlib | |
2 import hmac | |
3 import json | |
4 import logging | |
5 import time | |
6 | |
7 from google.appengine.api import app_identity | |
8 from google.appengine.api import mail | |
9 from google.appengine.ext import ndb | |
10 import webapp2 | |
11 from webapp2_extras import jinja2 | |
12 | |
13 import gatekeeper_mailer | |
14 | |
15 | |
16 class MailerSecret(ndb.Model): | |
17 """Model to represent the shared secret for the mail endpoint.""" | |
18 secret = ndb.StringProperty() | |
19 | |
20 | |
21 class BaseHandler(webapp2.RequestHandler): | |
22 """Provide a cached Jinja environment to each request.""" | |
23 @webapp2.cached_property | |
24 def jinja2(self): | |
25 # Returns a Jinja2 renderer cached in the app registry. | |
26 return jinja2.get_jinja2(app=self.app) | |
27 | |
28 def render_response(self, _template, **context): | |
29 # Renders a template and writes the result to the response. | |
30 rv = self.jinja2.render_template(_template, **context) | |
31 self.response.write(rv) | |
32 | |
33 | |
34 class MainPage(BaseHandler): | |
35 def get(self): | |
36 context = {'title': 'Chromium Gatekeeper Mailer'} | |
37 self.render_response('main_mailer.html', **context) | |
38 | |
39 | |
40 class Email(BaseHandler): | |
41 @staticmethod | |
42 def linear_compare(a, b): | |
43 """Scan through the entire string even if a mismatch is detected early. | |
44 | |
45 This thwarts timing attacks attempting to guess the key one byte at a | |
46 time. | |
47 """ | |
48 if len(a) != len(b): | |
49 return False | |
50 result = 0 | |
51 for x, y in zip(a, b): | |
52 result |= ord(x) ^ ord(y) | |
53 return result == 0 | |
54 | |
55 @staticmethod | |
56 def _validate_message(message, url, secret): | |
57 """Cryptographically validates the message.""" | |
58 mytime = time.time() | |
59 | |
60 if abs(mytime - message['time']) > 60: | |
61 logging.error('message was rejected due to time') | |
62 return False | |
63 | |
64 cleaned_url = url.rstrip('/') + '/' | |
65 cleaned_message_url = message['url'].rstrip('/') + '/' | |
66 | |
67 if cleaned_message_url != cleaned_url: | |
68 logging.error('message URL did not match: %s vs %s', cleaned_message_url, | |
69 cleaned_url) | |
70 return False | |
71 | |
72 hasher = hmac.new(str(secret), '%s:%d:%d' % (message['message'], | |
73 message['time'], | |
74 message['salt']), | |
75 hashlib.sha256) | |
iannucci
2013/08/30 04:24:06
just use hmac.update() 3 times, lose the colons
| |
76 | |
77 client_hash = hasher.hexdigest() | |
78 | |
79 return Email.linear_compare(client_hash, message['sha256']) | |
iannucci
2013/08/30 04:24:06
key should be hmac-sha256
| |
80 | |
81 @staticmethod | |
82 def _verify_json(build_data): | |
83 """Verifies that the submitted JSON contains all the proper fields.""" | |
84 fields = ['waterfall_url', | |
85 'build_url', | |
86 'project_name', | |
87 'builderName', | |
88 'steps', | |
89 'unsatisfied', | |
90 'revisions', | |
91 'blamelist', | |
92 'result', | |
93 'number', | |
94 'changes', | |
95 'reason', | |
96 'recipients'] | |
97 | |
98 for field in fields: | |
99 if field not in build_data: | |
100 logging.error('build_data did not contain field %s' % field) | |
101 return False | |
102 | |
103 step_fields = ['started', | |
104 'text', | |
105 'results', | |
106 'name', | |
107 'logs', | |
108 'urls'] | |
109 | |
110 if not build_data['steps']: | |
111 logging.error('build_data did not contain any steps') | |
112 return False | |
113 for step in build_data['steps']: | |
114 for field in step_fields: | |
115 if field not in step: | |
116 logging.error('build_step did not contain field %s' % field) | |
117 return False | |
118 | |
119 return True | |
120 | |
121 def post(self): | |
122 blob = self.request.get('json') | |
123 if not blob: | |
124 self.response.out.write('no json data sent') | |
125 logging.error('error no json sent') | |
126 self.error(400) | |
127 return | |
128 | |
129 message = {} | |
130 try: | |
131 message = json.loads(blob) | |
132 except ValueError as e: | |
133 self.response.out.write('couldn\'t decode json') | |
134 logging.error('error decoding incoming json: %s' % e) | |
135 self.error(400) | |
136 return | |
137 | |
138 secret = MailerSecret.get_or_insert('mailer_secret').secret | |
139 if not secret: | |
140 self.response.out.write('unauthorized') | |
141 logging.critical('mailer shared secret has not been set!') | |
142 self.error(500) | |
143 return | |
144 | |
145 if not self._validate_message(message, self.request.url, secret): | |
146 self.response.out.write('unauthorized') | |
147 logging.error('incoming message did not validate') | |
148 self.error(403) | |
149 return | |
150 | |
151 try: | |
152 build_data = json.loads(message['message']) | |
153 except ValueError as e: | |
154 self.response.out.write('couldn\'t decode payload json') | |
155 logging.error('error decoding incoming json: %s' % e) | |
156 self.error(400) | |
157 return | |
158 | |
159 if not self._verify_json(build_data): | |
160 logging.error('error verifying incoming json: %s' % build_data) | |
161 self.response.out.write('json build format is incorrect') | |
162 self.error(400) | |
163 return | |
164 | |
165 # Emails can only come from the app ID, so we split on '@' here just in | |
166 # case the user specified a full email address. | |
167 from_addr_prefix = build_data.get('from_addr', 'buildbot').split('@')[0] | |
168 from_addr = from_addr_prefix + '@%s.appspotmail.com' % ( | |
169 app_identity.get_application_id()) | |
170 | |
171 recipients = ', '.join(build_data['recipients']) | |
172 | |
173 template = gatekeeper_mailer.MailTemplate(build_data['waterfall_url'], | |
174 build_data['build_url'], | |
175 build_data['project_name'], | |
176 from_addr) | |
177 | |
178 | |
179 text_content, html_content, subject = template.genMessageContent(build_data) | |
180 | |
181 message = mail.EmailMessage(sender=from_addr, | |
182 subject=subject, | |
183 #to=recipients, | |
184 to=['xusydoc@chromium.org'], | |
185 body=text_content, | |
186 html=html_content) | |
187 logging.info('sending email to %s', recipients) | |
188 logging.info('sending from %s', from_addr) | |
189 logging.info('subject is %s', subject) | |
190 message.send() | |
191 self.response.out.write('email sent') | |
192 | |
193 | |
194 app = webapp2.WSGIApplication([('/mailer', MainPage), | |
195 ('/mailer/email', Email)], | |
196 debug=True) | |
OLD | NEW |