Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(158)

Side by Side Diff: mailer.py

Issue 19878007: Add build mailer capability to support gatekeeper_ng. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/chromium-build@master
Patch Set: Set proper mailer address. Created 7 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « gatekeeper_mailer.py ('k') | templates/base_mail.html » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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)
OLDNEW
« no previous file with comments | « gatekeeper_mailer.py ('k') | templates/base_mail.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698