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 |