Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 import datetime | |
| 2 from google.appengine.api import mail | |
| 3 import hashlib | |
| 4 import hmac | |
| 5 import logging | |
| 6 import json | |
| 7 import webapp2 | |
| 8 from webapp2_extras import jinja2 | |
| 9 | |
| 10 import gatekeeper_mailer | |
| 11 | |
| 12 | |
| 13 class BaseHandler(webapp2.RequestHandler): | |
| 14 """Provide a cached Jinja environment to each request.""" | |
| 15 @webapp2.cached_property | |
| 16 def jinja2(self): | |
| 17 # Returns a Jinja2 renderer cached in the app registry. | |
| 18 return jinja2.get_jinja2(app=self.app) | |
| 19 | |
| 20 def render_response(self, _template, **context): | |
| 21 # Renders a template and writes the result to the response. | |
| 22 rv = self.jinja2.render_template(_template, **context) | |
| 23 self.response.write(rv) | |
| 24 | |
| 25 | |
| 26 class MainPage(BaseHandler): | |
| 27 def get(self): | |
| 28 context = {'title': 'Chromium Gatekeeper Mailer'} | |
| 29 self.render_response('main_mailer.html', **context) | |
| 30 | |
| 31 | |
| 32 class Email(BaseHandler): | |
| 33 @staticmethod | |
| 34 def _validate_message(message, secret): | |
| 35 # Make this backwards compatable with python 2.6, since total_seconds() | |
| 36 # exists only in python 2.7. | |
| 37 utc_now_td = (datetime.datetime.utcnow() - | |
| 38 datetime.datetime.utcfromtimestamp(0)) | |
| 39 utc_now = utc_now_td.days * 86400 + utc_now_td.seconds | |
| 40 | |
| 41 if (utc_now < message['time']) or (utc_now - message['time'] > 60): | |
| 42 logging.error('message was rejected due to time') | |
| 43 return False | |
| 44 | |
| 45 hasher = hmac.new(secret, '%s:%d:%d' % (message['message'], | |
| 46 message['time'], | |
| 47 message['salt']), | |
| 48 hashlib.sha256) | |
| 49 | |
| 50 client_hash = hasher.hexdigest() | |
| 51 | |
| 52 return client_hash == message['sha256'] | |
| 53 | |
| 54 | |
| 55 @staticmethod | |
| 56 def _verify_json(build_data): | |
| 57 fields = ['waterfall_url', | |
| 58 'build_url', | |
| 59 'project_name', | |
| 60 'builderName', | |
| 61 'steps', | |
| 62 'unsatisfied', | |
| 63 'revisions', | |
| 64 'blamelist', | |
| 65 'result', | |
| 66 'number', | |
| 67 'changes', | |
| 68 'reason', | |
| 69 'recipients'] | |
| 70 | |
| 71 for field in fields: | |
| 72 if field not in build_data: | |
| 73 logging.error('build_data did not contain field %s' % field) | |
| 74 return False | |
| 75 | |
| 76 stepfields = ['started', | |
| 77 'text', | |
| 78 'results', | |
| 79 'name', | |
| 80 'logs', | |
| 81 'urls'] | |
| 82 | |
| 83 if not build_data['steps']: | |
| 84 logging.error('build_data did not contain any steps') | |
| 85 return False | |
| 86 for step in build_data['steps']: | |
| 87 for field in stepfields: | |
| 88 if field not in step: | |
| 89 logging.error('build_step did not contain field %s' % field) | |
| 90 return False | |
| 91 | |
| 92 return True | |
| 93 | |
| 94 def post(self): | |
| 95 blob = self.request.get('json') | |
| 96 if not blob: | |
| 97 self.response.out.write('no json data sent') | |
| 98 logging.error('error no json sent') | |
| 99 self.error(400) | |
| 100 return | |
| 101 | |
| 102 message = {} | |
| 103 try: | |
| 104 message = json.loads(blob) | |
| 105 except ValueError as e: | |
| 106 self.response.out.write('couldn\'t decode json') | |
| 107 logging.error('error decoding incoming json: %s' % e) | |
| 108 self.error(400) | |
| 109 return | |
| 110 | |
| 111 if not self._validate_message(message, 'pajamas'): | |
|
agable
2013/08/01 17:52:44
Nice secret :)
| |
| 112 self.response.out.write('unauthorized') | |
| 113 logging.error('incoming message did not validate') | |
| 114 self.error(403) | |
| 115 return | |
| 116 | |
| 117 build_data = json.loads(message['message']) | |
| 118 | |
| 119 if not self._verify_json(build_data): | |
| 120 logging.error('error verifying incoming json: %s' % build_data) | |
| 121 self.response.out.write('json build format is incorrect') | |
|
agable
2013/08/01 17:52:44
Could be a more informative error message -- what
| |
| 122 self.error(400) | |
| 123 return | |
| 124 | |
| 125 from_addr = build_data.get('from_addr', 'buildbot@chromium.org') | |
|
agable
2013/08/01 17:52:44
Does sending as this address work, or does appengi
Mike Stip (use stip instead)
2013/08/29 19:56:46
it doesn't, fixed
| |
| 126 recipients = ', '.join(build_data['recipients']) | |
| 127 | |
| 128 template = gatekeeper_mailer.MailTemplate(build_data['waterfall_url'], | |
| 129 build_data['build_url'], | |
| 130 build_data['project_name'], | |
| 131 from_addr) | |
| 132 | |
| 133 | |
| 134 text_content, html_content, subject = template.genMessageContent(build_data) | |
| 135 | |
| 136 message = mail.EmailMessage(sender=from_addr, | |
| 137 subject=subject, | |
| 138 #to=recipients, | |
| 139 to=['xusydoc@chromium.org'], | |
| 140 body=text_content, | |
| 141 html=html_content) | |
| 142 logging.info('sending email to %s', recipients) | |
| 143 message.send() | |
| 144 self.response.out.write('email sent') | |
| 145 | |
| 146 | |
| 147 app = webapp2.WSGIApplication([('/mailer', MainPage), | |
| 148 ('/mailer/email', Email)], | |
| 149 debug=True) | |
| OLD | NEW |