blob: b7d85acdb089de42257330cdcd8f3c6742b4887e [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""This file defines a subclass of Servlet for JSON feeds.
7
8A "feed" is a servlet that is accessed by another part of our system and that
9responds with a JSON value rather than HTML to display in a browser.
10"""
11from __future__ import print_function
12from __future__ import division
13from __future__ import absolute_import
14
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020015from six.moves import http_client
Copybara854996b2021-09-07 19:36:02 +000016import json
17import logging
18
19from google.appengine.api import app_identity
20
21import settings
22
23from framework import framework_constants
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020024from framework import flaskservlet
25from framework import servlet_helpers
Copybara854996b2021-09-07 19:36:02 +000026from framework import permissions
27from framework import servlet
28from framework import xsrf
29from search import query2ast
30
31# This causes a JS error for a hacker trying to do a cross-site inclusion.
32XSSI_PREFIX = ")]}'\n"
33
34
35class JsonFeed(servlet.Servlet):
36 """A convenient base class for JSON feeds."""
37
38 # By default, JSON output is compact. Subclasses can set this to
39 # an integer, like 4, for pretty-printed output.
40 JSON_INDENT = None
41
42 # Some JSON handlers can only be accessed from our own app.
43 CHECK_SAME_APP = False
44
45 def HandleRequest(self, _mr):
46 """Override this method to implement handling of the request.
47
48 Args:
49 mr: common information parsed from the HTTP request.
50
51 Returns:
52 A dictionary of json data.
53 """
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020054 raise servlet_helpers.MethodNotSupportedError()
Copybara854996b2021-09-07 19:36:02 +000055
56 def _DoRequestHandling(self, request, mr):
57 """Do permission checking, page processing, and response formatting."""
58 try:
59 # TODO(jrobbins): check the XSRF token even for anon users
60 # after the next deployment.
61 if self.CHECK_SECURITY_TOKEN and mr.auth.user_id:
62 # Validate the XSRF token with the specific request path for this
63 # servlet. But, not every XHR request has a distinct token, so just
64 # use 'xhr' for ones that don't.
65 # TODO(jrobbins): make specific tokens for:
66 # user and project stars, issue options, check names.
67 try:
68 logging.info('request in jsonfeed is %r', request)
69 xsrf.ValidateToken(mr.token, mr.auth.user_id, request.path)
70 except xsrf.TokenIncorrect:
71 logging.info('using token path "xhr"')
72 xsrf.ValidateToken(mr.token, mr.auth.user_id, xsrf.XHR_SERVLET_PATH)
73
74 if self.CHECK_SAME_APP and not settings.local_mode:
75 calling_app_id = request.headers.get('X-Appengine-Inbound-Appid')
76 if calling_app_id != app_identity.get_application_id():
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020077 self.response.status = http_client.FORBIDDEN
Copybara854996b2021-09-07 19:36:02 +000078 return
79
80 self._CheckForMovedProject(mr, request)
81 self.AssertBasePermission(mr)
82
83 json_data = self.HandleRequest(mr)
84
85 self._RenderJsonResponse(json_data)
86
87 except query2ast.InvalidQueryError as e:
88 logging.warning('Trapped InvalidQueryError: %s', e)
89 logging.exception(e)
90 msg = e.message if e.message else 'invalid query'
91 self.abort(400, msg)
92 except permissions.PermissionException as e:
93 logging.info('Trapped PermissionException %s', e)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020094 self.response.status = http_client.FORBIDDEN
Copybara854996b2021-09-07 19:36:02 +000095
96 # pylint: disable=unused-argument
97 # pylint: disable=arguments-differ
98 # Note: unused arguments necessary because they are specified in
99 # registerpages.py as an extra URL validation step even though we
100 # do our own URL parsing in monorailrequest.py
101 def get(self, project_name=None, viewed_username=None, hotlist_id=None):
102 """Collect page-specific and generic info, then render the page.
103
104 Args:
105 project_name: string project name parsed from the URL by webapp2,
106 but we also parse it out in our code.
107 viewed_username: string user email parsed from the URL by webapp2,
108 but we also parse it out in our code.
109 hotlist_id: string hotlist id parsed from the URL by webapp2,
110 but we also parse it out in our code.
111 """
112 self._DoRequestHandling(self.mr.request, self.mr)
113
114 # pylint: disable=unused-argument
115 # pylint: disable=arguments-differ
116 def post(self, project_name=None, viewed_username=None, hotlist_id=None):
117 """Parse the request, check base perms, and call form-specific code."""
118 self._DoRequestHandling(self.mr.request, self.mr)
119
120 def _RenderJsonResponse(self, json_data):
121 """Serialize the data as JSON so that it can be sent to the browser."""
122 json_str = json.dumps(json_data, indent=self.JSON_INDENT)
123 logging.debug(
124 'Sending JSON response: %r length: %r',
125 json_str[:framework_constants.LOGGING_MAX_LENGTH], len(json_str))
126 self.response.content_type = framework_constants.CONTENT_TYPE_JSON
127 self.response.headers['X-Content-Type-Options'] = (
128 framework_constants.CONTENT_TYPE_JSON_OPTIONS)
129 self.response.write(XSSI_PREFIX)
130 self.response.write(json_str)
131
132
133class InternalTask(JsonFeed):
134 """Internal tasks are JSON feeds that can only be reached by our own code."""
135
136 CHECK_SECURITY_TOKEN = False
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200137
138
139class FlaskJsonFeed(flaskservlet.FlaskServlet):
140 """A convenient base class for JSON feeds."""
141
142 # By default, JSON output is compact. Subclasses can set this to
143 # an integer, like 4, for pretty-printed output.
144 JSON_INDENT = None
145
146 # Some JSON handlers can only be accessed from our own app.
147 CHECK_SAME_APP = False
148
149 def HandleRequest(self, _mr):
150 """Override this method to implement handling of the request.
151
152 Args:
153 mr: common information parsed from the HTTP request.
154
155 Returns:
156 A dictionary of json data.
157 """
158 raise servlet_helpers.MethodNotSupportedError()
159
160 def _DoRequestHandling(self, request, mr):
161 """Do permission checking, page processing, and response formatting."""
162 try:
163 if self.CHECK_SECURITY_TOKEN and mr.auth.user_id:
164 try:
165 logging.info('request in jsonfeed is %r', request)
166 xsrf.ValidateToken(mr.token, mr.auth.user_id, request.path)
167 except xsrf.TokenIncorrect:
168 logging.info('using token path "xhr"')
169 xsrf.ValidateToken(mr.token, mr.auth.user_id, xsrf.XHR_SERVLET_PATH)
170
171 if self.CHECK_SAME_APP and not settings.local_mode:
172 calling_app_id = request.headers.get('X-Appengine-Inbound-Appid')
173 if calling_app_id != app_identity.get_application_id():
174 self.response.status = http_client.FORBIDDEN
175 return
176
177 self._CheckForMovedProject(mr, request)
178 self.AssertBasePermission(mr)
179
180 json_data = self.HandleRequest(mr)
181
182 self._RenderJsonResponse(json_data)
183
184 except query2ast.InvalidQueryError as e:
185 logging.warning('Trapped InvalidQueryError: %s', e)
186 logging.exception(e)
187 msg = e.message if e.message else 'invalid query'
188 self.abort(400, msg)
189 except permissions.PermissionException as e:
190 logging.info('Trapped PermissionException %s', e)
191 self.response.status = http_client.FORBIDDEN
192
193 # pylint: disable=unused-argument
194 # pylint: disable=arguments-differ
195 # Note: unused arguments necessary because they are specified in
196 # registerpages.py as an extra URL validation step even though we
197 # do our own URL parsing in monorailrequest.py
198 def get(self, **kwargs):
199 """Collect page-specific and generic info, then render the page.
200
201 Args:
202 project_name: string project name parsed from the URL by webapp2,
203 but we also parse it out in our code.
204 viewed_username: string user email parsed from the URL by webapp2,
205 but we also parse it out in our code.
206 hotlist_id: string hotlist id parsed from the URL by webapp2,
207 but we also parse it out in our code.
208 """
209 self._DoRequestHandling(self.mr.request, self.mr)
210
211 # pylint: disable=unused-argument
212 # pylint: disable=arguments-differ
213 def post(self, **kwargs):
214 """Parse the request, check base perms, and call form-specific code."""
215 self._DoRequestHandling(self.mr.request, self.mr)
216
217 def _RenderJsonResponse(self, json_data):
218 """Serialize the data as JSON so that it can be sent to the browser."""
219 json_str = json.dumps(json_data, indent=self.JSON_INDENT)
220 logging.debug(
221 'Sending JSON response: %r length: %r',
222 json_str[:framework_constants.LOGGING_MAX_LENGTH], len(json_str))
223 self.response.content_type = framework_constants.CONTENT_TYPE_JSON
224 self.response.headers['X-Content-Type-Options'] = (
225 framework_constants.CONTENT_TYPE_JSON_OPTIONS)
226 self.response.set_data(XSSI_PREFIX + json_str)
227
228
229class FlaskInternalTask(FlaskJsonFeed):
230 """Internal tasks are JSON feeds that can only be reached by our own code."""
231
232 CHECK_SECURITY_TOKEN = False