blob: eaa9381cf343117338c214f7b7d70df2086d379c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.servlet;
import io.opentracing.Span;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.solr.api.ApiBag;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.annotation.SolrThreadSafe;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.QoSParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.JsonSchemaValidator;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.TimeSource;
import org.apache.solr.common.util.Utils;
import org.apache.solr.common.util.ValidatingJsonMap;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.core.SolrCore;
import org.apache.solr.handler.ContentStreamHandlerBase;
import org.apache.solr.handler.admin.PrepRecoveryOp;
import org.apache.solr.logging.MDCLoggingContext;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrQueryRequestBase;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.response.QueryResponseWriter;
import org.apache.solr.response.QueryResponseWriterUtil;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuditEvent;
import org.apache.solr.security.AuditEvent.EventType;
import org.apache.solr.security.AuthenticationPlugin;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.AuthorizationContext.CollectionRequest;
import org.apache.solr.security.AuthorizationContext.RequestType;
import org.apache.solr.security.AuthorizationResponse;
import org.apache.solr.security.PublicKeyHandler;
import org.apache.solr.servlet.SolrDispatchFilter.Action;
import org.apache.solr.servlet.cache.HttpCacheHeaderUtil;
import org.apache.solr.servlet.cache.Method;
import org.apache.solr.update.processor.DistributingUpdateProcessorFactory;
import org.apache.solr.util.RTimerTree;
import org.apache.solr.util.TimeOut;
import org.apache.solr.util.tracing.GlobalTracer;
import org.apache.zookeeper.KeeperException;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.InputStreamContentProvider;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
import static org.apache.solr.common.params.CollectionAdminParams.SYSTEM_COLL;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATE;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETE;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.RELOAD;
import static org.apache.solr.common.params.CommonParams.NAME;
import static org.apache.solr.common.params.CoreAdminParams.ACTION;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.FORWARD;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.PASSTHROUGH;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.PROCESS;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.REMOTEQUERY;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.RETRY;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.RETURN;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.invoke.MethodHandles;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/**
* This class represents a call made to Solr
**/
@SolrThreadSafe
public class HttpSolrCall {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String ORIGINAL_USER_PRINCIPAL_HEADER = "originalUserPrincipal";
public static final Random random;
static {
// We try to make things reproducible in the context of our tests by initializing the random instance
// based on the current seed
String seed = System.getProperty("tests.seed");
if (seed == null) {
random = new Random();
} else {
random = new Random(seed.hashCode());
}
}
private final boolean preserveHost = false;
protected final SolrDispatchFilter solrDispatchFilter;
protected final CoreContainer cores;
protected final HttpServletRequest req;
protected final HttpServletResponse response;
protected final boolean retry;
protected volatile SolrCore core = null;
protected SolrQueryRequest solrReq = null;
protected SolrRequestHandler handler = null;
protected final SolrParams queryParams;
protected volatile String path;
protected volatile Action action;
protected volatile String coreUrl;
protected SolrConfig config;
protected Map<String, Integer> invalidStates;
//The states of client that is invalid in this request
protected String origCorename; // What's in the URL path; might reference a collection/alias or a Solr core name
protected List<String> collectionsList; // The list of SolrCloud collections if in SolrCloud (usually 1)
public RequestType getRequestType() {
return requestType;
}
protected RequestType requestType;
public HttpSolrCall(SolrDispatchFilter solrDispatchFilter, CoreContainer cores,
HttpServletRequest request, HttpServletResponse response, boolean retry) {
this.solrDispatchFilter = solrDispatchFilter;
this.cores = cores;
this.req = request;
this.response = response;
this.retry = retry;
this.requestType = RequestType.UNKNOWN;
req.setAttribute(HttpSolrCall.class.getName(), this);
queryParams = SolrRequestParsers.parseQueryString(req.getQueryString());
// set a request timer which can be reused by requests if needed
req.setAttribute(SolrRequestParsers.REQUEST_TIMER_SERVLET_ATTRIBUTE, new RTimerTree());
// put the core container in request attribute
req.setAttribute("org.apache.solr.CoreContainer", cores);
path = ServletUtils.getPathAfterContext(req);
if (log.isTraceEnabled()) log.trace("Path is parsed as {}", path);
}
public String getPath() {
return path;
}
public HttpServletRequest getReq() {
return req;
}
public SolrParams getQueryParams() {
return queryParams;
}
protected Aliases getAliases() {
return cores.isZooKeeperAware() ? cores.getZkController().getZkStateReader().getAliases() : Aliases.EMPTY;
}
/** The collection(s) referenced in this request. Populated in {@link #init()}. Not null. */
public List<String> getCollectionsList() {
return collectionsList != null ? collectionsList : Collections.emptyList();
}
protected void init() throws Exception {
// check for management path
if (!cores.isZooKeeperAware()) {
String alternate = cores.getManagementPath();
if (alternate != null && path.startsWith(alternate)) {
path = path.substring(0, alternate.length());
}
}
if (log.isTraceEnabled()) log.trace("Full path {} Path is parsed as {} managment path is {}", req.getRequestURI() + req.getQueryString(), path);
// Check for container handlers
handler = cores.getRequestHandler(path);
if (log.isTraceEnabled()) log.trace("Check for handler {} returned {} handlers={}", path, handler, cores.getRequestHandlers().keySet());
if (handler != null) {
solrReq = SolrRequestParsers.DEFAULT.parse(null, path, req);
solrReq.getContext().put(CoreContainer.class.getName(), cores);
requestType = RequestType.ADMIN;
action = ADMIN;
return;
}
// int idx = path.lastIndexOf('/');
// if (idx > 0) {
// // save the portion after the ':' for a 'handler' path parameter
// path = path.substring(0, idx);
// }
// Parse a core or collection name from the path and attempt to see if it's a core name
int idx = path.indexOf("/", 0);
int idx2 = -1;
if (idx > -1) {
idx2 = path.indexOf('/', 1);
if (idx2 > 0) {
// save the portion after the ':' for a 'handler' path parameter
origCorename = path.substring(1, idx2);
} else {
origCorename = path.substring(1, path.length());
}
// Try to resolve a Solr core name
core = cores.getCore(origCorename);
if (core == null && cores.isCoreLoading(origCorename)) {
log.debug("core is loading, will wait a bit");
cores.waitForLoadingCore(origCorename, 5000);
core = cores.getCore(origCorename);
}
if (core == null && log.isDebugEnabled()) {
log.debug("tried to get core by name {} got {}, existing cores {} loading={} found={}", origCorename, core, cores.getAllCoreNames(), cores.getLoadedCoreNames(), core != null);
}
if (core != null) {
if (idx2 > 0) {
path = path.substring(idx2);
}
if (log.isDebugEnabled()) log.debug("Path is parsed as {}", path);
} else {
if (!cores.isZooKeeperAware()) {
core = cores.getCore("");
}
}
}
if (core == null && cores.isZooKeeperAware()) {
// init collectionList (usually one name but not when there are aliases)
String def = core != null ? core.getCoreDescriptor().getCollectionName() : origCorename;
collectionsList = resolveCollectionListOrAlias(queryParams.get(COLLECTION_PROP, def)); // &collection= takes precedence
if (core == null) {
// lookup core from collection, or route away if need to
String collectionName = collectionsList.isEmpty() ? null : collectionsList.get(0); // route to 1st
//TODO try the other collections if can't find a local replica of the first? (and do to V2HttpSolrCall)
boolean isPreferLeader = (path.endsWith("/update") || path.contains("/update/"));
if (SYSTEM_COLL.equals(collectionName)) {
autoCreateSystemColl(collectionName);
}
core = getCoreByCollection(collectionName, isPreferLeader); // find a local replica/core for the collection
if (core != null) {
if (idx2 > 0) {
path = path.substring(idx2);
}
if (log.isDebugEnabled()) log.debug("Path is parsed as {}", path);
} else {
// if we couldn't find it locally, look on other nodes
if (log.isDebugEnabled()) log.debug("check remote path extraction {} {}", collectionName, origCorename);
extractRemotePath(origCorename);
if (action == REMOTEQUERY) {
if (idx2 > 0) {
path = path.substring(idx2);
}
if (log.isDebugEnabled()) log.debug("Path is parsed as {}", path);
return;
} else {
extractRemotePath(collectionName);
if (action == REMOTEQUERY) {
if (idx2 > 0) {
path = path.substring(idx2);
}
if (log.isDebugEnabled()) log.debug("Path is parsed as {}", path);
return;
}
}
}
//core is not available locally or remotely
if (action != null) return;
}
}
// With a valid core...
if (core != null) {
config = core.getSolrConfig();
// get or create/cache the parser for the core
SolrRequestParsers parser = config.getRequestParsers();
// Determine the handler from the url path if not set
// (we might already have selected the cores handler)
extractHandlerFromURLPath(parser);
if (action != null) return;
// With a valid handler and a valid core...
if (handler != null) {
// if not a /select, create the request
if (solrReq == null) {
solrReq = parser.parse(core, path, req);
}
invalidStates = checkStateVersionsAreValid(getCollectionsList(), queryParams.get(CloudSolrClient.STATE_VERSION));
addCollectionParamIfNeeded(getCollectionsList());
action = PROCESS;
return; // we are done with a valid handler
} else {
if (req.getMethod().equals("HEAD")) {
action = RETURN;
return;
}
}
}
log.debug("no handler or core retrieved for {}, follow through...", path);
action = PASSTHROUGH;
}
protected void autoCreateSystemColl(String corename) throws Exception {
if (core == null &&
SYSTEM_COLL.equals(corename) &&
"POST".equals(req.getMethod()) &&
!cores.getZkController().getClusterState().hasCollection(SYSTEM_COLL)) {
log.info("Going to auto-create {} collection", SYSTEM_COLL);
SolrQueryResponse rsp = new SolrQueryResponse();
String repFactor = String.valueOf(Math.min(3, cores.getZkController().getZkStateReader().getLiveNodes().size()));
cores.getCollectionsHandler().handleRequestBody(new LocalSolrQueryRequest(null,
new ModifiableSolrParams()
.add(ACTION, CREATE.toString())
.add( NAME, SYSTEM_COLL)
.add(REPLICATION_FACTOR, repFactor)), rsp);
if (rsp.getValues().get("success") == null) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Could not auto-create " + SYSTEM_COLL + " collection: "+ Utils.toJSONString(rsp.getValues()));
}
TimeOut timeOut = new TimeOut(1, TimeUnit.SECONDS, TimeSource.NANO_TIME);
for (; ; ) {
if (cores.getZkController().getClusterState().getCollectionOrNull(SYSTEM_COLL) != null) {
break;
} else {
if (timeOut.hasTimedOut()) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Could not find " + SYSTEM_COLL + " collection even after 1 second");
}
timeOut.sleep(50);
}
}
action = RETRY;
}
}
/**
* Resolves the parameter as a potential comma delimited list of collections, and resolves aliases too.
* One level of aliases pointing to another alias is supported.
* De-duplicates and retains the order.
* {@link #getCollectionsList()}
*/
protected List<String> resolveCollectionListOrAlias(String collectionStr) {
if (collectionStr == null || collectionStr.trim().isEmpty()) {
return Collections.emptyList();
}
List<String> result = null;
LinkedHashSet<String> uniqueList = null;
Aliases aliases = getAliases();
List<String> inputCollections = StrUtils.splitSmart(collectionStr, ",", true);
if (inputCollections.size() > 1) {
uniqueList = new LinkedHashSet<>();
}
for (String inputCollection : inputCollections) {
List<String> resolvedCollections = aliases.resolveAliases(inputCollection);
if (uniqueList != null) {
uniqueList.addAll(resolvedCollections);
} else {
result = resolvedCollections;
}
}
if (uniqueList != null) {
return Collections.unmodifiableList(new ArrayList<>(uniqueList));
} else {
return Collections.unmodifiableList(result);
}
}
/**
* Extract handler from the URL path if not set.
*/
protected void extractHandlerFromURLPath(SolrRequestParsers parser) throws Exception {
if (log.isTraceEnabled()) {
log.trace("Extract handler from url path {} {}", handler, path);
}
if (handler == null && path.length() > 1) { // don't match "" or "/" as valid path
handler = core.getRequestHandler(path);
if (log.isTraceEnabled()) {
log.trace("handler={} name={}", handler, path);
}
// no handler yet but <requestDispatcher> allows us to handle /select with a 'qt' param
if (handler == null && parser.isHandleSelect()) {
if ("/select".equals(path) || "/select/".equals(path)) {
solrReq = parser.parse(core, path, req);
String qt = solrReq.getParams().get(CommonParams.QT);
handler = core.getRequestHandler(qt);
if (handler == null) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "unknown handler: " + qt);
}
if (qt != null && qt.startsWith("/") && (handler instanceof ContentStreamHandlerBase)) {
//For security reasons it's a bad idea to allow a leading '/', ex: /select?qt=/update see SOLR-3161
//There was no restriction from Solr 1.4 thru 3.5 and it's not supported for update handlers.
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid Request Handler ('qt'). Do not use /select to access: " + qt);
}
}
}
}
}
protected void extractRemotePath(String collectionName) throws UnsupportedEncodingException, KeeperException, InterruptedException, SolrException, TimeoutException {
coreUrl = getRemoteCoreUrl(collectionName);
// don't proxy for internal update requests
// Map<String,Integer> invalidStates = checkStateVersionsAreValid(getCollectionsList(), queryParams.get(CloudSolrClient.STATE_VERSION));
if (coreUrl != null
&& queryParams.get(DistributingUpdateProcessorFactory.DISTRIB_UPDATE_PARAM) == null) {
// if (invalidStates != null) {
// //it does not make sense to send the request to a remote node
// throw new SolrException(SolrException.ErrorCode.INVALID_STATE, new String(Utils.toJSON(invalidStates), org.apache.lucene.util.IOUtils.UTF_8));
// }
action = REMOTEQUERY;
} else {
if (!retry) {
// we couldn't find a core to work with, try reloading aliases & this collection
// cores.getZkController().getZkStateReader().aliasesManager.update();
// action = RETRY;
}
}
}
Action authorize() throws IOException {
AuthorizationContext context = getAuthCtx();
log.debug("AuthorizationContext : {}", context);
AuthorizationResponse authResponse = cores.getAuthorizationPlugin().authorize(context);
int statusCode = authResponse.statusCode;
log.info("Authorization response status code {}", authResponse.statusCode);
if (statusCode == AuthorizationResponse.PROMPT.statusCode) {
Map<String, String> headers = (Map) getReq().getAttribute(AuthenticationPlugin.class.getName());
if (headers != null) {
for (Map.Entry<String, String> e : headers.entrySet()) response.setHeader(e.getKey(), e.getValue());
}
if (log.isDebugEnabled()) {
log.debug("USER_REQUIRED {} {}", req.getHeader("Authorization"), req.getUserPrincipal());
}
sendError(statusCode,
"Authentication failed, Response code: " + statusCode);
if (shouldAudit(EventType.REJECTED)) {
cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.REJECTED, req, context));
}
return RETURN;
}
if (statusCode == AuthorizationResponse.FORBIDDEN.statusCode) {
if (log.isDebugEnabled()) {
log.debug("UNAUTHORIZED auth header {} context : {}, msg: {}", req.getHeader("Authorization"), context, authResponse.getMessage());
}
sendError(statusCode,
"Unauthorized request, Response code: " + statusCode);
if (shouldAudit(EventType.UNAUTHORIZED)) {
cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.UNAUTHORIZED, req, context));
}
return RETURN;
}
if (!(statusCode == HttpStatus.SC_ACCEPTED) && !(statusCode == HttpStatus.SC_OK)) {
log.warn("ERROR {} during authentication: {}", statusCode, authResponse.getMessage());
sendError(statusCode,
"ERROR during authorization, Response code: " + statusCode);
if (shouldAudit(EventType.ERROR)) {
cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.ERROR, req, context));
}
return RETURN;
}
if (shouldAudit(EventType.AUTHORIZED)) {
cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.AUTHORIZED, req, context));
}
return ADMIN;
}
/**
* This method processes the request.
*/
public Action call() throws IOException {
MDCLoggingContext.reset();
Span activeSpan = GlobalTracer.getTracer().activeSpan();
if (activeSpan != null) {
MDCLoggingContext.setTracerId(activeSpan.context().toTraceId());
}
if (cores.isZooKeeperAware()) {
MDCLoggingContext.setNode(cores.getZkController().getNodeName());
}
if (cores == null) {
sendError(503, "Server is shutting down or failed to initialize");
return RETURN;
}
if (solrDispatchFilter.abortErrorMessage != null) {
sendError(500, solrDispatchFilter.abortErrorMessage);
if (shouldAudit(EventType.ERROR)) {
cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.ERROR, getReq()));
}
return RETURN;
}
try {
if (action == null) {
init();
}
// Perform authorization here, if:
// (a) Authorization is enabled, and
// (b) The requested resource is not a known static file
// (c) And this request should be handled by this node (see NOTE below)
// NOTE: If the query is to be handled by another node, then let that node do the authorization.
// In case of authentication using BasicAuthPlugin, for example, the internode request
// is secured using PKI authentication and the internode request here will contain the
// original user principal as a payload/header, using which the receiving node should be
// able to perform the authorization.
if (cores.getAuthorizationPlugin() != null && shouldAuthorize()
&& !(action == REMOTEQUERY || action == FORWARD)) {
Action authorizationAction = authorize();
if (authorizationAction == RETURN) {
return authorizationAction;
}
}
HttpServletResponse resp = response;
switch (action) {
case ADMIN:
handleAdminRequest();
return RETURN;
case REMOTEQUERY:
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, new SolrQueryResponse(), action));
Action a = remoteQuery(coreUrl + path);
return a;
case PROCESS:
final Method reqMethod = Method.getMethod(req.getMethod());
HttpCacheHeaderUtil.setCacheControlHeader(config, resp, reqMethod);
// unless we have been explicitly told not to, do cache validation
// if we fail cache validation, execute the query
if (config.getHttpCachingConfig().isNever304() ||
!HttpCacheHeaderUtil.doCacheHeaderValidation(solrReq, req, reqMethod, resp)) {
SolrQueryResponse solrRsp = new SolrQueryResponse();
/* even for HEAD requests, we need to execute the handler to
* ensure we don't get an error (and to make sure the correct
* QueryResponseWriter is selected and we get the correct
* Content-Type)
*/
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp, action));
execute(solrRsp);
if (shouldAudit()) {
EventType eventType = solrRsp.getException() == null ? EventType.COMPLETED : EventType.ERROR;
if (shouldAudit(eventType)) {
cores.getAuditLoggerPlugin().doAudit(
new AuditEvent(eventType, req, getAuthCtx(), solrReq.getRequestTimer().getTime(), solrRsp.getException()));
}
}
HttpCacheHeaderUtil.checkHttpCachingVeto(solrRsp, resp, reqMethod);
Iterator<Map.Entry<String, String>> headers = solrRsp.httpHeaders();
while (headers.hasNext()) {
Map.Entry<String, String> entry = headers.next();
resp.addHeader(entry.getKey(), entry.getValue());
}
QueryResponseWriter responseWriter = getResponseWriter();
//if (invalidStates != null) solrReq.getContext().put(CloudSolrClient.STATE_VERSION, invalidStates);
writeResponse(solrRsp, responseWriter, reqMethod);
}
return RETURN;
default: return action;
}
} catch (Throwable ex) {
if (!(ex instanceof PrepRecoveryOp.NotValidLeader) && shouldAudit(EventType.ERROR)) {
cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.ERROR, ex, req));
}
sendError(ex);
// walk the the entire cause chain to search for an Error
Throwable t = ex;
while (t != null) {
if (t instanceof Error) {
if (t != ex) {
log.error("An Error was wrapped in another exception - please report complete stacktrace on SOLR-6161", ex);
}
throw (Error) t;
}
t = t.getCause();
}
return RETURN;
}
}
private boolean shouldAudit() {
return cores.getAuditLoggerPlugin() != null;
}
private boolean shouldAudit(AuditEvent.EventType eventType) {
return shouldAudit() && cores.getAuditLoggerPlugin().shouldLog(eventType);
}
private boolean shouldAuthorize() {
if(PublicKeyHandler.PATH.equals(path)) return false;
//admin/info/key is the path where public key is exposed . it is always unsecured
if ("/".equals(path) || "/solr/".equals(path)) return false; // Static Admin UI files must always be served
if (cores.getPkiAuthenticationPlugin() != null && req.getUserPrincipal() != null) {
boolean b = cores.getPkiAuthenticationPlugin().needsAuthorization(req);
log.debug("PkiAuthenticationPlugin says authorization required : {} ", b);
return b;
}
return true;
}
void destroy() {
try {
if (solrReq != null) {
if (log.isTraceEnabled()) {
log.trace("Closing out SolrRequest: {}", solrReq);
}
IOUtils.closeQuietly(solrReq);
}
} finally {
try {
if (core != null) {
IOUtils.closeQuietly(core);
}
} finally {
SolrRequestInfo.clearRequestInfo();
}
AuthenticationPlugin authcPlugin = cores.getAuthenticationPlugin();
if (authcPlugin != null) authcPlugin.closeRequest();
}
}
private Action remoteQuery(String coreUrl) throws IOException {
if (req != null) {
log.info("proxy to:" + coreUrl + "?" + req.getQueryString());
// MRM TODO: - dont proxy around too much
String fhost = req.getHeader(HttpHeader.X_FORWARDED_FOR.toString());
if (fhost != null) {
// Already proxied
log.warn("Already proxied, return 404");
sendError(404, "No SolrCore found to service request.");
return RETURN;
}
//System.out.println("protocol:" + req.getProtocol());
URL url = new URL(coreUrl + "?" + (req.getQueryString() != null ? req.getQueryString() : ""));
final Request proxyRequest;
try {
proxyRequest = solrDispatchFilter.httpClient.newRequest(url.toURI())
.method(req.getMethod())
.version(HttpVersion.fromString(req.getProtocol()));
} catch(IllegalArgumentException e) {
log.error("Error parsing URI for proxying " + url, e);
throw new SolrException(ErrorCode.SERVER_ERROR, e);
} catch (URISyntaxException e) {
log.error("Error parsing URI for proxying " + url, e);
throw new SolrException(ErrorCode.SERVER_ERROR, e);
}
copyRequestHeaders(req, proxyRequest);
addProxyHeaders(req, proxyRequest);
if (hasContent(req)) {
InputStreamContentProvider defferedContent = new InputStreamContentProvider(req.getInputStream(), 8192, false);
proxyRequest.content(defferedContent);
}
AtomicReference<Throwable> failException = new AtomicReference<>();
InputStreamResponseListener listener = new InputStreamResponseListener() {
public void onComplete(Result result) {
super.onComplete(result);
if (result.isFailed()) {
failException.set(result.getFailure());
}
}
@Override
public void onHeaders(Response resp) {
super.onHeaders(resp);
//System.out.println("resp code:" + resp.getStatus());
for (HttpField field : resp.getHeaders()) {
String headerName = field.getName();
String lowerHeaderName = headerName.toLowerCase(Locale.ROOT);
// System.out.println("response header: " + headerName + " : " + field.getValue() + " status:" +
// resp.getStatus());
if (HOP_HEADERS.contains(lowerHeaderName))
continue;
response.addHeader(headerName, field.getValue());
}
response.setStatus(resp.getStatus());
super.onHeaders(resp);
}
};
proxyRequest.send(listener);
try {
listener.get(60, TimeUnit.SECONDS);
} catch (Exception e) {
throw new SolrException(ErrorCode.SERVER_ERROR, e);
}
listener.getInputStream().transferTo(response.getOutputStream());
if (failException.get() != null) {
sendError(failException.get());
}
}
return RETURN;
}
protected boolean hasContent(HttpServletRequest clientRequest) {
boolean hasContent = clientRequest.getContentLength() > 0 ||
clientRequest.getContentType() != null ||
clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
return hasContent;
}
protected void addProxyHeaders(HttpServletRequest clientRequest, Request proxyRequest) {
proxyRequest.header(HttpHeader.VIA, "HTTP/2.0 Solr Proxy"); //MRM TODO: protocol hard code
proxyRequest.header(HttpHeader.X_FORWARDED_FOR, clientRequest.getRemoteAddr());
// we have some tricky to see in tests header size limitations
// proxyRequest.header(HttpHeader.X_FORWARDED_PROTO, clientRequest.getScheme());
// proxyRequest.header(HttpHeader.X_FORWARDED_HOST, clientRequest.getHeader(HttpHeader.HOST.asString()));
// proxyRequest.header(HttpHeader.X_FORWARDED_SERVER, clientRequest.getLocalName());
proxyRequest.header(QoSParams.REQUEST_SOURCE, QoSParams.INTERNAL);
}
protected void copyRequestHeaders(HttpServletRequest clientRequest, Request proxyRequest) {
// First clear possibly existing headers, as we are going to copy those from the client request.
proxyRequest.getHeaders().clear();
Set<String> headersToRemove = findConnectionHeaders(clientRequest);
for (Enumeration<String> headerNames = clientRequest.getHeaderNames(); headerNames.hasMoreElements();) {
String headerName = headerNames.nextElement();
String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
if (HttpHeader.HOST.is(headerName) && !preserveHost)
continue;
// Remove hop-by-hop headers.
if (HOP_HEADERS.contains(lowerHeaderName))
continue;
if (headersToRemove != null && headersToRemove.contains(lowerHeaderName))
continue;
for (Enumeration<String> headerValues = clientRequest.getHeaders(headerName); headerValues.hasMoreElements();) {
String headerValue = headerValues.nextElement();
if (headerValue != null) {
proxyRequest.header(headerName, headerValue);
//System.out.println("request header: " + headerName + " : " + headerValue);
}
}
}
// Force the Host header if configured
// if (_hostHeader != null)
// proxyRequest.header(HttpHeader.HOST, _hostHeader);
}
protected Set<String> findConnectionHeaders(HttpServletRequest clientRequest)
{
// Any header listed by the Connection header must be removed:
// http://tools.ietf.org/html/rfc7230#section-6.1.
Set<String> hopHeaders = null;
Enumeration<String> connectionHeaders = clientRequest.getHeaders(HttpHeader.CONNECTION.asString());
while (connectionHeaders.hasMoreElements())
{
String value = connectionHeaders.nextElement();
String[] values = value.split(",");
for (String name : values)
{
name = name.trim().toLowerCase(Locale.ENGLISH);
if (hopHeaders == null)
hopHeaders = new HashSet<>();
hopHeaders.add(name);
}
}
return hopHeaders;
}
protected void sendError(Throwable ex) throws IOException {
Exception exp = null;
try {
SolrQueryResponse solrResp = new SolrQueryResponse();
if (ex instanceof Exception) {
solrResp.setException((Exception) ex);
} else {
solrResp.setException(new RuntimeException(ex));
}
if (solrReq == null) {
final SolrParams solrParams;
if (req != null) {
// use GET parameters if available:
solrParams = SolrRequestParsers.parseQueryString(req.getQueryString());
} else {
// we have no params at all, use empty ones:
solrParams = new MapSolrParams(Collections.emptyMap());
}
solrReq = new SolrQueryRequestBase(core, solrParams) {
};
}
QueryResponseWriter writer = getResponseWriter();
writeResponse(solrResp, writer, Method.GET);
} catch (Exception e) { // This error really does not matter
exp = e;
} finally {
if (exp != null) {
SimpleOrderedMap info = new SimpleOrderedMap();
int code = ResponseUtils.getErrorInfo(ex, info, log);
sendError(code, info.toString());
}
}
}
protected void sendError(int code, String message) throws IOException {
try {
response.sendError(code, message);
} catch (Exception e) {
log.info("Unable to write error response, client closed connection or we are shutting down", e);
}
}
protected void execute(SolrQueryResponse rsp) {
// a custom filter could add more stuff to the request before passing it on.
// for example: sreq.getContext().put( "HttpServletRequest", req );
// used for logging query stats in SolrCore.execute()
solrReq.getContext().put("webapp", req.getContextPath());
solrReq.getCore().execute(handler, solrReq, rsp);
}
private void handleAdminRequest() throws IOException {
SolrQueryResponse solrResp = new SolrQueryResponse();
SolrCore.preDecorateResponse(solrReq, solrResp);
handleAdmin(solrResp);
SolrCore.postDecorateResponse(handler, solrReq, solrResp);
if (log.isInfoEnabled() && solrResp.getToLog().size() > 0) {
log.info(solrResp.getToLogAsString("[admin]"));
}
QueryResponseWriter respWriter = SolrCore.DEFAULT_RESPONSE_WRITERS.get(solrReq.getParams().get(CommonParams.WT));
if (respWriter == null) respWriter = getResponseWriter();
writeResponse(solrResp, respWriter, Method.getMethod(req.getMethod()));
if (shouldAudit()) {
EventType eventType = solrResp.getException() == null ? EventType.COMPLETED : EventType.ERROR;
if (shouldAudit(eventType)) {
cores.getAuditLoggerPlugin().doAudit(
new AuditEvent(eventType, req, getAuthCtx(), solrReq.getRequestTimer().getTime(), solrResp.getException()));
}
}
}
/**
* Returns {@link QueryResponseWriter} to be used.
* When {@link CommonParams#WT} not specified in the request or specified value doesn't have
* corresponding {@link QueryResponseWriter} then, returns the default query response writer
* Note: This method must not return null
*/
protected QueryResponseWriter getResponseWriter() {
String wt = solrReq.getParams().get(CommonParams.WT);
if (core != null) {
return core.getQueryResponseWriter(wt);
} else {
return SolrCore.DEFAULT_RESPONSE_WRITERS.getOrDefault(wt,
SolrCore.DEFAULT_RESPONSE_WRITERS.get("standard"));
}
}
protected void handleAdmin(SolrQueryResponse solrResp) {
handler.handleRequest(solrReq, solrResp);
}
/**
* Sets the "collection" parameter on the request to the list of alias-resolved collections for this request.
* It can be avoided sometimes.
* Note: {@link org.apache.solr.handler.component.HttpShardHandler} processes this param.
* @see #getCollectionsList()
*/
protected void addCollectionParamIfNeeded(List<String> collections) {
if (collections.isEmpty()) {
return;
}
assert cores.isZooKeeperAware();
String collectionParam = queryParams.get(COLLECTION_PROP);
// if there is no existing collection param and the core we go to is for the expected collection,
// then we needn't add a collection param
if (collectionParam == null && // if collection param already exists, we may need to over-write it
core != null && collections.equals(Collections.singletonList(core.getCoreDescriptor().getCollectionName()))) {
return;
}
String newCollectionParam = StrUtils.join(collections, ',');
if (newCollectionParam.equals(collectionParam)) {
return;
}
// TODO add a SolrRequest.getModifiableParams ?
ModifiableSolrParams params = new ModifiableSolrParams(solrReq.getParams());
params.set(COLLECTION_PROP, newCollectionParam);
solrReq.setParams(params);
}
private void writeResponse(SolrQueryResponse solrRsp, QueryResponseWriter responseWriter, Method reqMethod)
throws IOException {
try {
Object invalidStates = solrReq.getContext().get(CloudSolrClient.STATE_VERSION);
//This is the last item added to the response and the client would expect it that way.
//If that assumption is changed , it would fail. This is done to avoid an O(n) scan on
// the response for each request
if (invalidStates != null) solrRsp.add(CloudSolrClient.STATE_VERSION, invalidStates);
// Now write it out
final String ct = responseWriter.getContentType(solrReq, solrRsp);
// don't call setContentType on null
if (null != ct) response.setContentType(ct);
if (solrRsp.getException() != null) {
NamedList info = new SimpleOrderedMap();
int code = ResponseUtils.getErrorInfo(solrRsp.getException(), info, log);
solrRsp.add("error", info);
response.setStatus(code);
}
if (Method.HEAD != reqMethod) {
OutputStream out = response.getOutputStream();
QueryResponseWriterUtil.writeQueryResponse(out, responseWriter, solrReq, solrRsp, ct);
}
//else http HEAD request, nothing to write out, waited this long just to get ContentType
} catch (EOFException e) {
log.info("Unable to write response, client closed connection or we are shutting down", e);
}
}
/** Returns null if the state ({@link CloudSolrClient#STATE_VERSION}) is good; otherwise returns state problems. */
private Map<String, Integer> checkStateVersionsAreValid(List<String> collectionsList, String stateVer) {
Map<String, Integer> result = null;
String[] pairs;
if (stateVer != null && !stateVer.isEmpty() && cores.isZooKeeperAware()) {
// many have multiple collections separated by |
pairs = StringUtils.split(stateVer, '|');
for (String pair : pairs) {
String[] pcs = StringUtils.split(pair, ':');
if (pcs.length == 2 && !pcs[0].isEmpty() && !pcs[1].isEmpty()) {
String[] versionAndUpdatesHash = pcs[1].split(">");
int version = Integer.parseInt(versionAndUpdatesHash[0]);
int updateHash = Integer.parseInt(versionAndUpdatesHash[1]);
Integer status = cores.getZkController().getZkStateReader().compareStateVersions(pcs[0], version, updateHash);
if (status != null) {
if (result == null) result = new HashMap<>();
result.put(pcs[0], status);
}
}
}
}
return result;
}
private Map<String, Integer> getStateVersions(String stateVer) {
// TODO: for collections that are local and watched, we should just wait for the right min state, not eager fetch everything
Map<String, Integer> result = null;
String[] pairs;
if (stateVer != null && !stateVer.isEmpty() && cores.isZooKeeperAware()) {
// many have multiple collections separated by |
pairs = StringUtils.split(stateVer, '|');
for (String pair : pairs) {
String[] pcs = StringUtils.split(pair, ':');
if (pcs.length == 2 && !pcs[0].isEmpty() && !pcs[1].isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("compare version states {} {}", pcs[0], Integer.parseInt(pcs[1]));
}
if (result == null) result = new HashMap<>();
result.put(pcs[0], Integer.parseInt(pcs[1]));
}
}
}
if (log.isTraceEnabled()) {
log.trace("compare version states result {} {}", stateVer, result);
}
return result;
}
protected SolrCore getCoreByCollection(String collectionName, boolean isPreferLeader) throws TimeoutException, InterruptedException {
log.debug("get core by collection {} {}", collectionName, isPreferLeader);
ZkStateReader zkStateReader = cores.getZkController().getZkStateReader();
ClusterState clusterState = zkStateReader.getClusterState();
DocCollection collection = clusterState.getCollectionOrNull(collectionName);
if (collection == null) {
log.debug("no local core found for collection={}", collectionName);
return null;
}
if (isPreferLeader) {
List<Replica> leaderReplicas = collection.getLeaderReplicas(cores.getZkController().getNodeName());
log.debug("preferLeader leaderReplicas={}", leaderReplicas);
SolrCore core = randomlyGetSolrCore(leaderReplicas, true);
if (core != null) return core;
}
List<Replica> replicas = collection.getReplicas(cores.getZkController().getNodeName());
if (log.isDebugEnabled()) log.debug("replicas for node {} {}", replicas, cores.getZkController().getNodeName());
SolrCore returnCore = randomlyGetSolrCore(replicas, true);
if (log.isDebugEnabled()) log.debug("returning core by collection {}", returnCore == null ? null : returnCore.getName());
return returnCore;
}
private SolrCore randomlyGetSolrCore(List<Replica> replicas, boolean checkActive) {
if (replicas != null) {
RandomIterator<Replica> it = new RandomIterator<>(random, replicas);
while (it.hasNext()) {
Replica replica = it.next();
SolrCore core = checkProps(replica);
if (core != null && checkActive) {
try {
cores.getZkController().getZkStateReader().waitForState(core.getCoreDescriptor().getCollectionName(), 1, TimeUnit.SECONDS, (liveNodes1, coll) -> {
if (coll == null) {
return false;
}
Replica rep = coll.getReplica(replica.getName());
if (rep == null) {
return false;
}
if (rep.getState() == Replica.State.ACTIVE) {
return true;
}
return false;
});
} catch (InterruptedException e) {
log.debug("interrupted waiting to see active replica");
return null;
} catch (TimeoutException e) {
log.debug("timeout waiting to see active replica {} {}", replica.getName(), replica.getState());
return null;
}
return core;
}
}
}
return null;
}
private SolrCore checkProps(Replica replica) {
SolrCore core = null;
boolean nodeMatches = cores.getZkController().getNodeName().equals(replica.getNodeName());
if (nodeMatches) {
if (cores.isCoreLoading(replica.getName())) {
cores.waitForLoadingCore(replica.getName(), 10000);
}
core = cores.getCore(replica.getName());
}
log.debug("check local core has correct props replica={} nodename={} nodematches={} core found={}", replica, cores.getZkController().getNodeName(), nodeMatches, core != null);
return core;
}
protected String getRemoteCoreUrl(String collectionName) throws SolrException {
ClusterState clusterState = cores.getZkController().getClusterState();
final DocCollection docCollection = clusterState.getCollectionOrNull(collectionName, false);
if (docCollection == null) {
return null;
}
String coreUrl = getCoreUrl(docCollection.getSlices());
if (log.isDebugEnabled()) {
log.debug("get remote core url returning {} for {} {}", coreUrl, collectionName, origCorename);
}
return coreUrl;
}
private String getCoreUrl(Collection<Slice> slices) {
String coreUrl;
for (Slice slice : slices) {
List<Replica> randomizedReplicas = new ArrayList<>(slice.getReplicas());
Collections.shuffle(randomizedReplicas, random);
for (Replica replica : randomizedReplicas) {
log.debug("check replica {} with node name {} against live nodes {} with state {}",
replica.getName(), replica.getNodeName(), cores.getZkController().getZkStateReader().getLiveNodes(), replica.getState());
if (!replica.getNodeName().equals(cores.getZkController().getNodeName()) && cores.getZkController().zkStateReader.getLiveNodes().contains(replica.getNodeName())
&& replica.getState() == Replica.State.ACTIVE) {
coreUrl = replica.getCoreUrl();
return coreUrl;
}
}
}
return null;
}
protected Object _getHandler(){
return handler;
}
private AuthorizationContext getAuthCtx() {
String resource = getPath();
SolrParams params = getQueryParams();
final ArrayList<CollectionRequest> collectionRequests = new ArrayList<>();
for (String collection : getCollectionsList()) {
collectionRequests.add(new CollectionRequest(collection));
}
// Extract collection name from the params in case of a Collection Admin request
if (getPath().equals("/admin/collections")) {
if (CREATE.isEqual(params.get("action"))||
RELOAD.isEqual(params.get("action"))||
DELETE.isEqual(params.get("action")))
collectionRequests.add(new CollectionRequest(params.get("name")));
else if (params.get(COLLECTION_PROP) != null)
collectionRequests.add(new CollectionRequest(params.get(COLLECTION_PROP)));
}
// Populate the request type if the request is select or update
if(requestType == RequestType.UNKNOWN) {
if(resource.startsWith("/select") || resource.startsWith("/get"))
requestType = RequestType.READ;
if(resource.startsWith("/update"))
requestType = RequestType.WRITE;
}
return new AuthorizationContext() {
@Override
public SolrParams getParams() {
return null == solrReq ? null : solrReq.getParams();
}
@Override
public Principal getUserPrincipal() {
return getReq().getUserPrincipal();
}
@Override
public String getHttpHeader(String s) {
return getReq().getHeader(s);
}
@Override
public Enumeration<String> getHeaderNames() {
return getReq().getHeaderNames();
}
@Override
public List<CollectionRequest> getCollectionRequests() {
return collectionRequests;
}
@Override
public RequestType getRequestType() {
return requestType;
}
public String getResource() {
return path;
}
@Override
public String getHttpMethod() {
return getReq().getMethod();
}
@Override
public Object getHandler() {
return _getHandler();
}
@Override
public String toString() {
StringBuilder response = new StringBuilder("userPrincipal: [").append(getUserPrincipal()).append("]")
.append(" type: [").append(requestType.toString()).append("], collections: [");
for (CollectionRequest collectionRequest : collectionRequests) {
response.append(collectionRequest.collectionName).append(", ");
}
if(collectionRequests.size() > 0)
response.delete(response.length() - 1, response.length());
response.append("], Path: [").append(resource).append("]");
response.append(" path : ").append(path).append(" params :").append(getParams());
return response.toString();
}
@Override
public String getRemoteAddr() {
return getReq().getRemoteAddr();
}
@Override
public String getRemoteHost() {
return getReq().getRemoteHost();
}
};
}
static final String CONNECTION_HEADER = "Connection";
static final String TRANSFER_ENCODING_HEADER = "Transfer-Encoding";
static final String CONTENT_LENGTH_HEADER = "Content-Length";
List<CommandOperation> parsedCommands;
public List<CommandOperation> getCommands(boolean validateInput) {
if (parsedCommands == null) {
Iterable<ContentStream> contentStreams = solrReq.getContentStreams();
if (contentStreams == null) parsedCommands = Collections.EMPTY_LIST;
else {
parsedCommands = ApiBag.getCommandOperations(contentStreams.iterator().next(), getValidators(), validateInput);
}
}
return CommandOperation.clone(parsedCommands);
}
protected ValidatingJsonMap getSpec() {
return null;
}
protected Map<String, JsonSchemaValidator> getValidators(){
return Collections.EMPTY_MAP;
}
/**
* A faster method for randomly picking items when you do not need to
* consume all items.
*/
private static class RandomIterator<E> implements Iterator<E> {
private Random rand;
private ArrayList<E> elements;
private int size;
public RandomIterator(Random rand, Collection<E> elements) {
this.rand = rand;
this.elements = new ArrayList<>(elements);
this.size = elements.size();
}
@Override
public boolean hasNext() {
return size > 0;
}
@Override
public E next() {
int idx = rand.nextInt(size);
E e1 = elements.get(idx);
E e2 = elements.get(size-1);
elements.set(idx,e2);
size--;
return e1;
}
}
protected static final Set<String> HOP_HEADERS;
static
{
Set<String> hopHeaders = new HashSet<>(12);
hopHeaders.add("accept-encoding");
hopHeaders.add("connection");
hopHeaders.add("keep-alive");
hopHeaders.add("proxy-authorization");
hopHeaders.add("proxy-authenticate");
hopHeaders.add("proxy-connection");
hopHeaders.add("transfer-encoding");
hopHeaders.add("te");
hopHeaders.add("trailer");
hopHeaders.add("upgrade");
// hopHeaders.add(HttpHeader.X_FORWARDED_FOR.asString());
// hopHeaders.add(HttpHeader.X_FORWARDED_PROTO.asString());
// hopHeaders.add(HttpHeader.VIA.asString());
// hopHeaders.add(HttpHeader.X_FORWARDED_HOST.asString());
// hopHeaders.add(HttpHeader.SERVER.asString());
//
HOP_HEADERS = Collections.unmodifiableSet(hopHeaders);
}
}