org.elasticsearch.rest.RestController Maven / Gradle / Ivy
Show all versions of elasticsearch Show documentation
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.rest;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.breaker.CircuitBreaker;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.BytesStream;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.path.PathTrie;
import org.elasticsearch.common.util.Maps;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.RestApiVersion;
import org.elasticsearch.core.Streams;
import org.elasticsearch.http.HttpServerTransport;
import org.elasticsearch.indices.breaker.CircuitBreakerService;
import org.elasticsearch.rest.RestHandler.Route;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.tracing.Tracer;
import org.elasticsearch.usage.UsageService;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentType;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import static org.elasticsearch.indices.SystemIndices.EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY;
import static org.elasticsearch.indices.SystemIndices.SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY;
import static org.elasticsearch.rest.RestResponse.TEXT_CONTENT_TYPE;
import static org.elasticsearch.rest.RestStatus.BAD_REQUEST;
import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR;
import static org.elasticsearch.rest.RestStatus.METHOD_NOT_ALLOWED;
import static org.elasticsearch.rest.RestStatus.NOT_ACCEPTABLE;
import static org.elasticsearch.rest.RestStatus.OK;
public class RestController implements HttpServerTransport.Dispatcher {
private static final Logger logger = LogManager.getLogger(RestController.class);
private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestController.class);
/**
* list of browser safelisted media types - not allowed on Content-Type header
* https://fetch.spec.whatwg.org/#cors-safelisted-request-header
*/
static final Set SAFELISTED_MEDIA_TYPES = Set.of("application/x-www-form-urlencoded", "multipart/form-data", "text/plain");
static final String ELASTIC_PRODUCT_HTTP_HEADER = "X-elastic-product";
static final String ELASTIC_PRODUCT_HTTP_HEADER_VALUE = "Elasticsearch";
private static final BytesReference FAVICON_RESPONSE;
static {
try (InputStream stream = RestController.class.getResourceAsStream("/config/favicon.ico")) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.copy(stream, out);
FAVICON_RESPONSE = new BytesArray(out.toByteArray());
} catch (IOException e) {
throw new AssertionError(e);
}
}
private final PathTrie handlers = new PathTrie<>(RestUtils.REST_DECODER);
private final UnaryOperator handlerWrapper;
private final NodeClient client;
private final CircuitBreakerService circuitBreakerService;
/** Rest headers that are copied to internal requests made during a rest request. */
private final Set headersToCopy;
private final UsageService usageService;
private final Tracer tracer;
public RestController(
Set headersToCopy,
UnaryOperator handlerWrapper,
NodeClient client,
CircuitBreakerService circuitBreakerService,
UsageService usageService,
Tracer tracer
) {
this.headersToCopy = headersToCopy;
this.usageService = usageService;
this.tracer = tracer;
if (handlerWrapper == null) {
handlerWrapper = h -> h; // passthrough if no wrapper set
}
this.handlerWrapper = handlerWrapper;
this.client = client;
this.circuitBreakerService = circuitBreakerService;
registerHandlerNoWrap(
RestRequest.Method.GET,
"/favicon.ico",
RestApiVersion.current(),
(request, channel, clnt) -> channel.sendResponse(new RestResponse(RestStatus.OK, "image/x-icon", FAVICON_RESPONSE))
);
}
/**
* Registers a REST handler to be executed when the provided {@code method} and {@code path} match the request.
*
* @param method GET, POST, etc.
* @param path Path to handle (e.g. "/{index}/{type}/_bulk")
* @param version API version to handle (e.g. RestApiVersion.V_8)
* @param handler The handler to actually execute
* @param deprecationMessage The message to log and send as a header in the response
*/
protected void registerAsDeprecatedHandler(
RestRequest.Method method,
String path,
RestApiVersion version,
RestHandler handler,
String deprecationMessage
) {
registerAsDeprecatedHandler(method, path, version, handler, deprecationMessage, null);
}
/**
* Registers a REST handler to be executed when the provided {@code method} and {@code path} match the request.
*
* @param method GET, POST, etc.
* @param path Path to handle (e.g. "/{index}/{type}/_bulk")
* @param version API version to handle (e.g. RestApiVersion.V_8)
* @param handler The handler to actually execute
* @param deprecationMessage The message to log and send as a header in the response
* @param deprecationLevel The deprecation log level to use for the deprecation warning, either WARN or CRITICAL
*/
protected void registerAsDeprecatedHandler(
RestRequest.Method method,
String path,
RestApiVersion version,
RestHandler handler,
String deprecationMessage,
@Nullable Level deprecationLevel
) {
assert (handler instanceof DeprecationRestHandler) == false;
if (version == RestApiVersion.current()) {
// e.g. it was marked as deprecated in 8.x, and we're currently running 8.x
registerHandler(
method,
path,
version,
new DeprecationRestHandler(handler, method, path, deprecationLevel, deprecationMessage, deprecationLogger, false)
);
} else if (version == RestApiVersion.minimumSupported()) {
// e.g. it was marked as deprecated in 7.x, and we're currently running 8.x
registerHandler(
method,
path,
version,
new DeprecationRestHandler(handler, method, path, deprecationLevel, deprecationMessage, deprecationLogger, true)
);
} else {
// e.g. it was marked as deprecated in 7.x, and we're currently running *9.x*
logger.debug(
"Deprecated route ["
+ method
+ " "
+ path
+ "] for handler ["
+ handler.getClass()
+ "] "
+ "with version ["
+ version
+ "], which is less than the minimum supported version ["
+ RestApiVersion.minimumSupported()
+ "]"
);
}
}
/**
* Registers a REST handler to be executed when the provided {@code method} and {@code path} match the request, or when provided
* with {@code replacedMethod} and {@code replacedPath}. Expected usage:
*
* // remove deprecation in next major release
* controller.registerAsDeprecatedHandler(POST, "/_forcemerge", RestApiVersion.V_8, someHandler,
* POST, "/_optimize", RestApiVersion.V_7);
* controller.registerAsDeprecatedHandler(POST, "/{index}/_forcemerge", RestApiVersion.V_8, someHandler,
* POST, "/{index}/_optimize", RestApiVersion.V_7);
*
*
* The registered REST handler ({@code method} with {@code path}) is a normal REST handler that is not deprecated and it is
* replacing the deprecated REST handler ({@code replacedMethod} with {@code replacedPath}) that is using the same
* {@code handler}.
*
* Deprecated REST handlers without a direct replacement should be deprecated directly using {@link #registerAsDeprecatedHandler}
* and a specific message.
*
* @param method GET, POST, etc.
* @param path Path to handle (e.g. "/_forcemerge")
* @param version API version to handle (e.g. RestApiVersion.V_8)
* @param handler The handler to actually execute
* @param replacedMethod GET, POST, etc.
* @param replacedPath Replaced path to handle (e.g. "/_optimize")
* @param replacedVersion Replaced API version to handle (e.g. RestApiVersion.V_7)
*/
protected void registerAsReplacedHandler(
RestRequest.Method method,
String path,
RestApiVersion version,
RestHandler handler,
RestRequest.Method replacedMethod,
String replacedPath,
RestApiVersion replacedVersion
) {
// e.g. [POST /_optimize] is deprecated! Use [POST /_forcemerge] instead.
final String replacedMessage = "["
+ replacedMethod.name()
+ " "
+ replacedPath
+ "] is deprecated! Use ["
+ method.name()
+ " "
+ path
+ "] instead.";
registerHandler(method, path, version, handler);
registerAsDeprecatedHandler(replacedMethod, replacedPath, replacedVersion, handler, replacedMessage);
}
/**
* Registers a REST handler to be executed when one of the provided methods and path match the request.
*
* @param method GET, POST, etc.
* @param path Path to handle (e.g. "/{index}/{type}/_bulk")
* @param version API version to handle (e.g. RestApiVersion.V_8)
* @param handler The handler to actually execute
*/
protected void registerHandler(RestRequest.Method method, String path, RestApiVersion version, RestHandler handler) {
if (handler instanceof BaseRestHandler) {
usageService.addRestHandler((BaseRestHandler) handler);
}
registerHandlerNoWrap(method, path, version, handlerWrapper.apply(handler));
}
private void registerHandlerNoWrap(RestRequest.Method method, String path, RestApiVersion version, RestHandler handler) {
assert RestApiVersion.minimumSupported() == version || RestApiVersion.current() == version
: "REST API compatibility is only supported for version " + RestApiVersion.minimumSupported().major;
handlers.insertOrUpdate(
path,
new MethodHandlers(path).addMethod(method, version, handler),
(handlers, ignoredHandler) -> handlers.addMethod(method, version, handler)
);
}
public void registerHandler(final Route route, final RestHandler handler) {
if (route.isReplacement()) {
Route replaced = route.getReplacedRoute();
registerAsReplacedHandler(
route.getMethod(),
route.getPath(),
route.getRestApiVersion(),
handler,
replaced.getMethod(),
replaced.getPath(),
replaced.getRestApiVersion()
);
} else if (route.isDeprecated()) {
registerAsDeprecatedHandler(
route.getMethod(),
route.getPath(),
route.getRestApiVersion(),
handler,
route.getDeprecationMessage(),
route.getDeprecationLevel()
);
} else {
// it's just a normal route
registerHandler(route.getMethod(), route.getPath(), route.getRestApiVersion(), handler);
}
}
/**
* Registers a REST handler with the controller. The REST handler declares the {@code method}
* and {@code path} combinations.
*/
public void registerHandler(final RestHandler handler) {
handler.routes().forEach(route -> registerHandler(route, handler));
}
@Override
public void dispatchRequest(RestRequest request, RestChannel channel, ThreadContext threadContext) {
threadContext.addResponseHeader(ELASTIC_PRODUCT_HTTP_HEADER, ELASTIC_PRODUCT_HTTP_HEADER_VALUE);
try {
tryAllHandlers(request, channel, threadContext);
} catch (Exception e) {
try {
channel.sendResponse(new RestResponse(channel, e));
} catch (Exception inner) {
inner.addSuppressed(e);
logger.error(() -> "failed to send failure response for uri [" + request.uri() + "]", inner);
}
}
}
@Override
public void dispatchBadRequest(final RestChannel channel, final ThreadContext threadContext, final Throwable cause) {
threadContext.addResponseHeader(ELASTIC_PRODUCT_HTTP_HEADER, ELASTIC_PRODUCT_HTTP_HEADER_VALUE);
try {
final Exception e;
if (cause == null) {
e = new ElasticsearchException("unknown cause");
} else if (cause instanceof Exception) {
e = (Exception) cause;
} else {
e = new ElasticsearchException(cause);
}
channel.sendResponse(new RestResponse(channel, BAD_REQUEST, e));
} catch (final IOException e) {
if (cause != null) {
e.addSuppressed(cause);
}
logger.warn("failed to send bad request response", e);
channel.sendResponse(new RestResponse(INTERNAL_SERVER_ERROR, RestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY));
}
}
private void dispatchRequest(RestRequest request, RestChannel channel, RestHandler handler, ThreadContext threadContext)
throws Exception {
final int contentLength = request.contentLength();
if (contentLength > 0) {
if (isContentTypeDisallowed(request) || handler.mediaTypesValid(request) == false) {
sendContentTypeErrorMessage(request.getAllHeaderValues("Content-Type"), channel);
return;
}
final XContentType xContentType = request.getXContentType();
// TODO consider refactoring to handler.supportsContentStream(xContentType). It is only used with JSON and SMILE
if (handler.supportsContentStream()
&& XContentType.JSON != xContentType.canonical()
&& XContentType.SMILE != xContentType.canonical()) {
channel.sendResponse(
RestResponse.createSimpleErrorResponse(
channel,
RestStatus.NOT_ACCEPTABLE,
"Content-Type [" + xContentType + "] does not support stream parsing. Use JSON or SMILE instead"
)
);
return;
}
}
RestChannel responseChannel = channel;
try {
if (handler.canTripCircuitBreaker()) {
inFlightRequestsBreaker(circuitBreakerService).addEstimateBytesAndMaybeBreak(contentLength, "");
} else {
inFlightRequestsBreaker(circuitBreakerService).addWithoutBreaking(contentLength);
}
// iff we could reserve bytes for the request we need to send the response also over this channel
responseChannel = new ResourceHandlingHttpChannel(channel, circuitBreakerService, contentLength);
// TODO: Count requests double in the circuit breaker if they need copying?
if (handler.allowsUnsafeBuffers() == false) {
request.ensureSafeBuffers();
}
if (handler.allowSystemIndexAccessByDefault() == false) {
// The ELASTIC_PRODUCT_ORIGIN_HTTP_HEADER indicates that the request is coming from an Elastic product and
// therefore we should allow a subset of external system index access.
// This header is intended for internal use only.
final String prodOriginValue = request.header(Task.X_ELASTIC_PRODUCT_ORIGIN_HTTP_HEADER);
if (prodOriginValue != null) {
threadContext.putHeader(SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, Boolean.TRUE.toString());
threadContext.putHeader(EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, prodOriginValue);
} else {
threadContext.putHeader(SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, Boolean.FALSE.toString());
}
} else {
threadContext.putHeader(SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY, Boolean.TRUE.toString());
}
handler.handleRequest(request, responseChannel, client);
} catch (Exception e) {
responseChannel.sendResponse(new RestResponse(responseChannel, e));
}
}
/**
* in order to prevent CSRF we have to reject all media types that are from a browser safelist
* see https://fetch.spec.whatwg.org/#cors-safelisted-request-header
* see https://www.elastic.co/blog/strict-content-type-checking-for-elasticsearch-rest-requests
* @param request
*/
private static boolean isContentTypeDisallowed(RestRequest request) {
return request.getParsedContentType() != null
&& SAFELISTED_MEDIA_TYPES.contains(request.getParsedContentType().mediaTypeWithoutParameters());
}
private boolean handleNoHandlerFound(
ThreadContext threadContext,
String rawPath,
RestRequest.Method method,
String uri,
RestChannel channel
) {
// Get the map of matching handlers for a request, for the full set of HTTP methods.
final Set validMethodSet = getValidHandlerMethodSet(rawPath);
if (validMethodSet.contains(method) == false) {
if (method == RestRequest.Method.OPTIONS) {
startTrace(threadContext, channel);
handleOptionsRequest(channel, validMethodSet);
return true;
}
if (validMethodSet.isEmpty() == false) {
// If an alternative handler for an explicit path is registered to a
// different HTTP method than the one supplied - return a 405 Method
// Not Allowed error.
startTrace(threadContext, channel);
handleUnsupportedHttpMethod(uri, method, channel, validMethodSet, null);
return true;
}
}
return false;
}
private void startTrace(ThreadContext threadContext, RestChannel channel) {
startTrace(threadContext, channel, null);
}
private void startTrace(ThreadContext threadContext, RestChannel channel, String restPath) {
final RestRequest req = channel.request();
if (restPath == null) {
restPath = req.path();
}
String method = null;
try {
method = req.method().name();
} catch (IllegalArgumentException e) {
// Invalid methods throw an exception
}
String name;
if (method != null) {
name = method + " " + restPath;
} else {
name = restPath;
}
Map attributes = Maps.newMapWithExpectedSize(req.getHeaders().size() + 3);
req.getHeaders().forEach((key, values) -> {
final String lowerKey = key.toLowerCase(Locale.ROOT).replace('-', '_');
final String value = switch (lowerKey) {
case "authorization", "cookie", "secret", "session", "set_cookie", "token" -> "[REDACTED]";
default -> String.join("; ", values);
};
attributes.put("http.request.headers." + lowerKey, value);
});
attributes.put("http.method", method);
attributes.put("http.url", req.uri());
switch (req.getHttpRequest().protocolVersion()) {
case HTTP_1_0 -> attributes.put("http.flavour", "1.0");
case HTTP_1_1 -> attributes.put("http.flavour", "1.1");
}
tracer.startTrace(threadContext, "rest-" + channel.request().getRequestId(), name, attributes);
}
private void traceException(RestChannel channel, Throwable e) {
this.tracer.addError("rest-" + channel.request().getRequestId(), e);
}
private static void sendContentTypeErrorMessage(@Nullable List contentTypeHeader, RestChannel channel) throws IOException {
final String errorMessage;
if (contentTypeHeader == null) {
errorMessage = "Content-Type header is missing";
} else {
errorMessage = "Content-Type header [" + Strings.collectionToCommaDelimitedString(contentTypeHeader) + "] is not supported";
}
channel.sendResponse(RestResponse.createSimpleErrorResponse(channel, NOT_ACCEPTABLE, errorMessage));
}
private void tryAllHandlers(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) throws Exception {
try {
copyRestHeaders(request, threadContext);
validateErrorTrace(request, channel);
} catch (IllegalArgumentException e) {
startTrace(threadContext, channel);
channel.sendResponse(RestResponse.createSimpleErrorResponse(channel, BAD_REQUEST, e.getMessage()));
return;
}
final String rawPath = request.rawPath();
final String uri = request.uri();
final RestRequest.Method requestMethod;
RestApiVersion restApiVersion = request.getRestApiVersion();
try {
// Resolves the HTTP method and fails if the method is invalid
requestMethod = request.method();
// Loop through all possible handlers, attempting to dispatch the request
Iterator allHandlers = getAllHandlers(request.params(), rawPath);
while (allHandlers.hasNext()) {
final RestHandler handler;
final MethodHandlers handlers = allHandlers.next();
if (handlers == null) {
handler = null;
} else {
handler = handlers.getHandler(requestMethod, restApiVersion);
}
if (handler == null) {
if (handleNoHandlerFound(threadContext, rawPath, requestMethod, uri, channel)) {
return;
}
} else {
startTrace(threadContext, channel, handlers.getPath());
dispatchRequest(request, channel, handler, threadContext);
return;
}
}
} catch (final IllegalArgumentException e) {
startTrace(threadContext, channel);
traceException(channel, e);
handleUnsupportedHttpMethod(uri, null, channel, getValidHandlerMethodSet(rawPath), e);
return;
}
// If request has not been handled, fallback to a bad request error.
startTrace(threadContext, channel);
handleBadRequest(uri, requestMethod, channel);
}
private static void validateErrorTrace(RestRequest request, RestChannel channel) {
// error_trace cannot be used when we disable detailed errors
// we consume the error_trace parameter first to ensure that it is always consumed
if (request.paramAsBoolean("error_trace", false) && channel.detailedErrorsEnabled() == false) {
throw new IllegalArgumentException("error traces in responses are disabled.");
}
}
private void copyRestHeaders(RestRequest request, ThreadContext threadContext) {
for (final RestHeaderDefinition restHeader : headersToCopy) {
final String name = restHeader.getName();
final List headerValues = request.getAllHeaderValues(name);
if (headerValues != null && headerValues.isEmpty() == false) {
final List distinctHeaderValues = headerValues.stream().distinct().toList();
if (restHeader.isMultiValueAllowed() == false && distinctHeaderValues.size() > 1) {
throw new IllegalArgumentException("multiple values for single-valued header [" + name + "].");
} else if (name.equals(Task.TRACE_PARENT_HTTP_HEADER)) {
String traceparent = distinctHeaderValues.get(0);
if (traceparent.length() >= 55) {
threadContext.putHeader(Task.TRACE_ID, traceparent.substring(3, 35));
threadContext.putTransient("parent_" + Task.TRACE_PARENT_HTTP_HEADER, traceparent);
}
} else if (name.equals(Task.TRACE_STATE)) {
threadContext.putTransient("parent_" + Task.TRACE_STATE, distinctHeaderValues.get(0));
} else {
threadContext.putHeader(name, String.join(",", distinctHeaderValues));
}
}
}
}
Iterator getAllHandlers(@Nullable Map requestParamsRef, String rawPath) {
final Supplier