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

Unified 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: Rebase to latest master Created 7 years, 4 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « gatekeeper_mailer.py ('k') | templates/base_mail.html » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: mailer.py
diff --git a/mailer.py b/mailer.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ff2dc62b2680671e3e0b7f635ee37a6affa6ff5
--- /dev/null
+++ b/mailer.py
@@ -0,0 +1,195 @@
+import hashlib
+import hmac
+import json
+import logging
+import time
+
+from google.appengine.api import app_identity
+from google.appengine.api import mail
+from google.appengine.ext import ndb
+import webapp2
+from webapp2_extras import jinja2
+
+import gatekeeper_mailer
+
+
+class MailerSecret(ndb.Model):
+ """Model to represent the shared secret for the mail endpoint."""
+ secret = ndb.StringProperty()
+
+
+class BaseHandler(webapp2.RequestHandler):
+ """Provide a cached Jinja environment to each request."""
+ @webapp2.cached_property
+ def jinja2(self):
+ # Returns a Jinja2 renderer cached in the app registry.
+ return jinja2.get_jinja2(app=self.app)
+
+ def render_response(self, _template, **context):
+ # Renders a template and writes the result to the response.
+ rv = self.jinja2.render_template(_template, **context)
+ self.response.write(rv)
+
+
+class MainPage(BaseHandler):
+ def get(self):
+ context = {'title': 'Chromium Gatekeeper Mailer'}
+ self.render_response('main_mailer.html', **context)
+
+
+class Email(BaseHandler):
+ @staticmethod
+ def linear_compare(a, b):
+ """Scan through the entire string even if a mismatch is detected early.
+
+ This thwarts timing attacks attempting to guess the key one byte at a
+ time.
+ """
+ if len(a) != len(b):
+ return False
+ result = 0
+ for x, y in zip(a, b):
+ result |= ord(x) ^ ord(y)
+ return result == 0
+
+ @staticmethod
+ def _validate_message(message, url, secret):
+ """Cryptographically validates the message."""
+ mytime = time.time()
+
+ if abs(mytime - message['time']) > 60:
+ logging.error('message was rejected due to time')
+ return False
+
+ cleaned_url = url.rstrip('/') + '/'
+ cleaned_message_url = message['url'].rstrip('/') + '/'
+
+ if cleaned_message_url != cleaned_url:
+ logging.error('message URL did not match: %s vs %s', cleaned_message_url,
+ cleaned_url)
+ return False
+
+ hasher = hmac.new(str(secret), message['message'], hashlib.sha256)
+ hasher.update(str(message['time']))
+ hasher.update(str(message['salt']))
+
+ client_hash = hasher.hexdigest()
+
+ return Email.linear_compare(client_hash, message['hmac-sha256'])
+
+ @staticmethod
+ def _verify_json(build_data):
+ """Verifies that the submitted JSON contains all the proper fields."""
+ fields = ['waterfall_url',
+ 'build_url',
+ 'project_name',
+ 'builderName',
+ 'steps',
+ 'unsatisfied',
+ 'revisions',
+ 'blamelist',
+ 'result',
+ 'number',
+ 'changes',
+ 'reason',
+ 'recipients']
+
+ for field in fields:
+ if field not in build_data:
+ logging.error('build_data did not contain field %s' % field)
+ return False
+
+ step_fields = ['started',
+ 'text',
+ 'results',
+ 'name',
+ 'logs',
+ 'urls']
+
+ if not build_data['steps']:
+ logging.error('build_data did not contain any steps')
+ return False
+ for step in build_data['steps']:
+ for field in step_fields:
+ if field not in step:
+ logging.error('build_step did not contain field %s' % field)
+ return False
+
+ return True
+
+ def post(self):
+ blob = self.request.get('json')
+ if not blob:
+ self.response.out.write('no json data sent')
+ logging.error('error no json sent')
+ self.error(400)
+ return
+
+ message = {}
+ try:
+ message = json.loads(blob)
+ except ValueError as e:
+ self.response.out.write('couldn\'t decode json')
+ logging.error('error decoding incoming json: %s' % e)
+ self.error(400)
+ return
+
+ secret = MailerSecret.get_or_insert('mailer_secret').secret
+ if not secret:
+ self.response.out.write('unauthorized')
+ logging.critical('mailer shared secret has not been set!')
+ self.error(500)
+ return
+
+ if not self._validate_message(message, self.request.url, secret):
+ self.response.out.write('unauthorized')
+ logging.error('incoming message did not validate')
+ self.error(403)
+ return
+
+ try:
+ build_data = json.loads(message['message'])
+ except ValueError as e:
+ self.response.out.write('couldn\'t decode payload json')
+ logging.error('error decoding incoming json: %s' % e)
+ self.error(400)
+ return
+
+ if not self._verify_json(build_data):
+ logging.error('error verifying incoming json: %s' % build_data)
+ self.response.out.write('json build format is incorrect')
+ self.error(400)
+ return
+
+ # Emails can only come from the app ID, so we split on '@' here just in
+ # case the user specified a full email address.
+ from_addr_prefix = build_data.get('from_addr', 'buildbot').split('@')[0]
+ from_addr = from_addr_prefix + '@%s.appspotmail.com' % (
+ app_identity.get_application_id())
+
+ recipients = ', '.join(build_data['recipients'])
+
+ template = gatekeeper_mailer.MailTemplate(build_data['waterfall_url'],
+ build_data['build_url'],
+ build_data['project_name'],
+ from_addr)
+
+
+ text_content, html_content, subject = template.genMessageContent(build_data)
+
+ message = mail.EmailMessage(sender=from_addr,
+ subject=subject,
+ #to=recipients,
+ to=['xusydoc@chromium.org'],
+ body=text_content,
+ html=html_content)
+ logging.info('sending email to %s', recipients)
+ logging.info('sending from %s', from_addr)
+ logging.info('subject is %s', subject)
+ message.send()
+ self.response.out.write('email sent')
+
+
+app = webapp2.WSGIApplication([('/mailer', MainPage),
+ ('/mailer/email', Email)],
+ debug=True)
« 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