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), message['message'], hashlib.sha256) |
| 73 hasher.update(str(message['time'])) |
| 74 hasher.update(str(message['salt'])) |
| 75 |
| 76 client_hash = hasher.hexdigest() |
| 77 |
| 78 return Email.linear_compare(client_hash, message['hmac-sha256']) |
| 79 |
| 80 @staticmethod |
| 81 def _verify_json(build_data): |
| 82 """Verifies that the submitted JSON contains all the proper fields.""" |
| 83 fields = ['waterfall_url', |
| 84 'build_url', |
| 85 'project_name', |
| 86 'builderName', |
| 87 'steps', |
| 88 'unsatisfied', |
| 89 'revisions', |
| 90 'blamelist', |
| 91 'result', |
| 92 'number', |
| 93 'changes', |
| 94 'reason', |
| 95 'recipients'] |
| 96 |
| 97 for field in fields: |
| 98 if field not in build_data: |
| 99 logging.error('build_data did not contain field %s' % field) |
| 100 return False |
| 101 |
| 102 step_fields = ['started', |
| 103 'text', |
| 104 'results', |
| 105 'name', |
| 106 'logs', |
| 107 'urls'] |
| 108 |
| 109 if not build_data['steps']: |
| 110 logging.error('build_data did not contain any steps') |
| 111 return False |
| 112 for step in build_data['steps']: |
| 113 for field in step_fields: |
| 114 if field not in step: |
| 115 logging.error('build_step did not contain field %s' % field) |
| 116 return False |
| 117 |
| 118 return True |
| 119 |
| 120 def post(self): |
| 121 blob = self.request.get('json') |
| 122 if not blob: |
| 123 self.response.out.write('no json data sent') |
| 124 logging.error('error no json sent') |
| 125 self.error(400) |
| 126 return |
| 127 |
| 128 message = {} |
| 129 try: |
| 130 message = json.loads(blob) |
| 131 except ValueError as e: |
| 132 self.response.out.write('couldn\'t decode json') |
| 133 logging.error('error decoding incoming json: %s' % e) |
| 134 self.error(400) |
| 135 return |
| 136 |
| 137 secret = MailerSecret.get_or_insert('mailer_secret').secret |
| 138 if not secret: |
| 139 self.response.out.write('unauthorized') |
| 140 logging.critical('mailer shared secret has not been set!') |
| 141 self.error(500) |
| 142 return |
| 143 |
| 144 if not self._validate_message(message, self.request.url, secret): |
| 145 self.response.out.write('unauthorized') |
| 146 logging.error('incoming message did not validate') |
| 147 self.error(403) |
| 148 return |
| 149 |
| 150 try: |
| 151 build_data = json.loads(message['message']) |
| 152 except ValueError as e: |
| 153 self.response.out.write('couldn\'t decode payload json') |
| 154 logging.error('error decoding incoming json: %s' % e) |
| 155 self.error(400) |
| 156 return |
| 157 |
| 158 if not self._verify_json(build_data): |
| 159 logging.error('error verifying incoming json: %s' % build_data) |
| 160 self.response.out.write('json build format is incorrect') |
| 161 self.error(400) |
| 162 return |
| 163 |
| 164 # Emails can only come from the app ID, so we split on '@' here just in |
| 165 # case the user specified a full email address. |
| 166 from_addr_prefix = build_data.get('from_addr', 'buildbot').split('@')[0] |
| 167 from_addr = from_addr_prefix + '@%s.appspotmail.com' % ( |
| 168 app_identity.get_application_id()) |
| 169 |
| 170 recipients = ', '.join(build_data['recipients']) |
| 171 |
| 172 template = gatekeeper_mailer.MailTemplate(build_data['waterfall_url'], |
| 173 build_data['build_url'], |
| 174 build_data['project_name'], |
| 175 from_addr) |
| 176 |
| 177 |
| 178 text_content, html_content, subject = template.genMessageContent(build_data) |
| 179 |
| 180 message = mail.EmailMessage(sender=from_addr, |
| 181 subject=subject, |
| 182 #to=recipients, |
| 183 to=['xusydoc@chromium.org'], |
| 184 body=text_content, |
| 185 html=html_content) |
| 186 logging.info('sending email to %s', recipients) |
| 187 logging.info('sending from %s', from_addr) |
| 188 logging.info('subject is %s', subject) |
| 189 message.send() |
| 190 self.response.out.write('email sent') |
| 191 |
| 192 |
| 193 app = webapp2.WSGIApplication([('/mailer', MainPage), |
| 194 ('/mailer/email', Email)], |
| 195 debug=True) |
OLD | NEW |