Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 import datetime | |
| 2 from google.appengine.ext import db | |
|
Vadim Sh.
2013/08/26 21:30:41
ndb?
It will speed up secret fetch by automagica
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
| 3 from google.appengine.api import mail | |
| 4 import hashlib | |
| 5 import hmac | |
|
Vadim Sh.
2013/08/26 21:30:41
nit: reorganize imports order:
1. stdlib.
2. other
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
| 6 import logging | |
| 7 import json | |
| 8 import webapp2 | |
| 9 from webapp2_extras import jinja2 | |
| 10 | |
| 11 import gatekeeper_mailer | |
| 12 | |
| 13 | |
| 14 class BaseHandler(webapp2.RequestHandler): | |
| 15 """Provide a cached Jinja environment to each request.""" | |
| 16 @webapp2.cached_property | |
| 17 def jinja2(self): | |
| 18 # Returns a Jinja2 renderer cached in the app registry. | |
| 19 return jinja2.get_jinja2(app=self.app) | |
| 20 | |
| 21 def render_response(self, _template, **context): | |
| 22 # Renders a template and writes the result to the response. | |
| 23 rv = self.jinja2.render_template(_template, **context) | |
| 24 self.response.write(rv) | |
| 25 | |
| 26 | |
| 27 class MainPage(BaseHandler): | |
| 28 def get(self): | |
| 29 context = {'title': 'Chromium Gatekeeper Mailer'} | |
| 30 self.render_response('main_mailer.html', **context) | |
| 31 | |
| 32 class MailerSecret(db.Model): | |
|
Vadim Sh.
2013/08/26 21:30:41
nit: Move this class to the top of the page before
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
| 33 """Model to represent the shared secret for the mail endpoint.""" | |
| 34 secret = db.StringProperty() | |
| 35 | |
| 36 class Email(BaseHandler): | |
| 37 @staticmethod | |
| 38 def _validate_message(message, secret): | |
| 39 # Make this backwards compatable with python 2.6, since total_seconds() | |
| 40 # exists only in python 2.7. | |
|
Vadim Sh.
2013/08/26 21:30:41
Is this comment still relevant?
Mike Stip (use stip instead)
2013/08/29 19:56:46
nope
| |
| 41 utc_now_td = (datetime.datetime.utcnow() - | |
| 42 datetime.datetime.utcfromtimestamp(0)) | |
| 43 utc_now = utc_now_td.days * 86400 + utc_now_td.seconds | |
|
Vadim Sh.
2013/08/26 21:30:41
And I think all this can be reduced to time.time()
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
| 44 | |
| 45 if (utc_now < message['time']) or (utc_now - message['time'] > 60): | |
|
Vadim Sh.
2013/08/26 21:30:41
abs(utc_now - message['time']) > 60
clock skew ca
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
| 46 logging.error('message was rejected due to time') | |
| 47 return False | |
| 48 | |
| 49 hasher = hmac.new(secret, '%s:%d:%d' % (message['message'], | |
| 50 message['time'], | |
| 51 message['salt']), | |
| 52 hashlib.sha256) | |
| 53 | |
| 54 client_hash = hasher.hexdigest() | |
| 55 | |
| 56 return client_hash == message['sha256'] | |
|
Vadim Sh.
2013/08/26 21:30:41
Usually this comparison is done using constant tim
iannucci
2013/08/27 21:45:20
((Actually it can make a difference. I've used a p
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
| 57 | |
| 58 | |
| 59 @staticmethod | |
| 60 def _verify_json(build_data): | |
| 61 fields = ['waterfall_url', | |
| 62 'build_url', | |
| 63 'project_name', | |
| 64 'builderName', | |
| 65 'steps', | |
| 66 'unsatisfied', | |
| 67 'revisions', | |
| 68 'blamelist', | |
| 69 'result', | |
| 70 'number', | |
| 71 'changes', | |
| 72 'reason', | |
| 73 'recipients'] | |
| 74 | |
| 75 for field in fields: | |
| 76 if field not in build_data: | |
| 77 logging.error('build_data did not contain field %s' % field) | |
| 78 return False | |
| 79 | |
| 80 stepfields = ['started', | |
|
Vadim Sh.
2013/08/26 21:30:41
nit: step_fields
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
| 81 'text', | |
| 82 'results', | |
| 83 'name', | |
| 84 'logs', | |
| 85 'urls'] | |
| 86 | |
| 87 if not build_data['steps']: | |
| 88 logging.error('build_data did not contain any steps') | |
| 89 return False | |
| 90 for step in build_data['steps']: | |
| 91 for field in stepfields: | |
| 92 if field not in step: | |
| 93 logging.error('build_step did not contain field %s' % field) | |
| 94 return False | |
| 95 | |
| 96 return True | |
| 97 | |
| 98 def post(self): | |
| 99 blob = self.request.get('json') | |
|
Vadim Sh.
2013/08/26 21:30:41
I'm not sure what part of the system is sending th
Mike Stip (use stip instead)
2013/08/29 19:56:46
that's scripts/slave/gatekeeper_ng.py. I'll make a
| |
| 100 if not blob: | |
| 101 self.response.out.write('no json data sent') | |
| 102 logging.error('error no json sent') | |
| 103 self.error(400) | |
| 104 return | |
| 105 | |
| 106 message = {} | |
| 107 try: | |
| 108 message = json.loads(blob) | |
| 109 except ValueError as e: | |
| 110 self.response.out.write('couldn\'t decode json') | |
| 111 logging.error('error decoding incoming json: %s' % e) | |
| 112 self.error(400) | |
| 113 return | |
| 114 | |
| 115 secret = str(MailerSecret.get_or_insert('mailer_secret').secret) | |
|
Vadim Sh.
2013/08/26 21:30:41
Why 'str' here?
I'm thinking about a possibility o
Mike Stip (use stip instead)
2013/08/29 19:56:46
stuff from db came in as unicode, while stuff from
| |
| 116 if not secret: | |
| 117 self.response.out.write('unauthorized') | |
| 118 logging.critical('mailer shared secret has not been set!') | |
| 119 self.error(500) | |
| 120 return | |
| 121 | |
| 122 if not self._validate_message(message, secret): | |
| 123 self.response.out.write('unauthorized') | |
| 124 logging.error('incoming message did not validate') | |
| 125 self.error(403) | |
| 126 return | |
| 127 | |
| 128 build_data = json.loads(message['message']) | |
|
Vadim Sh.
2013/08/26 21:30:41
Catch ValueError here as well?
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
| 129 | |
| 130 if not self._verify_json(build_data): | |
| 131 logging.error('error verifying incoming json: %s' % build_data) | |
| 132 self.response.out.write('json build format is incorrect') | |
| 133 self.error(400) | |
| 134 return | |
| 135 | |
| 136 from_addr = build_data.get('from_addr', 'buildbot@chromium.org') | |
| 137 recipients = ', '.join(build_data['recipients']) | |
| 138 | |
| 139 template = gatekeeper_mailer.MailTemplate(build_data['waterfall_url'], | |
| 140 build_data['build_url'], | |
| 141 build_data['project_name'], | |
| 142 from_addr) | |
| 143 | |
| 144 | |
| 145 text_content, html_content, subject = template.genMessageContent(build_data) | |
| 146 | |
| 147 message = mail.EmailMessage(sender=from_addr, | |
| 148 subject=subject, | |
| 149 #to=recipients, | |
| 150 to=['xusydoc@chromium.org'], | |
| 151 body=text_content, | |
| 152 html=html_content) | |
| 153 logging.info('sending email to %s', recipients) | |
| 154 message.send() | |
| 155 self.response.out.write('email sent') | |
| 156 | |
| 157 | |
| 158 app = webapp2.WSGIApplication([('/mailer', MainPage), | |
| 159 ('/mailer/email', Email)], | |
| 160 debug=True) | |
| OLD | NEW |