summaryrefslogtreecommitdiffstats
path: root/webapp/codereview/proto_server.py
blob: fe9784a7e1ed59b768a2a75816c8b71c29e9420b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# Copyright 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
import hashlib
import hmac
import logging
import time
import zlib

from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext.webapp import util

from django.http import HttpResponse
from froofle.protobuf.service import RpcController

from codereview.models import Account, Settings
from codereview.internal.util import InternalAPI
from codereview.view_util import xsrf_for, is_xsrf_ok

from codereview.review_service import ReviewServiceImp
from codereview.internal.admin_service import AdminServiceImp
from codereview.internal.build_service import BuildServiceImp
from codereview.internal.bundle_store_service import BundleStoreServiceImp
from codereview.internal.change_service import ChangeServiceImp
from codereview.internal.merge_service import MergeServiceImp

MAX_TIME_WINDOW = 5 * 60 # seconds
XSRF_PATH = '/proto/'
services = {}

def register_service(s_impl):
  services[s_impl.GetDescriptor().name] = s_impl

register_service(ReviewServiceImp())
register_service(AdminServiceImp())
register_service(BuildServiceImp())
register_service(BundleStoreServiceImp())
register_service(ChangeServiceImp())
register_service(MergeServiceImp())

class _LocalController(RpcController):
  def __init__(self, s_impl):
    self._failed = None
    self._service = s_impl

  def Reset(self):
    pass

  def Failed(self):
    pass

  def ErrorText(self):
    pass

  def StartCancel(self):
    pass

  def SetFailed(self, reason):
    logging.error("Failed on %s: %s" % (
      self._service.__class__.__name__,
      reason))
    self._failed = reason

  def IsCancelled(self):
    pass

  def NotifyOnCancel(self, callback):
    pass

  def HasFailed(self):
    return self._failed is not None

def token(req):
  user = users.get_current_user()
  if not user:
    return HttpResponse(status=401, content="User must be logged in.")
  return HttpResponse(content = xsrf_for(XSRF_PATH),
                      content_type = 'application/octet-stream')

def serve(req, service_name, action_name):
  try:
    type_str = req.META['CONTENT_TYPE']
  except KeyError:
    return HttpResponse(status=415, content="Invalid request body type")

  type = type_str.split('; ')
  type_dict = {}
  type_dict['name'] = None
  type_dict['compress'] = None
  for t in type[1:]:
    name, val = t.split('=', 1)
    type_dict[name] = val
  if type[0] != "application/x-google-protobuf":
    return HttpResponse(status=415, content="Invalid request body type")

  try: s_impl = services[service_name]
  except KeyError:
    return HttpResponse(status=404, content="Service not recognized.")

  method = s_impl.GetDescriptor().FindMethodByName(action_name)
  if not method:
    return HttpResponse(status=404, content="Method not recognized.")

  request = s_impl.GetRequestClass(method)()
  request_name = request.DESCRIPTOR.full_name
  if type_dict['name'] != request_name:
    return HttpResponse(status=415,
                        content="Expected a %s" % request_name)

  raw_body = req.raw_post_data
  msg_bin = raw_body

  if 'HTTP_CONTENT_MD5' in req.META:
    expmd5 = req.META['HTTP_CONTENT_MD5']

    actmd5 = hashlib.md5()
    actmd5.update(raw_body)
    actmd5 = base64.b64encode(actmd5.digest())

    if actmd5 != expmd5:
      return HttpResponse(status=412,
                          content="Content-MD5 incorrect")

  compression = type_dict['compress']
  if compression == 'deflate':
    msg_bin = zlib.decompress(msg_bin)
  elif compression:
    return HttpResponse(status=415,
                        content="Unsupported compression %s" % compression)

  if isinstance(s_impl, InternalAPI):
    key = Settings.get_settings().internal_api_key
    key = base64.b64decode(key)

    try:
      date = int(req.META['HTTP_X_DATE_UTC'])
    except KeyError:
      return HttpResponse(status=403,
                          content="X-Date-UTC header is required.")

    try:
      exp_sig = req.META['HTTP_AUTHORIZATION']
    except KeyError:
      return HttpResponse(status=403,
                          content="Authorization header is required.")

    if not exp_sig.startswith("proto :"):
      return HttpResponse(status=403,
                          content="Malformed authorization header.")
    exp_sig = exp_sig[len("proto :"):]

    now = time.time()
    if abs(date - now) > MAX_TIME_WINDOW:
      return HttpResponse(status=403,
                          content="Request is too early or too late.")

    m = hmac.new(key, digestmod=hashlib.sha1)
    m.update('POST %s\n' % req.path)
    m.update('X-Date-UTC: %s\n' % date)
    m.update('Content-Type: %s\n' % type_str)
    m.update('\n')
    m.update(raw_body)
    if base64.b64encode(m.digest()) != exp_sig:
      return HttpResponse(status=403,
                          content="Invalid request signature.")
  else:
    user = users.get_current_user()
    if not user:
      return HttpResponse(status=401, content="User must be logged in.")

    try:
      xsrf = req.META['HTTP_X_XSRF_TOKEN']
    except KeyError:
      return HttpResponse(status=403,
                          content="X-XSRF-Token header required.")
    if not is_xsrf_ok(req, path=XSRF_PATH, xsrf=xsrf):
      return HttpResponse(status=403,
                          content="X-XSRF-Token invalid.")

  request.ParseFromString(msg_bin)
  controller = _LocalController(s_impl)

  class result_caddy:
    _r = HttpResponse(status=500)
    def __call__(self, r):
      if r is not None:
        r_bin = r.SerializeToString()
        r_name = r.DESCRIPTOR.full_name
        r_type = "application/x-google-protobuf; name=%s" % r_name
        self._r = HttpResponse(content_type = r_type, content = r_bin)
  done = result_caddy()

  s_impl.http_request = req

  s_impl.CallMethod(method, controller, request, done)
  if controller.HasFailed():
    return HttpResponse(status=500)
  return done._r