Line data Source code
1 : // Copyright (C) 2012 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : // WARNING: NoteDbUpdateManager cares about the package name RestApiServlet lives in.
16 : package com.google.gerrit.httpd.restapi;
17 :
18 : import static com.google.common.base.Preconditions.checkArgument;
19 : import static com.google.common.base.Preconditions.checkState;
20 : import static com.google.common.collect.ImmutableList.toImmutableList;
21 : import static com.google.common.flogger.LazyArgs.lazy;
22 : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
23 : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
24 : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
25 : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
26 : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_MAX_AGE;
27 : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
28 : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD;
29 : import static com.google.common.net.HttpHeaders.AUTHORIZATION;
30 : import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
31 : import static com.google.common.net.HttpHeaders.ORIGIN;
32 : import static com.google.common.net.HttpHeaders.VARY;
33 : import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_REMOVE_REVISION_ETAG;
34 : import static java.math.RoundingMode.CEILING;
35 : import static java.nio.charset.StandardCharsets.ISO_8859_1;
36 : import static java.nio.charset.StandardCharsets.UTF_8;
37 : import static java.util.Objects.requireNonNull;
38 : import static java.util.stream.Collectors.joining;
39 : import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
40 : import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
41 : import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
42 : import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
43 : import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
44 : import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
45 : import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
46 : import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
47 : import static javax.servlet.http.HttpServletResponse.SC_OK;
48 : import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
49 : import static javax.servlet.http.HttpServletResponse.SC_REQUEST_TIMEOUT;
50 :
51 : import com.google.common.annotations.VisibleForTesting;
52 : import com.google.common.base.Joiner;
53 : import com.google.common.base.Splitter;
54 : import com.google.common.base.Strings;
55 : import com.google.common.base.Throwables;
56 : import com.google.common.collect.ImmutableList;
57 : import com.google.common.collect.ImmutableListMultimap;
58 : import com.google.common.collect.ImmutableSet;
59 : import com.google.common.collect.Iterables;
60 : import com.google.common.collect.ListMultimap;
61 : import com.google.common.collect.Lists;
62 : import com.google.common.flogger.FluentLogger;
63 : import com.google.common.io.BaseEncoding;
64 : import com.google.common.io.CountingOutputStream;
65 : import com.google.common.math.IntMath;
66 : import com.google.common.net.HttpHeaders;
67 : import com.google.gerrit.common.Nullable;
68 : import com.google.gerrit.common.RawInputUtil;
69 : import com.google.gerrit.entities.Project;
70 : import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
71 : import com.google.gerrit.extensions.registration.DynamicItem;
72 : import com.google.gerrit.extensions.registration.DynamicMap;
73 : import com.google.gerrit.extensions.registration.DynamicSet;
74 : import com.google.gerrit.extensions.registration.PluginName;
75 : import com.google.gerrit.extensions.restapi.AuthException;
76 : import com.google.gerrit.extensions.restapi.BadRequestException;
77 : import com.google.gerrit.extensions.restapi.BinaryResult;
78 : import com.google.gerrit.extensions.restapi.CacheControl;
79 : import com.google.gerrit.extensions.restapi.DefaultInput;
80 : import com.google.gerrit.extensions.restapi.ETagView;
81 : import com.google.gerrit.extensions.restapi.IdString;
82 : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
83 : import com.google.gerrit.extensions.restapi.NeedsParams;
84 : import com.google.gerrit.extensions.restapi.NotImplementedException;
85 : import com.google.gerrit.extensions.restapi.PreconditionFailedException;
86 : import com.google.gerrit.extensions.restapi.RawInput;
87 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
88 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
89 : import com.google.gerrit.extensions.restapi.Response;
90 : import com.google.gerrit.extensions.restapi.RestApiException;
91 : import com.google.gerrit.extensions.restapi.RestCollection;
92 : import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
93 : import com.google.gerrit.extensions.restapi.RestCollectionDeleteMissingView;
94 : import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
95 : import com.google.gerrit.extensions.restapi.RestCollectionView;
96 : import com.google.gerrit.extensions.restapi.RestModifyView;
97 : import com.google.gerrit.extensions.restapi.RestReadView;
98 : import com.google.gerrit.extensions.restapi.RestResource;
99 : import com.google.gerrit.extensions.restapi.RestView;
100 : import com.google.gerrit.extensions.restapi.TopLevelResource;
101 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
102 : import com.google.gerrit.extensions.restapi.Url;
103 : import com.google.gerrit.httpd.WebSession;
104 : import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams;
105 : import com.google.gerrit.json.OutputFormat;
106 : import com.google.gerrit.server.AccessPath;
107 : import com.google.gerrit.server.AnonymousUser;
108 : import com.google.gerrit.server.CancellationMetrics;
109 : import com.google.gerrit.server.CurrentUser;
110 : import com.google.gerrit.server.DeadlineChecker;
111 : import com.google.gerrit.server.DynamicOptions;
112 : import com.google.gerrit.server.ExceptionHook;
113 : import com.google.gerrit.server.InvalidDeadlineException;
114 : import com.google.gerrit.server.OptionUtil;
115 : import com.google.gerrit.server.RequestInfo;
116 : import com.google.gerrit.server.RequestListener;
117 : import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
118 : import com.google.gerrit.server.cache.PerThreadCache;
119 : import com.google.gerrit.server.cancellation.RequestCancelledException;
120 : import com.google.gerrit.server.cancellation.RequestStateContext;
121 : import com.google.gerrit.server.cancellation.RequestStateProvider;
122 : import com.google.gerrit.server.change.ChangeFinder;
123 : import com.google.gerrit.server.change.RevisionResource;
124 : import com.google.gerrit.server.config.GerritServerConfig;
125 : import com.google.gerrit.server.experiments.ExperimentFeatures;
126 : import com.google.gerrit.server.group.GroupAuditService;
127 : import com.google.gerrit.server.logging.Metadata;
128 : import com.google.gerrit.server.logging.PerformanceLogContext;
129 : import com.google.gerrit.server.logging.PerformanceLogger;
130 : import com.google.gerrit.server.logging.RequestId;
131 : import com.google.gerrit.server.logging.TraceContext;
132 : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
133 : import com.google.gerrit.server.notedb.ChangeNotes;
134 : import com.google.gerrit.server.permissions.GlobalPermission;
135 : import com.google.gerrit.server.permissions.PermissionBackend;
136 : import com.google.gerrit.server.permissions.PermissionBackendException;
137 : import com.google.gerrit.server.plugincontext.PluginSetContext;
138 : import com.google.gerrit.server.quota.QuotaException;
139 : import com.google.gerrit.server.restapi.change.ChangesCollection;
140 : import com.google.gerrit.server.restapi.project.ProjectsCollection;
141 : import com.google.gerrit.server.update.RetryHelper;
142 : import com.google.gerrit.server.update.RetryableAction;
143 : import com.google.gerrit.server.update.RetryableAction.Action;
144 : import com.google.gerrit.server.update.RetryableAction.ActionType;
145 : import com.google.gerrit.server.util.time.TimeUtil;
146 : import com.google.gerrit.util.http.CacheHeaders;
147 : import com.google.gerrit.util.http.RequestUtil;
148 : import com.google.gson.ExclusionStrategy;
149 : import com.google.gson.FieldAttributes;
150 : import com.google.gson.FieldNamingPolicy;
151 : import com.google.gson.Gson;
152 : import com.google.gson.GsonBuilder;
153 : import com.google.gson.JsonElement;
154 : import com.google.gson.JsonParseException;
155 : import com.google.gson.stream.JsonReader;
156 : import com.google.gson.stream.JsonToken;
157 : import com.google.gson.stream.JsonWriter;
158 : import com.google.gson.stream.MalformedJsonException;
159 : import com.google.inject.Inject;
160 : import com.google.inject.Injector;
161 : import com.google.inject.Provider;
162 : import com.google.inject.TypeLiteral;
163 : import com.google.inject.util.Providers;
164 : import java.io.BufferedReader;
165 : import java.io.BufferedWriter;
166 : import java.io.ByteArrayOutputStream;
167 : import java.io.EOFException;
168 : import java.io.FilterOutputStream;
169 : import java.io.IOException;
170 : import java.io.InputStream;
171 : import java.io.OutputStream;
172 : import java.io.OutputStreamWriter;
173 : import java.io.Writer;
174 : import java.lang.reflect.Constructor;
175 : import java.lang.reflect.Field;
176 : import java.lang.reflect.InvocationTargetException;
177 : import java.lang.reflect.ParameterizedType;
178 : import java.lang.reflect.Type;
179 : import java.sql.Timestamp;
180 : import java.util.ArrayList;
181 : import java.util.Collections;
182 : import java.util.HashMap;
183 : import java.util.HashSet;
184 : import java.util.List;
185 : import java.util.Locale;
186 : import java.util.Map;
187 : import java.util.Optional;
188 : import java.util.Set;
189 : import java.util.TreeMap;
190 : import java.util.concurrent.TimeUnit;
191 : import java.util.concurrent.atomic.AtomicReference;
192 : import java.util.regex.Pattern;
193 : import java.util.stream.Stream;
194 : import java.util.zip.GZIPOutputStream;
195 : import javax.servlet.ServletException;
196 : import javax.servlet.http.HttpServlet;
197 : import javax.servlet.http.HttpServletRequest;
198 : import javax.servlet.http.HttpServletRequestWrapper;
199 : import javax.servlet.http.HttpServletResponse;
200 : import org.eclipse.jgit.http.server.ServletUtils;
201 : import org.eclipse.jgit.lib.Config;
202 : import org.eclipse.jgit.util.TemporaryBuffer;
203 : import org.eclipse.jgit.util.TemporaryBuffer.Heap;
204 :
205 : public class RestApiServlet extends HttpServlet {
206 : private static final long serialVersionUID = 1L;
207 :
208 100 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
209 :
210 : /** MIME type used for a JSON response body. */
211 : private static final String JSON_TYPE = "application/json";
212 :
213 : private static final String FORM_TYPE = "application/x-www-form-urlencoded";
214 :
215 : @VisibleForTesting public static final String X_GERRIT_DEADLINE = "X-Gerrit-Deadline";
216 : @VisibleForTesting public static final String X_GERRIT_TRACE = "X-Gerrit-Trace";
217 : @VisibleForTesting public static final String X_GERRIT_UPDATED_REF = "X-Gerrit-UpdatedRef";
218 :
219 : @VisibleForTesting
220 : public static final String X_GERRIT_UPDATED_REF_ENABLED = "X-Gerrit-UpdatedRef-Enabled";
221 :
222 : private static final String X_REQUESTED_WITH = "X-Requested-With";
223 : private static final String X_GERRIT_AUTH = "X-Gerrit-Auth";
224 100 : static final ImmutableSet<String> ALLOWED_CORS_METHODS =
225 100 : ImmutableSet.of("GET", "HEAD", "POST", "PUT", "DELETE");
226 100 : private static final ImmutableSet<String> ALLOWED_CORS_REQUEST_HEADERS =
227 100 : Stream.of(AUTHORIZATION, CONTENT_TYPE, X_GERRIT_AUTH, X_REQUESTED_WITH)
228 100 : .map(s -> s.toLowerCase(Locale.US))
229 100 : .collect(ImmutableSet.toImmutableSet());
230 :
231 : public static final String XD_AUTHORIZATION = "access_token";
232 : public static final String XD_CONTENT_TYPE = "$ct";
233 : public static final String XD_METHOD = "$m";
234 : public static final int SC_UNPROCESSABLE_ENTITY = 422;
235 : public static final int SC_TOO_MANY_REQUESTS = 429;
236 : public static final int SC_CLIENT_CLOSED_REQUEST = 499;
237 :
238 : private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
239 : private static final String PLAIN_TEXT = "text/plain";
240 100 : private static final Pattern TYPE_SPLIT_PATTERN = Pattern.compile("[ ,;][ ,;]*");
241 :
242 : /**
243 : * Garbage prefix inserted before JSON output to prevent XSSI.
244 : *
245 : * <p>This prefix is ")]}'\n" and is designed to prevent a web browser from executing the response
246 : * body if the resource URI were to be referenced using a <script src="...> HTML tag from
247 : * another web site. Clients using the HTTP interface will need to always strip the first line of
248 : * response data to remove this magic header.
249 : */
250 : public static final byte[] JSON_MAGIC;
251 :
252 : static {
253 100 : JSON_MAGIC = ")]}'\n".getBytes(UTF_8);
254 100 : }
255 :
256 : public static class Globals {
257 : final Provider<CurrentUser> currentUser;
258 : final DynamicItem<WebSession> webSession;
259 : final Provider<ParameterParser> paramParser;
260 : final PluginSetContext<RequestListener> requestListeners;
261 : final PermissionBackend permissionBackend;
262 : final GroupAuditService auditService;
263 : final RestApiMetrics metrics;
264 : final Pattern allowOrigin;
265 : final RestApiQuotaEnforcer quotaChecker;
266 : final Config config;
267 : final DynamicSet<PerformanceLogger> performanceLoggers;
268 : final ChangeFinder changeFinder;
269 : final RetryHelper retryHelper;
270 : final PluginSetContext<ExceptionHook> exceptionHooks;
271 : final Injector injector;
272 : final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
273 : final ExperimentFeatures experimentFeatures;
274 : final DeadlineChecker.Factory deadlineCheckerFactory;
275 : final CancellationMetrics cancellationMetrics;
276 :
277 : @Inject
278 : Globals(
279 : Provider<CurrentUser> currentUser,
280 : DynamicItem<WebSession> webSession,
281 : Provider<ParameterParser> paramParser,
282 : PluginSetContext<RequestListener> requestListeners,
283 : PermissionBackend permissionBackend,
284 : GroupAuditService auditService,
285 : RestApiMetrics metrics,
286 : RestApiQuotaEnforcer quotaChecker,
287 : @GerritServerConfig Config config,
288 : DynamicSet<PerformanceLogger> performanceLoggers,
289 : ChangeFinder changeFinder,
290 : RetryHelper retryHelper,
291 : PluginSetContext<ExceptionHook> exceptionHooks,
292 : Injector injector,
293 : DynamicMap<DynamicOptions.DynamicBean> dynamicBeans,
294 : ExperimentFeatures experimentFeatures,
295 : DeadlineChecker.Factory deadlineCheckerFactory,
296 99 : CancellationMetrics cancellationMetrics) {
297 99 : this.currentUser = currentUser;
298 99 : this.webSession = webSession;
299 99 : this.paramParser = paramParser;
300 99 : this.requestListeners = requestListeners;
301 99 : this.permissionBackend = permissionBackend;
302 99 : this.auditService = auditService;
303 99 : this.metrics = metrics;
304 99 : this.quotaChecker = quotaChecker;
305 99 : this.config = config;
306 99 : this.performanceLoggers = performanceLoggers;
307 99 : this.changeFinder = changeFinder;
308 99 : this.retryHelper = retryHelper;
309 99 : this.exceptionHooks = exceptionHooks;
310 99 : allowOrigin = makeAllowOrigin(config);
311 99 : this.injector = injector;
312 99 : this.dynamicBeans = dynamicBeans;
313 99 : this.experimentFeatures = experimentFeatures;
314 99 : this.deadlineCheckerFactory = deadlineCheckerFactory;
315 99 : this.cancellationMetrics = cancellationMetrics;
316 99 : }
317 :
318 : @Nullable
319 : private static Pattern makeAllowOrigin(Config cfg) {
320 99 : String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
321 99 : if (allow.length > 0) {
322 1 : return Pattern.compile(Joiner.on('|').join(allow));
323 : }
324 99 : return null;
325 : }
326 : }
327 :
328 : private final Globals globals;
329 : private final Provider<RestCollection<RestResource, RestResource>> members;
330 :
331 : public RestApiServlet(
332 : Globals globals, RestCollection<? extends RestResource, ? extends RestResource> members) {
333 99 : this(globals, Providers.of(members));
334 99 : }
335 :
336 : public RestApiServlet(
337 : Globals globals,
338 99 : Provider<? extends RestCollection<? extends RestResource, ? extends RestResource>> members) {
339 : @SuppressWarnings("unchecked")
340 99 : Provider<RestCollection<RestResource, RestResource>> n =
341 99 : (Provider<RestCollection<RestResource, RestResource>>) requireNonNull((Object) members);
342 99 : this.globals = globals;
343 99 : this.members = n;
344 99 : }
345 :
346 : @Override
347 : protected final void service(HttpServletRequest req, HttpServletResponse res)
348 : throws ServletException, IOException {
349 28 : final long startNanos = System.nanoTime();
350 28 : long auditStartTs = TimeUtil.nowMs();
351 28 : res.setHeader("Content-Disposition", "attachment");
352 28 : res.setHeader("X-Content-Type-Options", "nosniff");
353 28 : int statusCode = SC_OK;
354 28 : long responseBytes = -1;
355 28 : Optional<Exception> cause = Optional.empty();
356 28 : Response<?> response = null;
357 28 : QueryParams qp = null;
358 28 : Object inputRequestBody = null;
359 28 : RestResource rsrc = TopLevelResource.INSTANCE;
360 28 : ViewData viewData = null;
361 :
362 28 : try (TraceContext traceContext = enableTracing(req, res)) {
363 28 : String requestUri = requestUri(req);
364 :
365 28 : try (PerThreadCache ignored = PerThreadCache.create()) {
366 28 : List<IdString> path = splitPath(req);
367 28 : RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
368 28 : globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
369 :
370 : // It's important that the PerformanceLogContext is closed before the response is sent to
371 : // the client. Only this way it is ensured that the invocation of the PerformanceLogger
372 : // plugins happens before the client sees the response. This is needed for being able to
373 : // test performance logging from an acceptance test (see
374 : // TraceIT#performanceLoggingForRestCall()).
375 : try (RequestStateContext requestStateContext =
376 28 : RequestStateContext.open()
377 28 : .addRequestStateProvider(
378 28 : globals.deadlineCheckerFactory.create(
379 28 : requestInfo, req.getHeader(X_GERRIT_DEADLINE)));
380 28 : PerformanceLogContext performanceLogContext =
381 : new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
382 28 : traceRequestData(req);
383 :
384 28 : if (isCorsPreflight(req)) {
385 1 : doCorsPreflight(req, res);
386 1 : return;
387 : }
388 :
389 28 : qp = ParameterParser.getQueryParams(req);
390 28 : checkCors(req, res, qp.hasXdOverride());
391 28 : if (qp.hasXdOverride()) {
392 0 : req = applyXdOverrides(req, qp);
393 : }
394 28 : checkUserSession(req);
395 :
396 28 : RestCollection<RestResource, RestResource> rc = members.get();
397 28 : globals
398 : .permissionBackend
399 28 : .currentUser()
400 28 : .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
401 :
402 28 : viewData = new ViewData(null, null);
403 :
404 28 : if (path.isEmpty()) {
405 6 : globals.quotaChecker.enforce(req);
406 6 : if (rc instanceof NeedsParams) {
407 4 : ((NeedsParams) rc).setParams(qp.params());
408 : }
409 :
410 6 : if (isRead(req)) {
411 5 : viewData = new ViewData(null, rc.list());
412 4 : } else if (isPost(req)) {
413 4 : RestView<RestResource> restCollectionView =
414 4 : rc.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
415 4 : if (restCollectionView != null) {
416 3 : viewData = new ViewData(null, restCollectionView);
417 : } else {
418 1 : throw methodNotAllowed(req);
419 : }
420 3 : } else {
421 : // DELETE on root collections is not supported
422 1 : throw methodNotAllowed(req);
423 : }
424 : } else {
425 27 : IdString id = path.remove(0);
426 : try {
427 26 : rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, rc, rsrc, id);
428 26 : globals.quotaChecker.enforce(rsrc, req);
429 26 : if (path.isEmpty()) {
430 6 : checkPreconditions(req);
431 : }
432 8 : } catch (ResourceNotFoundException e) {
433 8 : if (!path.isEmpty()) {
434 4 : throw e;
435 : }
436 5 : globals.quotaChecker.enforce(req);
437 :
438 5 : if (isPost(req) || isPut(req)) {
439 5 : RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
440 5 : if (createView != null) {
441 5 : viewData = new ViewData(null, createView);
442 5 : path.add(id);
443 : } else {
444 0 : throw e;
445 : }
446 5 : } else if (isDelete(req)) {
447 0 : RestView<RestResource> deleteView =
448 0 : rc.views().get(PluginName.GERRIT, "DELETE_MISSING./");
449 0 : if (deleteView != null) {
450 0 : viewData = new ViewData(null, deleteView);
451 0 : path.add(id);
452 : } else {
453 0 : throw e;
454 : }
455 0 : } else {
456 0 : throw e;
457 : }
458 26 : }
459 27 : if (viewData.view == null) {
460 26 : viewData = view(rc, req.getMethod(), path);
461 : }
462 : }
463 28 : checkRequiresCapability(viewData);
464 :
465 28 : while (viewData.view instanceof RestCollection<?, ?>) {
466 : @SuppressWarnings("unchecked")
467 20 : RestCollection<RestResource, RestResource> c =
468 : (RestCollection<RestResource, RestResource>) viewData.view;
469 :
470 20 : if (path.isEmpty()) {
471 7 : if (isRead(req)) {
472 7 : viewData = new ViewData(null, c.list());
473 5 : } else if (isPost(req)) {
474 : // TODO: Here and on other collection methods: There is a bug that binds child views
475 : // with pluginName="gerrit" instead of the real plugin name. This has never worked
476 : // correctly and should be fixed where the binding gets created (DynamicMapProvider)
477 : // and here.
478 5 : RestView<RestResource> restCollectionView =
479 5 : c.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
480 5 : if (restCollectionView != null) {
481 4 : viewData = new ViewData(null, restCollectionView);
482 : } else {
483 1 : throw methodNotAllowed(req);
484 : }
485 5 : } else if (isDelete(req)) {
486 3 : RestView<RestResource> restCollectionView =
487 3 : c.views().get(PluginName.GERRIT, "DELETE_ON_COLLECTION./");
488 3 : if (restCollectionView != null) {
489 3 : viewData = new ViewData(null, restCollectionView);
490 : } else {
491 1 : throw methodNotAllowed(req);
492 : }
493 3 : } else {
494 1 : throw methodNotAllowed(req);
495 : }
496 : break;
497 : }
498 20 : IdString id = path.remove(0);
499 : try {
500 17 : rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, c, rsrc, id);
501 17 : checkPreconditions(req);
502 17 : viewData = new ViewData(null, null);
503 10 : } catch (ResourceNotFoundException e) {
504 10 : if (!path.isEmpty()) {
505 3 : throw e;
506 : }
507 :
508 9 : if (isPost(req) || isPut(req)) {
509 7 : RestView<RestResource> createView = c.views().get(PluginName.GERRIT, "CREATE./");
510 7 : if (createView != null) {
511 7 : viewData = new ViewData(viewData.pluginName, createView);
512 7 : path.add(id);
513 : } else {
514 0 : throw e;
515 : }
516 9 : } else if (isDelete(req)) {
517 4 : RestView<RestResource> deleteView =
518 4 : c.views().get(PluginName.GERRIT, "DELETE_MISSING./");
519 4 : if (deleteView != null) {
520 2 : viewData = new ViewData(viewData.pluginName, deleteView);
521 2 : path.add(id);
522 : } else {
523 2 : throw e;
524 : }
525 2 : } else {
526 2 : throw e;
527 : }
528 17 : }
529 20 : if (viewData.view == null) {
530 17 : viewData = view(c, req.getMethod(), path);
531 : }
532 20 : checkRequiresCapability(viewData);
533 20 : }
534 :
535 27 : if (notModified(req, traceContext, viewData, rsrc)) {
536 0 : logger.atFinest().log("REST call succeeded: %d", SC_NOT_MODIFIED);
537 0 : res.sendError(SC_NOT_MODIFIED);
538 0 : return;
539 : }
540 :
541 27 : try (DynamicOptions pluginOptions =
542 : new DynamicOptions(globals.injector, globals.dynamicBeans)) {
543 27 : if (!globals
544 : .paramParser
545 27 : .get()
546 27 : .parse(viewData.view, pluginOptions, qp.params(), req, res)) {
547 4 : return;
548 : }
549 :
550 26 : if (viewData.view instanceof RestReadView<?> && isRead(req)) {
551 17 : response =
552 17 : invokeRestReadViewWithRetry(
553 : req,
554 : traceContext,
555 : viewData,
556 : (RestReadView<RestResource>) viewData.view,
557 : rsrc);
558 19 : } else if (viewData.view instanceof RestModifyView<?, ?>) {
559 : @SuppressWarnings("unchecked")
560 15 : RestModifyView<RestResource, Object> m =
561 : (RestModifyView<RestResource, Object>) viewData.view;
562 :
563 15 : Type type = inputType(m);
564 15 : inputRequestBody = parseRequest(req, type);
565 15 : response =
566 14 : invokeRestModifyViewWithRetry(
567 : req, traceContext, viewData, m, rsrc, inputRequestBody);
568 :
569 14 : if (inputRequestBody instanceof RawInput) {
570 0 : try (InputStream is = req.getInputStream()) {
571 0 : ServletUtils.consumeRequestBody(is);
572 : }
573 : }
574 19 : } else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
575 : @SuppressWarnings("unchecked")
576 9 : RestCollectionCreateView<RestResource, RestResource, Object> m =
577 : (RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
578 :
579 9 : Type type = inputType(m);
580 9 : inputRequestBody = parseRequest(req, type);
581 9 : response =
582 8 : invokeRestCollectionCreateViewWithRetry(
583 9 : req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
584 8 : if (inputRequestBody instanceof RawInput) {
585 0 : try (InputStream is = req.getInputStream()) {
586 0 : ServletUtils.consumeRequestBody(is);
587 : }
588 : }
589 11 : } else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
590 : @SuppressWarnings("unchecked")
591 2 : RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
592 : (RestCollectionDeleteMissingView<RestResource, RestResource, Object>)
593 : viewData.view;
594 :
595 2 : Type type = inputType(m);
596 2 : inputRequestBody = parseRequest(req, type);
597 2 : response =
598 2 : invokeRestCollectionDeleteMissingViewWithRetry(
599 2 : req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
600 2 : if (inputRequestBody instanceof RawInput) {
601 0 : try (InputStream is = req.getInputStream()) {
602 0 : ServletUtils.consumeRequestBody(is);
603 : }
604 : }
605 6 : } else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
606 : @SuppressWarnings("unchecked")
607 6 : RestCollectionModifyView<RestResource, RestResource, Object> m =
608 : (RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
609 :
610 6 : Type type = inputType(m);
611 6 : inputRequestBody = parseRequest(req, type);
612 6 : response =
613 6 : invokeRestCollectionModifyViewWithRetry(
614 : req, traceContext, viewData, m, rsrc, inputRequestBody);
615 6 : if (inputRequestBody instanceof RawInput) {
616 0 : try (InputStream is = req.getInputStream()) {
617 0 : ServletUtils.consumeRequestBody(is);
618 : }
619 : }
620 6 : } else {
621 0 : throw new ResourceNotFoundException();
622 : }
623 26 : String isUpdatedRefEnabled = req.getHeader(X_GERRIT_UPDATED_REF_ENABLED);
624 26 : if (!Strings.isNullOrEmpty(isUpdatedRefEnabled)
625 1 : && Boolean.valueOf(isUpdatedRefEnabled)) {
626 1 : setXGerritUpdatedRefResponseHeaders(req, res);
627 : }
628 :
629 26 : if (response instanceof Response.Redirect) {
630 0 : CacheHeaders.setNotCacheable(res);
631 0 : String location = ((Response.Redirect) response).location();
632 0 : res.sendRedirect(location);
633 0 : logger.atFinest().log("REST call redirected to: %s", location);
634 0 : return;
635 26 : } else if (response instanceof Response.Accepted) {
636 1 : CacheHeaders.setNotCacheable(res);
637 1 : res.setStatus(response.statusCode());
638 1 : res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) response).location());
639 1 : logger.atFinest().log("REST call succeeded: %d", response.statusCode());
640 1 : return;
641 : }
642 :
643 26 : statusCode = response.statusCode();
644 26 : configureCaching(req, res, traceContext, rsrc, viewData, response.caching());
645 26 : res.setStatus(statusCode);
646 26 : logger.atFinest().log("REST call succeeded: %d", statusCode);
647 4 : }
648 :
649 26 : if (response != Response.none()) {
650 24 : Object value = Response.unwrap(response);
651 24 : if (value instanceof BinaryResult) {
652 8 : responseBytes = replyBinaryResult(req, res, (BinaryResult) value);
653 : } else {
654 22 : responseBytes = replyJson(req, res, false, qp.config(), value);
655 : }
656 : }
657 4 : }
658 4 : } catch (MalformedJsonException | JsonParseException e) {
659 0 : cause = Optional.of(e);
660 0 : logger.atFine().withCause(e).log("REST call failed on JSON parsing");
661 0 : responseBytes =
662 0 : replyError(
663 : req, res, statusCode = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
664 7 : } catch (BadRequestException e) {
665 7 : cause = Optional.of(e);
666 7 : responseBytes =
667 7 : replyError(
668 7 : req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
669 6 : } catch (AuthException e) {
670 6 : cause = Optional.of(e);
671 6 : responseBytes =
672 6 : replyError(
673 6 : req, res, statusCode = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e);
674 0 : } catch (AmbiguousViewException e) {
675 0 : cause = Optional.of(e);
676 0 : responseBytes =
677 0 : replyError(req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
678 12 : } catch (ResourceNotFoundException e) {
679 12 : cause = Optional.of(e);
680 12 : responseBytes =
681 12 : replyError(
682 12 : req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
683 3 : } catch (MethodNotAllowedException e) {
684 3 : cause = Optional.of(e);
685 3 : responseBytes =
686 3 : replyError(
687 : req,
688 : res,
689 : statusCode = SC_METHOD_NOT_ALLOWED,
690 3 : messageOr(e, "Method Not Allowed"),
691 3 : e.caching(),
692 : e);
693 4 : } catch (ResourceConflictException e) {
694 4 : cause = Optional.of(e);
695 4 : responseBytes =
696 4 : replyError(
697 4 : req, res, statusCode = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
698 2 : } catch (PreconditionFailedException e) {
699 2 : cause = Optional.of(e);
700 2 : responseBytes =
701 2 : replyError(
702 : req,
703 : res,
704 : statusCode = SC_PRECONDITION_FAILED,
705 2 : messageOr(e, "Precondition Failed"),
706 2 : e.caching(),
707 : e);
708 3 : } catch (UnprocessableEntityException e) {
709 3 : cause = Optional.of(e);
710 3 : responseBytes =
711 3 : replyError(
712 : req,
713 : res,
714 : statusCode = SC_UNPROCESSABLE_ENTITY,
715 3 : messageOr(e, "Unprocessable Entity"),
716 3 : e.caching(),
717 : e);
718 0 : } catch (NotImplementedException e) {
719 0 : cause = Optional.of(e);
720 0 : logger.atSevere().withCause(e).log("Error in %s %s", req.getMethod(), uriForLogging(req));
721 0 : responseBytes =
722 0 : replyError(
723 0 : req, res, statusCode = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
724 1 : } catch (QuotaException e) {
725 1 : cause = Optional.of(e);
726 1 : responseBytes =
727 1 : replyError(
728 : req,
729 : res,
730 : statusCode = SC_TOO_MANY_REQUESTS,
731 1 : messageOr(e, "Quota limit reached"),
732 1 : e.caching(),
733 : e);
734 1 : } catch (InvalidDeadlineException e) {
735 1 : cause = Optional.of(e);
736 1 : responseBytes =
737 1 : replyError(req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e);
738 5 : } catch (Exception e) {
739 5 : cause = Optional.of(e);
740 :
741 5 : Optional<RequestCancelledException> requestCancelledException =
742 5 : RequestCancelledException.getFromCausalChain(e);
743 5 : if (requestCancelledException.isPresent()) {
744 1 : RequestStateProvider.Reason cancellationReason =
745 1 : requestCancelledException.get().getCancellationReason();
746 1 : globals.cancellationMetrics.countCancelledRequest(
747 : RequestInfo.RequestType.REST, requestUri, cancellationReason);
748 1 : statusCode = getCancellationStatusCode(cancellationReason);
749 1 : responseBytes =
750 1 : replyError(
751 1 : req, res, statusCode, getCancellationMessage(requestCancelledException.get()), e);
752 1 : } else {
753 5 : statusCode = SC_INTERNAL_SERVER_ERROR;
754 :
755 5 : Optional<ExceptionHook.Status> status = getStatus(e);
756 5 : statusCode =
757 5 : status.map(ExceptionHook.Status::statusCode).orElse(SC_INTERNAL_SERVER_ERROR);
758 :
759 5 : if (res.isCommitted()) {
760 4 : responseBytes = 0;
761 4 : if (statusCode == SC_INTERNAL_SERVER_ERROR) {
762 4 : logger.atSevere().withCause(e).log(
763 : "Error in %s %s, response already committed",
764 4 : req.getMethod(), uriForLogging(req));
765 : } else {
766 0 : logger.atWarning().log(
767 : "Response for %s %s already committed, wanted to set status %d",
768 0 : req.getMethod(), uriForLogging(req), statusCode);
769 : }
770 : } else {
771 2 : res.reset();
772 2 : TraceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
773 :
774 2 : if (status.isPresent()) {
775 1 : responseBytes = reply(req, res, e, status.get(), getUserMessages(e));
776 : } else {
777 1 : responseBytes = replyInternalServerError(req, res, e, getUserMessages(e));
778 : }
779 : }
780 : }
781 : } finally {
782 28 : String metric = getViewName(viewData);
783 28 : String formattedCause = cause.map(globals.retryHelper::formatCause).orElse("_none");
784 28 : globals.metrics.count.increment(metric);
785 28 : if (statusCode >= SC_BAD_REQUEST) {
786 20 : globals.metrics.errorCount.increment(metric, statusCode, formattedCause);
787 : }
788 28 : if (responseBytes != -1) {
789 26 : globals.metrics.responseBytes.record(metric, responseBytes);
790 : }
791 28 : globals.metrics.serverLatency.record(
792 28 : metric, System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
793 28 : globals.auditService.dispatch(
794 : new ExtendedHttpAuditEvent(
795 28 : globals.webSession.get().getSessionId(),
796 28 : globals.currentUser.get(),
797 : req,
798 : auditStartTs,
799 28 : qp != null ? qp.params() : ImmutableListMultimap.of(),
800 : inputRequestBody,
801 : statusCode,
802 : response,
803 : rsrc,
804 28 : viewData == null ? null : viewData.view));
805 : }
806 4 : }
807 27 : }
808 :
809 : /**
810 : * Fill in the refs that were updated during this request in the response header. The updated refs
811 : * will be in the form of "project~ref~updated_SHA-1".
812 : */
813 : private void setXGerritUpdatedRefResponseHeaders(
814 : HttpServletRequest request, HttpServletResponse response) {
815 : for (GitReferenceUpdatedListener.Event refUpdate :
816 1 : globals.webSession.get().getRefUpdatedEvents()) {
817 1 : String refUpdateFormat =
818 1 : String.format(
819 : "%s~%s~%s~%s",
820 : // encode the project and ref names since they may contain `~`
821 1 : Url.encode(refUpdate.getProjectName()),
822 1 : Url.encode(refUpdate.getRefName()),
823 1 : refUpdate.getOldObjectId(),
824 1 : refUpdate.getNewObjectId());
825 :
826 1 : if (isRead(request)) {
827 0 : logger.atWarning().log(
828 : "request %s performed a ref update %s although the request is a READ request",
829 0 : request.getRequestURL(), refUpdateFormat);
830 : }
831 1 : response.addHeader(X_GERRIT_UPDATED_REF, refUpdateFormat);
832 1 : }
833 1 : globals.webSession.get().resetRefUpdatedEvents();
834 1 : }
835 :
836 : private String getEtagWithRetry(
837 : HttpServletRequest req,
838 : TraceContext traceContext,
839 : ViewData viewData,
840 : ETagView<RestResource> view,
841 : RestResource rsrc) {
842 2 : try (TraceTimer ignored =
843 2 : TraceContext.newTimer(
844 : "RestApiServlet#getEtagWithRetry:view",
845 2 : Metadata.builder().restViewName(getViewName(viewData)).build())) {
846 2 : return invokeRestEndpointWithRetry(
847 : req,
848 : traceContext,
849 2 : getViewName(viewData) + "#etag",
850 : ActionType.REST_READ_REQUEST,
851 2 : () -> view.getETag(rsrc));
852 0 : } catch (Exception e) {
853 0 : Throwables.throwIfUnchecked(e);
854 0 : throw new IllegalStateException("Failed to get ETag for view", e);
855 : }
856 : }
857 :
858 : @Nullable
859 : private String getEtagWithRetry(
860 : HttpServletRequest req, TraceContext traceContext, RestResource.HasETag rsrc) {
861 4 : try (TraceTimer ignored =
862 4 : TraceContext.newTimer(
863 : "RestApiServlet#getEtagWithRetry:resource",
864 4 : Metadata.builder().restViewName(rsrc.getClass().getSimpleName()).build())) {
865 4 : if (rsrc instanceof RevisionResource
866 2 : && globals.experimentFeatures.isFeatureEnabled(
867 : GERRIT_BACKEND_REQUEST_FEATURE_REMOVE_REVISION_ETAG)) {
868 0 : return null;
869 : }
870 4 : return invokeRestEndpointWithRetry(
871 : req,
872 : traceContext,
873 4 : rsrc.getClass().getSimpleName() + "#etag",
874 : ActionType.REST_READ_REQUEST,
875 4 : () -> rsrc.getETag());
876 0 : } catch (Exception e) {
877 0 : Throwables.throwIfUnchecked(e);
878 0 : throw new IllegalStateException("Failed to get ETag for resource", e);
879 : }
880 : }
881 :
882 : private RestResource parseResourceWithRetry(
883 : HttpServletRequest req,
884 : TraceContext traceContext,
885 : @Nullable String pluginName,
886 : RestCollection<RestResource, RestResource> restCollection,
887 : RestResource parentResource,
888 : IdString id)
889 : throws Exception {
890 27 : return invokeRestEndpointWithRetry(
891 : req,
892 : traceContext,
893 27 : globals.metrics.view(restCollection.getClass(), pluginName) + "#parse",
894 : ActionType.REST_READ_REQUEST,
895 26 : () -> restCollection.parse(parentResource, id));
896 : }
897 :
898 : private Response<?> invokeRestReadViewWithRetry(
899 : HttpServletRequest req,
900 : TraceContext traceContext,
901 : ViewData viewData,
902 : RestReadView<RestResource> view,
903 : RestResource rsrc)
904 : throws Exception {
905 17 : return invokeRestEndpointWithRetry(
906 : req,
907 : traceContext,
908 17 : getViewName(viewData),
909 : ActionType.REST_READ_REQUEST,
910 17 : () -> view.apply(rsrc));
911 : }
912 :
913 : private Response<?> invokeRestModifyViewWithRetry(
914 : HttpServletRequest req,
915 : TraceContext traceContext,
916 : ViewData viewData,
917 : RestModifyView<RestResource, Object> view,
918 : RestResource rsrc,
919 : Object inputRequestBody)
920 : throws Exception {
921 15 : return invokeRestEndpointWithRetry(
922 : req,
923 : traceContext,
924 15 : getViewName(viewData),
925 : ActionType.REST_WRITE_REQUEST,
926 14 : () -> view.apply(rsrc, inputRequestBody));
927 : }
928 :
929 : private Response<?> invokeRestCollectionCreateViewWithRetry(
930 : HttpServletRequest req,
931 : TraceContext traceContext,
932 : ViewData viewData,
933 : RestCollectionCreateView<RestResource, RestResource, Object> view,
934 : RestResource rsrc,
935 : IdString path,
936 : Object inputRequestBody)
937 : throws Exception {
938 9 : return invokeRestEndpointWithRetry(
939 : req,
940 : traceContext,
941 9 : getViewName(viewData),
942 : ActionType.REST_WRITE_REQUEST,
943 8 : () -> view.apply(rsrc, path, inputRequestBody));
944 : }
945 :
946 : private Response<?> invokeRestCollectionDeleteMissingViewWithRetry(
947 : HttpServletRequest req,
948 : TraceContext traceContext,
949 : ViewData viewData,
950 : RestCollectionDeleteMissingView<RestResource, RestResource, Object> view,
951 : RestResource rsrc,
952 : IdString path,
953 : Object inputRequestBody)
954 : throws Exception {
955 2 : return invokeRestEndpointWithRetry(
956 : req,
957 : traceContext,
958 2 : getViewName(viewData),
959 : ActionType.REST_WRITE_REQUEST,
960 2 : () -> view.apply(rsrc, path, inputRequestBody));
961 : }
962 :
963 : private Response<?> invokeRestCollectionModifyViewWithRetry(
964 : HttpServletRequest req,
965 : TraceContext traceContext,
966 : ViewData viewData,
967 : RestCollectionModifyView<RestResource, RestResource, Object> view,
968 : RestResource rsrc,
969 : Object inputRequestBody)
970 : throws Exception {
971 6 : return invokeRestEndpointWithRetry(
972 : req,
973 : traceContext,
974 6 : getViewName(viewData),
975 : ActionType.REST_WRITE_REQUEST,
976 6 : () -> view.apply(rsrc, inputRequestBody));
977 : }
978 :
979 : private <T> T invokeRestEndpointWithRetry(
980 : HttpServletRequest req,
981 : TraceContext traceContext,
982 : String caller,
983 : ActionType actionType,
984 : Action<T> action)
985 : throws Exception {
986 28 : RetryableAction<T> retryableAction = globals.retryHelper.action(actionType, caller, action);
987 28 : AtomicReference<Optional<String>> traceId = new AtomicReference<>(Optional.empty());
988 28 : if (!TraceContext.isTracing()) {
989 : // enable automatic retry with tracing in case of non-recoverable failure
990 28 : retryableAction
991 28 : .retryWithTrace(t -> !(t instanceof RestApiException))
992 28 : .onAutoTrace(
993 : autoTraceId -> {
994 1 : traceId.set(Optional.of(autoTraceId));
995 :
996 : // Include details of the request into the trace.
997 1 : traceRequestData(req);
998 1 : });
999 : }
1000 : try {
1001 28 : return retryableAction.call();
1002 : } finally {
1003 : // If auto-tracing got triggered due to a non-recoverable failure, also trace the rest of
1004 : // this request. This means logging is forced for all further log statements and the logs are
1005 : // associated with the same trace ID.
1006 28 : traceId
1007 28 : .get()
1008 28 : .ifPresent(tid -> traceContext.addTag(RequestId.Type.TRACE_ID, tid).forceLogging());
1009 : }
1010 : }
1011 :
1012 : private String getViewName(ViewData viewData) {
1013 28 : return viewData != null && viewData.view != null ? globals.metrics.view(viewData) : "_unknown";
1014 : }
1015 :
1016 : private static HttpServletRequest applyXdOverrides(HttpServletRequest req, QueryParams qp)
1017 : throws BadRequestException {
1018 0 : if (!isPost(req)) {
1019 0 : throw new BadRequestException("POST required");
1020 : }
1021 :
1022 0 : String method = qp.xdMethod();
1023 0 : String contentType = qp.xdContentType();
1024 0 : if (method.equals("POST") || method.equals("PUT")) {
1025 0 : if (!isType(PLAIN_TEXT, req.getContentType())) {
1026 0 : throw new BadRequestException("invalid " + CONTENT_TYPE);
1027 : }
1028 0 : if (Strings.isNullOrEmpty(contentType)) {
1029 0 : throw new BadRequestException(XD_CONTENT_TYPE + " required");
1030 : }
1031 : }
1032 :
1033 0 : return new HttpServletRequestWrapper(req) {
1034 : @Override
1035 : public String getMethod() {
1036 0 : return method;
1037 : }
1038 :
1039 : @Override
1040 : public String getContentType() {
1041 0 : return contentType;
1042 : }
1043 : };
1044 : }
1045 :
1046 : private void checkCors(HttpServletRequest req, HttpServletResponse res, boolean isXd)
1047 : throws BadRequestException {
1048 28 : String origin = req.getHeader(ORIGIN);
1049 28 : if (isXd) {
1050 : // Cross-domain, non-preflighted requests must come from an approved origin.
1051 1 : if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) {
1052 1 : throw new BadRequestException("origin not allowed");
1053 : }
1054 0 : res.addHeader(VARY, ORIGIN);
1055 0 : res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
1056 0 : res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
1057 28 : } else if (!Strings.isNullOrEmpty(origin)) {
1058 : // All other requests must be processed, but conditionally set CORS headers.
1059 2 : if (globals.allowOrigin != null) {
1060 1 : res.addHeader(VARY, ORIGIN);
1061 : }
1062 2 : if (isOriginAllowed(origin)) {
1063 1 : setCorsHeaders(res, origin);
1064 : }
1065 : }
1066 28 : }
1067 :
1068 : private static boolean isCorsPreflight(HttpServletRequest req) {
1069 28 : return "OPTIONS".equals(req.getMethod())
1070 1 : && !Strings.isNullOrEmpty(req.getHeader(ORIGIN))
1071 28 : && !Strings.isNullOrEmpty(req.getHeader(ACCESS_CONTROL_REQUEST_METHOD));
1072 : }
1073 :
1074 : private void doCorsPreflight(HttpServletRequest req, HttpServletResponse res)
1075 : throws BadRequestException {
1076 1 : CacheHeaders.setNotCacheable(res);
1077 1 : setHeaderList(
1078 : res,
1079 : VARY,
1080 1 : ImmutableList.of(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS));
1081 :
1082 1 : String origin = req.getHeader(ORIGIN);
1083 1 : if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) {
1084 1 : throw new BadRequestException("CORS not allowed");
1085 : }
1086 :
1087 1 : String method = req.getHeader(ACCESS_CONTROL_REQUEST_METHOD);
1088 1 : if (!ALLOWED_CORS_METHODS.contains(method)) {
1089 1 : throw new BadRequestException(method + " not allowed in CORS");
1090 : }
1091 :
1092 1 : String headers = req.getHeader(ACCESS_CONTROL_REQUEST_HEADERS);
1093 1 : if (headers != null) {
1094 1 : for (String reqHdr : Splitter.on(',').trimResults().split(headers)) {
1095 1 : if (!ALLOWED_CORS_REQUEST_HEADERS.contains(reqHdr.toLowerCase(Locale.US))) {
1096 1 : throw new BadRequestException(reqHdr + " not allowed in CORS");
1097 : }
1098 1 : }
1099 : }
1100 :
1101 1 : res.setStatus(SC_OK);
1102 1 : setCorsHeaders(res, origin);
1103 1 : res.setContentType(PLAIN_TEXT);
1104 1 : res.setContentLength(0);
1105 1 : }
1106 :
1107 : private static void setCorsHeaders(HttpServletResponse res, String origin) {
1108 1 : res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
1109 1 : res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
1110 1 : res.setHeader(ACCESS_CONTROL_MAX_AGE, "600");
1111 1 : setHeaderList(
1112 : res,
1113 : ACCESS_CONTROL_ALLOW_METHODS,
1114 1 : Iterables.concat(ALLOWED_CORS_METHODS, ImmutableList.of("OPTIONS")));
1115 1 : setHeaderList(res, ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_REQUEST_HEADERS);
1116 1 : }
1117 :
1118 : private static void setHeaderList(HttpServletResponse res, String name, Iterable<String> values) {
1119 1 : res.setHeader(name, Joiner.on(", ").join(values));
1120 1 : }
1121 :
1122 : private boolean isOriginAllowed(String origin) {
1123 2 : return globals.allowOrigin != null && globals.allowOrigin.matcher(origin).matches();
1124 : }
1125 :
1126 : private static String messageOr(Throwable t, String defaultMessage) {
1127 19 : if (!Strings.isNullOrEmpty(t.getMessage())) {
1128 19 : return t.getMessage();
1129 : }
1130 2 : return defaultMessage;
1131 : }
1132 :
1133 : private boolean notModified(
1134 : HttpServletRequest req, TraceContext traceContext, ViewData viewData, RestResource rsrc) {
1135 27 : if (!isRead(req)) {
1136 19 : return false;
1137 : }
1138 :
1139 18 : RestView<RestResource> view = viewData.view;
1140 18 : if (view instanceof ETagView) {
1141 3 : String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
1142 3 : if (have != null) {
1143 0 : String eTag =
1144 0 : getEtagWithRetry(req, traceContext, viewData, (ETagView<RestResource>) view, rsrc);
1145 0 : return have.equals(eTag);
1146 : }
1147 : }
1148 :
1149 18 : if (rsrc instanceof RestResource.HasETag) {
1150 8 : String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
1151 8 : if (!Strings.isNullOrEmpty(have)) {
1152 0 : String eTag = getEtagWithRetry(req, traceContext, (RestResource.HasETag) rsrc);
1153 0 : return have.equals(eTag);
1154 : }
1155 : }
1156 :
1157 18 : if (rsrc instanceof RestResource.HasLastModified) {
1158 0 : Timestamp m = ((RestResource.HasLastModified) rsrc).getLastModified();
1159 0 : long d = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
1160 :
1161 : // HTTP times are in seconds, database may have millisecond precision.
1162 0 : return d / 1000L == m.getTime() / 1000L;
1163 : }
1164 18 : return false;
1165 : }
1166 :
1167 : private <R extends RestResource> void configureCaching(
1168 : HttpServletRequest req,
1169 : HttpServletResponse res,
1170 : TraceContext traceContext,
1171 : R rsrc,
1172 : ViewData viewData,
1173 : CacheControl cacheControl) {
1174 26 : setCacheHeaders(req, res, cacheControl);
1175 26 : if (isRead(req)) {
1176 17 : switch (cacheControl.getType()) {
1177 : case NONE:
1178 : default:
1179 17 : break;
1180 : case PRIVATE:
1181 7 : addResourceStateHeaders(req, res, traceContext, viewData, rsrc);
1182 7 : break;
1183 : case PUBLIC:
1184 0 : addResourceStateHeaders(req, res, traceContext, viewData, rsrc);
1185 : break;
1186 : }
1187 : }
1188 26 : }
1189 :
1190 : private static <R extends RestResource> void setCacheHeaders(
1191 : HttpServletRequest req, HttpServletResponse res, CacheControl cacheControl) {
1192 28 : if (isRead(req)) {
1193 19 : switch (cacheControl.getType()) {
1194 : case NONE:
1195 : default:
1196 19 : CacheHeaders.setNotCacheable(res);
1197 19 : break;
1198 : case PRIVATE:
1199 7 : CacheHeaders.setCacheablePrivate(
1200 7 : res, cacheControl.getAge(), cacheControl.getUnit(), cacheControl.isMustRevalidate());
1201 7 : break;
1202 : case PUBLIC:
1203 0 : CacheHeaders.setCacheable(
1204 : req,
1205 : res,
1206 0 : cacheControl.getAge(),
1207 0 : cacheControl.getUnit(),
1208 0 : cacheControl.isMustRevalidate());
1209 0 : break;
1210 : }
1211 : } else {
1212 19 : CacheHeaders.setNotCacheable(res);
1213 : }
1214 28 : }
1215 :
1216 : private void addResourceStateHeaders(
1217 : HttpServletRequest req,
1218 : HttpServletResponse res,
1219 : TraceContext traceContext,
1220 : ViewData viewData,
1221 : RestResource rsrc) {
1222 7 : RestView<RestResource> view = viewData.view;
1223 7 : if (view instanceof ETagView) {
1224 2 : String eTag =
1225 2 : getEtagWithRetry(req, traceContext, viewData, (ETagView<RestResource>) view, rsrc);
1226 2 : res.setHeader(HttpHeaders.ETAG, eTag);
1227 7 : } else if (rsrc instanceof RestResource.HasETag) {
1228 4 : String eTag = getEtagWithRetry(req, traceContext, (RestResource.HasETag) rsrc);
1229 4 : if (!Strings.isNullOrEmpty(eTag)) {
1230 4 : res.setHeader(HttpHeaders.ETAG, eTag);
1231 : }
1232 : }
1233 7 : if (rsrc instanceof RestResource.HasLastModified) {
1234 0 : res.setDateHeader(
1235 : HttpHeaders.LAST_MODIFIED,
1236 0 : ((RestResource.HasLastModified) rsrc).getLastModified().getTime());
1237 : }
1238 7 : }
1239 :
1240 : private void checkPreconditions(HttpServletRequest req) throws PreconditionFailedException {
1241 19 : if ("*".equals(req.getHeader(HttpHeaders.IF_NONE_MATCH))) {
1242 1 : throw new PreconditionFailedException("Resource already exists");
1243 : }
1244 19 : }
1245 :
1246 : private static Type inputType(RestModifyView<RestResource, Object> m) {
1247 : // MyModifyView implements RestModifyView<SomeResource, MyInput>
1248 15 : TypeLiteral<?> typeLiteral = TypeLiteral.get(m.getClass());
1249 :
1250 : // RestModifyView<SomeResource, MyInput>
1251 : // This is smart enough to resolve even when there are intervening subclasses, even if they have
1252 : // reordered type arguments.
1253 15 : TypeLiteral<?> supertypeLiteral = typeLiteral.getSupertype(RestModifyView.class);
1254 :
1255 15 : Type supertype = supertypeLiteral.getType();
1256 15 : checkState(
1257 : supertype instanceof ParameterizedType,
1258 : "supertype of %s is not parameterized: %s",
1259 : typeLiteral,
1260 : supertypeLiteral);
1261 15 : return ((ParameterizedType) supertype).getActualTypeArguments()[1];
1262 : }
1263 :
1264 : private static Type inputType(RestCollectionView<RestResource, RestResource, Object> m) {
1265 : // MyCollectionView implements RestCollectionView<SomeResource, SomeResource, MyInput>
1266 12 : TypeLiteral<?> typeLiteral = TypeLiteral.get(m.getClass());
1267 :
1268 : // RestCollectionView<SomeResource, SomeResource, MyInput>
1269 : // This is smart enough to resolve even when there are intervening subclasses, even if they have
1270 : // reordered type arguments.
1271 12 : TypeLiteral<?> supertypeLiteral = typeLiteral.getSupertype(RestCollectionView.class);
1272 :
1273 12 : Type supertype = supertypeLiteral.getType();
1274 12 : checkState(
1275 : supertype instanceof ParameterizedType,
1276 : "supertype of %s is not parameterized: %s",
1277 : typeLiteral,
1278 : supertypeLiteral);
1279 12 : return ((ParameterizedType) supertype).getActualTypeArguments()[2];
1280 : }
1281 :
1282 : @Nullable
1283 : private Object parseRequest(HttpServletRequest req, Type type)
1284 : throws IOException, BadRequestException, SecurityException, IllegalArgumentException,
1285 : NoSuchMethodException, IllegalAccessException, InstantiationException,
1286 : InvocationTargetException, MethodNotAllowedException {
1287 : // HTTP/1.1 requires consuming the request body before writing non-error response (less than
1288 : // 400). Consume the request body for all but raw input request types here.
1289 19 : if (isType(JSON_TYPE, req.getContentType())) {
1290 12 : try (BufferedReader br = req.getReader();
1291 12 : JsonReader json = new JsonReader(br)) {
1292 : try {
1293 12 : json.setLenient(true);
1294 :
1295 : JsonToken first;
1296 : try {
1297 12 : first = json.peek();
1298 0 : } catch (EOFException e) {
1299 0 : throw new BadRequestException("Expected JSON object", e);
1300 12 : }
1301 12 : if (first == JsonToken.STRING) {
1302 2 : return parseString(json.nextString(), type);
1303 : }
1304 12 : return OutputFormat.JSON.newGson().fromJson(json, type);
1305 : } finally {
1306 : try {
1307 : // Reader.close won't consume the rest of the input. Explicitly consume the request
1308 : // body.
1309 12 : br.skip(Long.MAX_VALUE);
1310 0 : } catch (Exception e) {
1311 : // ignore, e.g. trying to consume the rest of the input may fail if the request was
1312 : // cancelled
1313 12 : }
1314 : }
1315 2 : }
1316 : }
1317 13 : String method = req.getMethod();
1318 13 : if (("PUT".equals(method) || "POST".equals(method)) && acceptsRawInput(type)) {
1319 2 : return parseRawInput(req, type);
1320 : }
1321 13 : if (isDelete(req) && hasNoBody(req)) {
1322 8 : return null;
1323 : }
1324 11 : if (hasNoBody(req)) {
1325 11 : return createInstance(type);
1326 : }
1327 0 : if (isType(PLAIN_TEXT, req.getContentType())) {
1328 0 : try (BufferedReader br = req.getReader()) {
1329 0 : char[] tmp = new char[256];
1330 0 : StringBuilder sb = new StringBuilder();
1331 : int n;
1332 0 : while (0 < (n = br.read(tmp))) {
1333 0 : sb.append(tmp, 0, n);
1334 : }
1335 0 : return parseString(sb.toString(), type);
1336 : }
1337 : }
1338 0 : if (isPost(req) && isType(FORM_TYPE, req.getContentType())) {
1339 0 : return OutputFormat.JSON.newGson().fromJson(ParameterParser.formToJson(req), type);
1340 : }
1341 0 : throw new BadRequestException("Expected Content-Type: " + JSON_TYPE);
1342 : }
1343 :
1344 : private static boolean hasNoBody(HttpServletRequest req) {
1345 13 : int len = req.getContentLength();
1346 13 : String type = req.getContentType();
1347 13 : return (len <= 0 && type == null) || (len == 0 && isType(FORM_TYPE, type));
1348 : }
1349 :
1350 : @SuppressWarnings("rawtypes")
1351 : private static boolean acceptsRawInput(Type type) {
1352 11 : if (type instanceof Class) {
1353 11 : for (Field f : ((Class) type).getDeclaredFields()) {
1354 11 : if (f.getType() == RawInput.class) {
1355 2 : return true;
1356 : }
1357 : }
1358 : }
1359 11 : return false;
1360 : }
1361 :
1362 : private Object parseRawInput(HttpServletRequest req, Type type)
1363 : throws SecurityException, NoSuchMethodException, IllegalArgumentException,
1364 : InstantiationException, IllegalAccessException, InvocationTargetException,
1365 : MethodNotAllowedException {
1366 2 : Object obj = createInstance(type);
1367 2 : for (Field f : obj.getClass().getDeclaredFields()) {
1368 2 : if (f.getType() == RawInput.class) {
1369 2 : f.setAccessible(true);
1370 2 : f.set(obj, RawInputUtil.create(req));
1371 2 : return obj;
1372 : }
1373 : }
1374 0 : throw new MethodNotAllowedException("raw input not supported");
1375 : }
1376 :
1377 : private Object parseString(String value, Type type)
1378 : throws BadRequestException, SecurityException, NoSuchMethodException,
1379 : IllegalArgumentException, IllegalAccessException, InstantiationException,
1380 : InvocationTargetException {
1381 2 : if (type == String.class) {
1382 0 : return value;
1383 : }
1384 :
1385 2 : Object obj = createInstance(type);
1386 2 : if (Strings.isNullOrEmpty(value)) {
1387 1 : return obj;
1388 : }
1389 2 : Field[] fields = obj.getClass().getDeclaredFields();
1390 2 : for (Field f : fields) {
1391 2 : if (f.getAnnotation(DefaultInput.class) != null && f.getType() == String.class) {
1392 2 : f.setAccessible(true);
1393 2 : f.set(obj, value);
1394 2 : return obj;
1395 : }
1396 : }
1397 0 : throw new BadRequestException("Expected JSON object");
1398 : }
1399 :
1400 : @SuppressWarnings("unchecked")
1401 : private static Object createInstance(Type type)
1402 : throws NoSuchMethodException, InstantiationException, IllegalAccessException,
1403 : InvocationTargetException {
1404 11 : if (type instanceof Class) {
1405 11 : Class<Object> clazz = (Class<Object>) type;
1406 11 : Constructor<Object> c = clazz.getDeclaredConstructor();
1407 11 : c.setAccessible(true);
1408 11 : return c.newInstance();
1409 : }
1410 1 : if (type instanceof ParameterizedType) {
1411 1 : Type rawType = ((ParameterizedType) type).getRawType();
1412 1 : if (rawType instanceof Class && List.class.isAssignableFrom((Class<Object>) rawType)) {
1413 1 : return new ArrayList<>();
1414 : }
1415 0 : if (rawType instanceof Class && Map.class.isAssignableFrom((Class<Object>) rawType)) {
1416 0 : return new HashMap<>();
1417 : }
1418 : }
1419 0 : throw new InstantiationException("Cannot make " + type);
1420 : }
1421 :
1422 : /**
1423 : * Sets a JSON reply on the given HTTP servlet response.
1424 : *
1425 : * @param req the HTTP servlet request
1426 : * @param res the HTTP servlet response on which the reply should be set
1427 : * @param allowTracing whether it is allowed to log the reply if tracing is enabled, must not be
1428 : * set to {@code true} if the reply may contain sensitive data
1429 : * @param config config parameters for the JSON formatting
1430 : * @param result the object that should be formatted as JSON
1431 : * @return the length of the response
1432 : */
1433 : public static long replyJson(
1434 : @Nullable HttpServletRequest req,
1435 : HttpServletResponse res,
1436 : boolean allowTracing,
1437 : ListMultimap<String, String> config,
1438 : Object result)
1439 : throws IOException {
1440 22 : TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE);
1441 22 : buf.write(JSON_MAGIC);
1442 22 : Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
1443 22 : Gson gson = newGson(config);
1444 22 : if (result instanceof JsonElement) {
1445 0 : gson.toJson((JsonElement) result, w);
1446 : } else {
1447 22 : gson.toJson(result, w);
1448 : }
1449 22 : w.write('\n');
1450 22 : w.flush();
1451 :
1452 22 : if (allowTracing) {
1453 0 : logger.atFinest().log(
1454 : "JSON response body:\n%s",
1455 0 : lazy(
1456 : () -> {
1457 : try {
1458 0 : ByteArrayOutputStream debugOut = new ByteArrayOutputStream();
1459 0 : buf.writeTo(debugOut, null);
1460 0 : return debugOut.toString(UTF_8.name());
1461 0 : } catch (IOException e) {
1462 0 : return "<JSON formatting failed>";
1463 : }
1464 : }));
1465 : }
1466 22 : return replyBinaryResult(
1467 22 : req, res, asBinaryResult(buf).setContentType(JSON_TYPE).setCharacterEncoding(UTF_8));
1468 : }
1469 :
1470 : private static Gson newGson(ListMultimap<String, String> config) {
1471 22 : GsonBuilder gb = OutputFormat.JSON_COMPACT.newGsonBuilder();
1472 :
1473 22 : enablePrettyPrint(gb, config);
1474 22 : enablePartialGetFields(gb, config);
1475 :
1476 22 : return gb.create();
1477 : }
1478 :
1479 : private static void enablePrettyPrint(GsonBuilder gb, ListMultimap<String, String> config) {
1480 22 : String pp =
1481 22 : Iterables.getFirst(config.get("pp"), Iterables.getFirst(config.get("prettyPrint"), "0"));
1482 22 : if ("1".equals(pp) || "true".equals(pp)) {
1483 1 : gb.setPrettyPrinting();
1484 : }
1485 22 : }
1486 :
1487 : private static void enablePartialGetFields(GsonBuilder gb, ListMultimap<String, String> config) {
1488 22 : final Set<String> want = new HashSet<>();
1489 22 : for (String p : config.get("fields")) {
1490 0 : Iterables.addAll(want, OptionUtil.splitOptionValue(p));
1491 0 : }
1492 22 : if (!want.isEmpty()) {
1493 0 : gb.addSerializationExclusionStrategy(
1494 0 : new ExclusionStrategy() {
1495 0 : private final Map<String, String> names = new HashMap<>();
1496 :
1497 : @Override
1498 : public boolean shouldSkipField(FieldAttributes field) {
1499 0 : String name = names.get(field.getName());
1500 0 : if (name == null) {
1501 : // Names are supplied by Gson in terms of Java source.
1502 : // Translate and cache the JSON lower_case_style used.
1503 : try {
1504 0 : name =
1505 0 : FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES.translateName( //
1506 0 : field.getDeclaringClass().getDeclaredField(field.getName()));
1507 0 : names.put(field.getName(), name);
1508 0 : } catch (SecurityException | NoSuchFieldException e) {
1509 0 : return true;
1510 0 : }
1511 : }
1512 0 : return !want.contains(name);
1513 : }
1514 :
1515 : @Override
1516 : public boolean shouldSkipClass(Class<?> clazz) {
1517 0 : return false;
1518 : }
1519 : });
1520 : }
1521 22 : }
1522 :
1523 : @SuppressWarnings("resource")
1524 : static long replyBinaryResult(
1525 : @Nullable HttpServletRequest req, HttpServletResponse res, BinaryResult bin)
1526 : throws IOException {
1527 27 : final BinaryResult appResult = bin;
1528 : try {
1529 27 : if (bin.getAttachmentName() != null) {
1530 1 : res.setHeader(
1531 1 : "Content-Disposition", "attachment; filename=\"" + bin.getAttachmentName() + "\"");
1532 : }
1533 27 : if (bin.isBase64()) {
1534 7 : if (req != null && JSON_TYPE.equals(req.getHeader(HttpHeaders.ACCEPT))) {
1535 1 : bin = stackJsonString(res, bin);
1536 : } else {
1537 6 : bin = stackBase64(res, bin);
1538 : }
1539 : }
1540 27 : if (bin.canGzip() && acceptsGzip(req)) {
1541 27 : bin = stackGzip(res, bin);
1542 : }
1543 :
1544 27 : res.setContentType(bin.getContentType());
1545 27 : long len = bin.getContentLength();
1546 27 : if (0 <= len && len < Integer.MAX_VALUE) {
1547 27 : res.setContentLength((int) len);
1548 4 : } else if (0 <= len) {
1549 0 : res.setHeader("Content-Length", Long.toString(len));
1550 : }
1551 :
1552 27 : if (req == null || !"HEAD".equals(req.getMethod())) {
1553 26 : try (CountingOutputStream dst = new CountingOutputStream(res.getOutputStream())) {
1554 26 : bin.writeTo(dst);
1555 26 : return dst.getCount();
1556 : }
1557 : }
1558 1 : return 0;
1559 : } finally {
1560 27 : appResult.close();
1561 : }
1562 : }
1563 :
1564 : private static BinaryResult stackJsonString(HttpServletResponse res, BinaryResult src)
1565 : throws IOException {
1566 1 : TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE);
1567 1 : buf.write(JSON_MAGIC);
1568 1 : try (Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
1569 1 : JsonWriter json = new JsonWriter(w)) {
1570 1 : json.setLenient(true);
1571 1 : json.setHtmlSafe(true);
1572 1 : json.value(src.asString());
1573 1 : w.write('\n');
1574 : }
1575 1 : res.setHeader("X-FYI-Content-Encoding", "json");
1576 1 : res.setHeader("X-FYI-Content-Type", src.getContentType());
1577 1 : return asBinaryResult(buf).setContentType(JSON_TYPE).setCharacterEncoding(UTF_8);
1578 : }
1579 :
1580 : private static BinaryResult stackBase64(HttpServletResponse res, BinaryResult src)
1581 : throws IOException {
1582 : BinaryResult b64;
1583 6 : long len = src.getContentLength();
1584 6 : if (0 <= len && len <= (7 << 20)) {
1585 4 : b64 = base64(src);
1586 : } else {
1587 3 : b64 =
1588 3 : new BinaryResult() {
1589 : @Override
1590 : public void writeTo(OutputStream out) throws IOException {
1591 3 : try (OutputStreamWriter w =
1592 : new OutputStreamWriter(
1593 3 : new FilterOutputStream(out) {
1594 : @Override
1595 : public void close() {
1596 : // Do not close out, but only w and e.
1597 3 : }
1598 : },
1599 : ISO_8859_1);
1600 3 : OutputStream e = BaseEncoding.base64().encodingStream(w)) {
1601 3 : src.writeTo(e);
1602 : }
1603 3 : }
1604 : };
1605 : }
1606 6 : res.setHeader("X-FYI-Content-Encoding", "base64");
1607 6 : res.setHeader("X-FYI-Content-Type", src.getContentType());
1608 6 : return b64.setContentType(PLAIN_TEXT).setCharacterEncoding(ISO_8859_1);
1609 : }
1610 :
1611 : private static BinaryResult stackGzip(HttpServletResponse res, BinaryResult src)
1612 : throws IOException {
1613 : BinaryResult gz;
1614 27 : long len = src.getContentLength();
1615 27 : if (len < 256) {
1616 26 : return src; // Do not compress very small payloads.
1617 : }
1618 18 : if (len <= (10 << 20)) {
1619 18 : gz = compress(src);
1620 18 : if (len <= gz.getContentLength()) {
1621 0 : return src;
1622 : }
1623 : } else {
1624 0 : gz =
1625 0 : new BinaryResult() {
1626 : @Override
1627 : public void writeTo(OutputStream out) throws IOException {
1628 0 : GZIPOutputStream gz = new GZIPOutputStream(out);
1629 0 : src.writeTo(gz);
1630 0 : gz.finish();
1631 0 : gz.flush();
1632 0 : }
1633 : };
1634 : }
1635 18 : res.setHeader("Content-Encoding", "gzip");
1636 18 : return gz.setContentType(src.getContentType());
1637 : }
1638 :
1639 : private ViewData view(
1640 : RestCollection<RestResource, RestResource> rc, String method, List<IdString> path)
1641 : throws AmbiguousViewException, RestApiException {
1642 26 : DynamicMap<RestView<RestResource>> views = rc.views();
1643 26 : final IdString projection = path.isEmpty() ? IdString.fromUrl("/") : path.remove(0);
1644 26 : if (!path.isEmpty()) {
1645 : // If there are path components still remaining after this projection
1646 : // is chosen, look for the projection based upon GET as the method as
1647 : // the client thinks it is a nested collection.
1648 20 : method = "GET";
1649 23 : } else if ("HEAD".equals(method)) {
1650 1 : method = "GET";
1651 : }
1652 :
1653 26 : List<String> p = splitProjection(projection);
1654 26 : if (p.size() == 2) {
1655 2 : String viewname = p.get(1);
1656 2 : if (Strings.isNullOrEmpty(viewname)) {
1657 0 : viewname = "/";
1658 : }
1659 2 : RestView<RestResource> view = views.get(p.get(0), method + "." + viewname);
1660 2 : if (view != null) {
1661 2 : return new ViewData(p.get(0), view);
1662 : }
1663 1 : view = views.get(p.get(0), "GET." + viewname);
1664 1 : if (view != null) {
1665 1 : return new ViewData(p.get(0), view);
1666 : }
1667 0 : throw new ResourceNotFoundException(projection);
1668 : }
1669 :
1670 26 : String name = method + "." + p.get(0);
1671 26 : RestView<RestResource> core = views.get(PluginName.GERRIT, name);
1672 26 : if (core != null) {
1673 26 : return new ViewData(PluginName.GERRIT, core);
1674 : }
1675 :
1676 : // Check if we want to delegate to a child collection. Child collections are bound with
1677 : // GET.name so we have to check for this since we haven't found any other views.
1678 5 : if (method.equals("GET")) {
1679 2 : core = views.get(PluginName.GERRIT, "GET." + p.get(0));
1680 2 : if (core != null) {
1681 0 : return new ViewData(PluginName.GERRIT, core);
1682 : }
1683 : }
1684 :
1685 5 : Map<String, RestView<RestResource>> r = new TreeMap<>();
1686 5 : for (String plugin : views.plugins()) {
1687 5 : RestView<RestResource> action = views.get(plugin, name);
1688 5 : if (action != null) {
1689 1 : r.put(plugin, action);
1690 : }
1691 5 : }
1692 :
1693 5 : if (r.isEmpty()) {
1694 : // Check if we want to delegate to a child collection. Child collections are bound with
1695 : // GET.name so we have to check for this since we haven't found any other views.
1696 5 : for (String plugin : views.plugins()) {
1697 5 : RestView<RestResource> action = views.get(plugin, "GET." + p.get(0));
1698 5 : if (action != null) {
1699 5 : r.put(plugin, action);
1700 : }
1701 5 : }
1702 : }
1703 :
1704 5 : if (r.size() == 1) {
1705 5 : Map.Entry<String, RestView<RestResource>> entry = Iterables.getOnlyElement(r.entrySet());
1706 5 : return new ViewData(entry.getKey(), entry.getValue());
1707 : }
1708 1 : if (r.isEmpty()) {
1709 1 : throw new ResourceNotFoundException(projection);
1710 : }
1711 0 : throw new AmbiguousViewException(
1712 0 : String.format(
1713 : "Projection %s is ambiguous: %s",
1714 0 : name, r.keySet().stream().map(in -> in + "~" + projection).collect(joining(", "))));
1715 : }
1716 :
1717 : private static List<IdString> splitPath(HttpServletRequest req) {
1718 28 : String path = RequestUtil.getEncodedPathInfo(req);
1719 28 : if (Strings.isNullOrEmpty(path)) {
1720 6 : return new ArrayList<>();
1721 : }
1722 27 : List<IdString> out = new ArrayList<>();
1723 27 : for (String p : Splitter.on('/').split(path)) {
1724 27 : out.add(IdString.fromUrl(p));
1725 27 : }
1726 27 : if (!out.isEmpty() && out.get(out.size() - 1).isEmpty()) {
1727 7 : out.remove(out.size() - 1);
1728 : }
1729 27 : return out;
1730 : }
1731 :
1732 : private static List<String> splitProjection(IdString projection) {
1733 26 : List<String> p = Lists.newArrayListWithCapacity(2);
1734 26 : Iterables.addAll(p, Splitter.on('~').limit(2).split(projection.get()));
1735 26 : return p;
1736 : }
1737 :
1738 : private void checkUserSession(HttpServletRequest req) throws AuthException {
1739 28 : CurrentUser user = globals.currentUser.get();
1740 28 : if (isRead(req)) {
1741 19 : user.setAccessPath(AccessPath.REST_API);
1742 19 : } else if (user instanceof AnonymousUser) {
1743 0 : throw new AuthException("Authentication required");
1744 19 : } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
1745 0 : throw new AuthException(
1746 : "Invalid authentication method. In order to authenticate, "
1747 : + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
1748 : }
1749 28 : }
1750 :
1751 : private List<String> getParameterNames(HttpServletRequest req) {
1752 28 : List<String> parameterNames = new ArrayList<>(req.getParameterMap().keySet());
1753 28 : Collections.sort(parameterNames);
1754 28 : return parameterNames;
1755 : }
1756 :
1757 : private TraceContext enableTracing(HttpServletRequest req, HttpServletResponse res) {
1758 : // There are 2 ways to enable tracing for REST calls:
1759 : // 1. by using the 'trace' or 'trace=<trace-id>' request parameter
1760 : // 2. by setting the 'X-Gerrit-Trace:' or 'X-Gerrit-Trace:<trace-id>' header
1761 28 : String traceValueFromHeader = req.getHeader(X_GERRIT_TRACE);
1762 28 : String traceValueFromRequestParam = req.getParameter(ParameterParser.TRACE_PARAMETER);
1763 28 : boolean doTrace = traceValueFromHeader != null || traceValueFromRequestParam != null;
1764 :
1765 : // Check whether no trace ID, one trace ID or 2 different trace IDs have been specified.
1766 : String traceId1;
1767 : String traceId2;
1768 28 : if (!Strings.isNullOrEmpty(traceValueFromHeader)) {
1769 1 : traceId1 = traceValueFromHeader;
1770 1 : if (!Strings.isNullOrEmpty(traceValueFromRequestParam)
1771 1 : && !traceValueFromHeader.equals(traceValueFromRequestParam)) {
1772 1 : traceId2 = traceValueFromRequestParam;
1773 : } else {
1774 1 : traceId2 = null;
1775 : }
1776 : } else {
1777 28 : traceId1 = Strings.emptyToNull(traceValueFromRequestParam);
1778 28 : traceId2 = null;
1779 : }
1780 :
1781 : // Use the first trace ID to start tracing. If this trace ID is null, a trace ID will be
1782 : // generated.
1783 28 : TraceContext traceContext =
1784 28 : TraceContext.newTrace(
1785 1 : doTrace, traceId1, (tagName, traceId) -> res.setHeader(X_GERRIT_TRACE, traceId));
1786 : // If a second trace ID was specified, add a tag for it as well.
1787 28 : if (traceId2 != null) {
1788 1 : traceContext.addTag(RequestId.Type.TRACE_ID, traceId2);
1789 1 : res.addHeader(X_GERRIT_TRACE, traceId2);
1790 : }
1791 28 : return traceContext;
1792 : }
1793 :
1794 : private RequestInfo createRequestInfo(
1795 : TraceContext traceContext, String requestUri, List<IdString> path) {
1796 28 : RequestInfo.Builder requestInfo =
1797 28 : RequestInfo.builder(RequestInfo.RequestType.REST, globals.currentUser.get(), traceContext)
1798 28 : .requestUri(requestUri);
1799 :
1800 28 : if (path.size() < 1) {
1801 6 : return requestInfo.build();
1802 : }
1803 :
1804 27 : RestCollection<?, ?> rootCollection = members.get();
1805 27 : String resourceId = path.get(0).get();
1806 27 : if (rootCollection instanceof ProjectsCollection) {
1807 14 : requestInfo.project(Project.nameKey(resourceId));
1808 16 : } else if (rootCollection instanceof ChangesCollection) {
1809 13 : Optional<ChangeNotes> changeNotes = globals.changeFinder.findOne(resourceId);
1810 13 : if (changeNotes.isPresent()) {
1811 13 : requestInfo.project(changeNotes.get().getProjectName());
1812 : }
1813 : }
1814 27 : return requestInfo.build();
1815 : }
1816 :
1817 : private void traceRequestData(HttpServletRequest req) {
1818 28 : logger.atFinest().log(
1819 : "Received REST request: %s %s (parameters: %s)",
1820 28 : req.getMethod(), req.getRequestURI(), getParameterNames(req));
1821 28 : Optional.ofNullable(req.getHeader(X_GERRIT_DEADLINE))
1822 28 : .ifPresent(
1823 : clientProvidedDeadline ->
1824 1 : logger.atFine().log("%s = %s", X_GERRIT_DEADLINE, clientProvidedDeadline));
1825 28 : logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
1826 28 : logger.atFinest().log(
1827 28 : "Groups: %s", lazy(() -> globals.currentUser.get().getEffectiveGroups().getKnownGroups()));
1828 28 : }
1829 :
1830 : private boolean isDelete(HttpServletRequest req) {
1831 14 : return "DELETE".equals(req.getMethod());
1832 : }
1833 :
1834 : private static boolean isPost(HttpServletRequest req) {
1835 13 : return "POST".equals(req.getMethod());
1836 : }
1837 :
1838 : private boolean isPut(HttpServletRequest req) {
1839 11 : return "PUT".equals(req.getMethod());
1840 : }
1841 :
1842 : private static boolean isRead(HttpServletRequest req) {
1843 28 : return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod());
1844 : }
1845 :
1846 : private static MethodNotAllowedException methodNotAllowed(HttpServletRequest req) {
1847 1 : return new MethodNotAllowedException(
1848 1 : String.format("Not implemented: %s %s", req.getMethod(), requestUri(req)));
1849 : }
1850 :
1851 : private static String requestUri(HttpServletRequest req) {
1852 28 : String uri = req.getRequestURI();
1853 28 : if (uri.startsWith("/a/")) {
1854 28 : return uri.substring(2);
1855 : }
1856 2 : return uri;
1857 : }
1858 :
1859 : private void checkRequiresCapability(ViewData d)
1860 : throws AuthException, PermissionBackendException {
1861 : try {
1862 22 : globals.permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
1863 16 : } catch (AuthException e) {
1864 : // Skiping
1865 16 : globals
1866 : .permissionBackend
1867 16 : .currentUser()
1868 15 : .checkAny(GlobalPermission.fromAnnotation(d.pluginName, d.view.getClass()));
1869 22 : }
1870 28 : }
1871 :
1872 : private Optional<ExceptionHook.Status> getStatus(Throwable err) {
1873 5 : return globals.exceptionHooks.stream()
1874 5 : .map(h -> h.getStatus(err))
1875 5 : .filter(Optional::isPresent)
1876 5 : .map(Optional::get)
1877 5 : .findFirst();
1878 : }
1879 :
1880 : private ImmutableList<String> getUserMessages(Throwable err) {
1881 2 : return globals.exceptionHooks.stream()
1882 2 : .flatMap(h -> h.getUserMessages(err, TraceContext.getTraceId().orElse(null)).stream())
1883 2 : .collect(toImmutableList());
1884 : }
1885 :
1886 : private static long reply(
1887 : HttpServletRequest req,
1888 : HttpServletResponse res,
1889 : Throwable err,
1890 : ExceptionHook.Status status,
1891 : ImmutableList<String> userMessages)
1892 : throws IOException {
1893 1 : res.setStatus(status.statusCode());
1894 :
1895 1 : StringBuilder msg = new StringBuilder(status.statusMessage());
1896 1 : if (!userMessages.isEmpty()) {
1897 1 : msg.append("\n");
1898 1 : userMessages.forEach(m -> msg.append("\n* ").append(m));
1899 : }
1900 :
1901 1 : if (status.statusCode() < SC_BAD_REQUEST) {
1902 0 : logger.atFinest().withCause(err).log("REST call finished: %d", status.statusCode());
1903 0 : return replyText(req, res, true, msg.toString());
1904 : }
1905 1 : if (status.statusCode() >= SC_INTERNAL_SERVER_ERROR) {
1906 0 : logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req));
1907 : }
1908 1 : return replyError(req, res, status.statusCode(), msg.toString(), err);
1909 : }
1910 :
1911 : private long replyInternalServerError(
1912 : HttpServletRequest req,
1913 : HttpServletResponse res,
1914 : Throwable err,
1915 : ImmutableList<String> userMessages)
1916 : throws IOException {
1917 1 : logger.atSevere().withCause(err).log(
1918 : "Error in %s %s: %s",
1919 1 : req.getMethod(), uriForLogging(req), globals.retryHelper.formatCause(err));
1920 :
1921 1 : StringBuilder msg = new StringBuilder("Internal server error");
1922 1 : if (!userMessages.isEmpty()) {
1923 0 : msg.append("\n");
1924 0 : userMessages.forEach(m -> msg.append("\n* ").append(m));
1925 : }
1926 :
1927 1 : return replyError(req, res, SC_INTERNAL_SERVER_ERROR, msg.toString(), err);
1928 : }
1929 :
1930 : private static String uriForLogging(HttpServletRequest req) {
1931 4 : String uri = req.getRequestURI();
1932 4 : if (!Strings.isNullOrEmpty(req.getQueryString())) {
1933 0 : uri += "?" + LogRedactUtil.redactQueryString(req.getQueryString());
1934 : }
1935 4 : return uri;
1936 : }
1937 :
1938 : public static long replyError(
1939 : HttpServletRequest req,
1940 : HttpServletResponse res,
1941 : int statusCode,
1942 : String msg,
1943 : @Nullable Throwable err)
1944 : throws IOException {
1945 7 : return replyError(req, res, statusCode, msg, CacheControl.NONE, err);
1946 : }
1947 :
1948 : public static long replyError(
1949 : HttpServletRequest req,
1950 : HttpServletResponse res,
1951 : int statusCode,
1952 : String msg,
1953 : CacheControl cacheControl,
1954 : @Nullable Throwable err)
1955 : throws IOException {
1956 21 : if (err != null) {
1957 21 : RequestUtil.setErrorTraceAttribute(req, err);
1958 : }
1959 21 : setCacheHeaders(req, res, cacheControl);
1960 21 : checkArgument(statusCode >= 400, "non-error status: %s", statusCode);
1961 21 : res.setStatus(statusCode);
1962 21 : logger.atFinest().withCause(err).log("REST call failed: %d", statusCode);
1963 21 : return replyText(req, res, true, msg);
1964 : }
1965 :
1966 : /**
1967 : * Sets a text reply on the given HTTP servlet response.
1968 : *
1969 : * @param req the HTTP servlet request
1970 : * @param res the HTTP servlet response on which the reply should be set
1971 : * @param allowTracing whether it is allowed to log the reply if tracing is enabled, must not be
1972 : * set to {@code true} if the reply may contain sensitive data
1973 : * @param text the text reply
1974 : * @return the length of the response
1975 : */
1976 : static long replyText(
1977 : @Nullable HttpServletRequest req, HttpServletResponse res, boolean allowTracing, String text)
1978 : throws IOException {
1979 21 : if (!text.endsWith("\n")) {
1980 21 : text += "\n";
1981 : }
1982 21 : if (allowTracing) {
1983 21 : logger.atFinest().log("Text response body:\n%s", text);
1984 : }
1985 21 : return replyBinaryResult(req, res, BinaryResult.create(text).setContentType(PLAIN_TEXT));
1986 : }
1987 :
1988 : private static int getCancellationStatusCode(RequestStateProvider.Reason cancellationReason) {
1989 1 : switch (cancellationReason) {
1990 : case CLIENT_CLOSED_REQUEST:
1991 1 : return SC_CLIENT_CLOSED_REQUEST;
1992 : case CLIENT_PROVIDED_DEADLINE_EXCEEDED:
1993 : case SERVER_DEADLINE_EXCEEDED:
1994 1 : return SC_REQUEST_TIMEOUT;
1995 : }
1996 0 : logger.atSevere().log("Unexpected cancellation reason: %s", cancellationReason);
1997 0 : return SC_INTERNAL_SERVER_ERROR;
1998 : }
1999 :
2000 : private static String getCancellationMessage(
2001 : RequestCancelledException requestCancelledException) {
2002 1 : StringBuilder msg = new StringBuilder(requestCancelledException.formatCancellationReason());
2003 1 : if (requestCancelledException.getCancellationMessage().isPresent()) {
2004 1 : msg.append("\n\n");
2005 1 : msg.append(requestCancelledException.getCancellationMessage().get());
2006 : }
2007 1 : return msg.toString();
2008 : }
2009 :
2010 : private static boolean acceptsGzip(HttpServletRequest req) {
2011 27 : if (req != null) {
2012 27 : String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
2013 27 : return accepts != null && accepts.contains("gzip");
2014 : }
2015 0 : return false;
2016 : }
2017 :
2018 : private static boolean isType(String expect, String given) {
2019 19 : if (given == null) {
2020 13 : return false;
2021 : }
2022 12 : if (expect.equals(given)) {
2023 12 : return true;
2024 : }
2025 1 : if (given.startsWith(expect + ",")) {
2026 0 : return true;
2027 : }
2028 1 : for (String p : Splitter.on(TYPE_SPLIT_PATTERN).split(given)) {
2029 1 : if (expect.equals(p)) {
2030 0 : return true;
2031 : }
2032 1 : }
2033 1 : return false;
2034 : }
2035 :
2036 : private static int base64MaxSize(long n) {
2037 4 : return 4 * IntMath.divide((int) n, 3, CEILING);
2038 : }
2039 :
2040 : private static BinaryResult base64(BinaryResult bin) throws IOException {
2041 4 : int maxSize = base64MaxSize(bin.getContentLength());
2042 4 : int estSize = Math.min(base64MaxSize(HEAP_EST_SIZE), maxSize);
2043 4 : TemporaryBuffer.Heap buf = heap(estSize, maxSize);
2044 : try (OutputStream encoded =
2045 4 : BaseEncoding.base64().encodingStream(new OutputStreamWriter(buf, ISO_8859_1))) {
2046 4 : bin.writeTo(encoded);
2047 : }
2048 4 : return asBinaryResult(buf);
2049 : }
2050 :
2051 : private static BinaryResult compress(BinaryResult bin) throws IOException {
2052 18 : TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, 20 << 20);
2053 18 : try (GZIPOutputStream gz = new GZIPOutputStream(buf)) {
2054 18 : bin.writeTo(gz);
2055 : }
2056 18 : return asBinaryResult(buf).setContentType(bin.getContentType());
2057 : }
2058 :
2059 : @SuppressWarnings("resource")
2060 : private static BinaryResult asBinaryResult(TemporaryBuffer.Heap buf) {
2061 23 : return new BinaryResult() {
2062 : @Override
2063 : public void writeTo(OutputStream os) throws IOException {
2064 22 : buf.writeTo(os, null);
2065 22 : }
2066 23 : }.setContentLength(buf.length());
2067 : }
2068 :
2069 : private static Heap heap(int est, int max) {
2070 23 : return new TemporaryBuffer.Heap(est, max);
2071 : }
2072 :
2073 : private static class AmbiguousViewException extends Exception {
2074 : private static final long serialVersionUID = 1L;
2075 :
2076 : AmbiguousViewException(String message) {
2077 0 : super(message);
2078 0 : }
2079 : }
2080 :
2081 : static class ViewData {
2082 : String pluginName;
2083 : RestView<RestResource> view;
2084 :
2085 28 : ViewData(String pluginName, RestView<RestResource> view) {
2086 28 : this.pluginName = pluginName;
2087 28 : this.view = view;
2088 28 : }
2089 : }
2090 : }
|