Chromium Code Reviews| 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 |