/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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.elasticsearch.test.rest.client; import com.carrotsearch.randomizedtesting.RandomizedTest; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContexts; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.lucene.util.IOUtils; import org.elasticsearch.Version; import org.elasticsearch.client.support.Headers; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.rest.client.http.HttpRequestBuilder; import org.elasticsearch.test.rest.client.http.HttpResponse; import org.elasticsearch.test.rest.spec.RestApi; import org.elasticsearch.test.rest.spec.RestSpec; import javax.net.ssl.SSLContext; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * REST client used to test the elasticsearch REST layer * Holds the {@link RestSpec} used to translate api calls into REST calls */ public class RestClient implements Closeable { public static final String PROTOCOL = "protocol"; public static final String TRUSTSTORE_PATH = "truststore.path"; public static final String TRUSTSTORE_PASSWORD = "truststore.password"; private static final ESLogger logger = Loggers.getLogger(RestClient.class); //query_string params that don't need to be declared in the spec, thay are supported by default private static final Set<String> ALWAYS_ACCEPTED_QUERY_STRING_PARAMS = Sets.newHashSet("pretty", "source", "filter_path"); private final String protocol; private final RestSpec restSpec; private final CloseableHttpClient httpClient; private final Headers headers; private final InetSocketAddress[] addresses; private final Version esVersion; public RestClient(RestSpec restSpec, Settings settings, InetSocketAddress[] addresses) throws IOException, RestException { assert addresses.length > 0; this.restSpec = restSpec; this.headers = new Headers(settings); this.protocol = settings.get(PROTOCOL, "http"); this.httpClient = createHttpClient(settings); this.addresses = addresses; this.esVersion = readAndCheckVersion(); logger.info("REST client initialized {}, elasticsearch version: [{}]", addresses, esVersion); } private Version readAndCheckVersion() throws IOException, RestException { //we make a manual call here without using callApi method, mainly because we are initializing //and the randomized context doesn't exist for the current thread (would be used to choose the method otherwise) RestApi restApi = restApi("info"); assert restApi.getPaths().size() == 1; assert restApi.getMethods().size() == 1; String version = null; for (InetSocketAddress address : addresses) { RestResponse restResponse = new RestResponse(httpRequestBuilder(address) .path(restApi.getPaths().get(0)) .method(restApi.getMethods().get(0)).execute()); checkStatusCode(restResponse); Object latestVersion = restResponse.evaluate("version.number"); if (latestVersion == null) { throw new RuntimeException("elasticsearch version not found in the response"); } if (version == null) { version = latestVersion.toString(); } else { if (!latestVersion.equals(version)) { throw new IllegalArgumentException("provided nodes addresses run different elasticsearch versions"); } } } return Version.fromString(version); } public Version getEsVersion() { return esVersion; } /** * Calls an api with the provided parameters and body * @throws RestException if the obtained status code is non ok, unless the specific error code needs to be ignored * according to the ignore parameter received as input (which won't get sent to elasticsearch) */ public RestResponse callApi(String apiName, Map<String, String> params, String body, Map<String, String> headers) throws IOException, RestException { List<Integer> ignores = new ArrayList<>(); Map<String, String> requestParams = null; if (params != null) { //makes a copy of the parameters before modifying them for this specific request requestParams = Maps.newHashMap(params); //ignore is a special parameter supported by the clients, shouldn't be sent to es String ignoreString = requestParams.remove("ignore"); if (Strings.hasLength(ignoreString)) { try { ignores.add(Integer.valueOf(ignoreString)); } catch(NumberFormatException e) { throw new IllegalArgumentException("ignore value should be a number, found [" + ignoreString + "] instead"); } } } HttpRequestBuilder httpRequestBuilder = callApiBuilder(apiName, requestParams, body); for (Map.Entry<String, String> header : headers.entrySet()) { httpRequestBuilder.addHeader(header.getKey(), header.getValue()); } logger.debug("calling api [{}]", apiName); HttpResponse httpResponse = httpRequestBuilder.execute(); // http HEAD doesn't support response body // For the few api (exists class of api) that use it we need to accept 404 too if (!httpResponse.supportsBody()) { ignores.add(404); } RestResponse restResponse = new RestResponse(httpResponse); checkStatusCode(restResponse, ignores); return restResponse; } private void checkStatusCode(RestResponse restResponse, List<Integer> ignores) throws RestException { //ignore is a catch within the client, to prevent the client from throwing error if it gets non ok codes back if (ignores.contains(restResponse.getStatusCode())) { if (logger.isDebugEnabled()) { logger.debug("ignored non ok status codes {} as requested", ignores); } return; } checkStatusCode(restResponse); } private void checkStatusCode(RestResponse restResponse) throws RestException { if (restResponse.isError()) { throw new RestException("non ok status code [" + restResponse.getStatusCode() + "] returned", restResponse); } } private HttpRequestBuilder callApiBuilder(String apiName, Map<String, String> params, String body) { //create doesn't exist in the spec but is supported in the clients (index with op_type=create) boolean indexCreateApi = "create".equals(apiName); String api = indexCreateApi ? "index" : apiName; RestApi restApi = restApi(api); HttpRequestBuilder httpRequestBuilder = httpRequestBuilder(); //divide params between ones that go within query string and ones that go within path Map<String, String> pathParts = Maps.newHashMap(); if (params != null) { for (Map.Entry<String, String> entry : params.entrySet()) { if (restApi.getPathParts().contains(entry.getKey())) { pathParts.put(entry.getKey(), entry.getValue()); } else { if (restApi.getParams().contains(entry.getKey()) || ALWAYS_ACCEPTED_QUERY_STRING_PARAMS.contains(entry.getKey())) { httpRequestBuilder.addParam(entry.getKey(), entry.getValue()); } else { throw new IllegalArgumentException("param [" + entry.getKey() + "] not supported in [" + restApi.getName() + "] api"); } } } } if (indexCreateApi) { httpRequestBuilder.addParam("op_type", "create"); } List<String> supportedMethods = restApi.getSupportedMethods(pathParts.keySet()); if (Strings.hasLength(body)) { if (!restApi.isBodySupported()) { throw new IllegalArgumentException("body is not supported by [" + restApi.getName() + "] api"); } //test the GET with source param instead of GET/POST with body if (supportedMethods.contains("GET") && RandomizedTest.rarely()) { logger.debug("sending the request body as source param with GET method"); httpRequestBuilder.addParam("source", body).method("GET"); } else { httpRequestBuilder.body(body).method(RandomizedTest.randomFrom(supportedMethods)); } } else { if (restApi.isBodyRequired()) { throw new IllegalArgumentException("body is required by [" + restApi.getName() + "] api"); } httpRequestBuilder.method(RandomizedTest.randomFrom(supportedMethods)); } //the rest path to use is randomized out of the matching ones (if more than one) RestPath restPath = RandomizedTest.randomFrom(restApi.getFinalPaths(pathParts)); return httpRequestBuilder.pathParts(restPath.getPathParts()); } private RestApi restApi(String apiName) { RestApi restApi = restSpec.getApi(apiName); if (restApi == null) { throw new IllegalArgumentException("rest api [" + apiName + "] doesn't exist in the rest spec"); } return restApi; } protected HttpRequestBuilder httpRequestBuilder(InetSocketAddress address) { return new HttpRequestBuilder(httpClient) .addHeaders(headers) .protocol(protocol) .host(NetworkAddress.format(address.getAddress())).port(address.getPort()); } protected HttpRequestBuilder httpRequestBuilder() { //the address used is randomized between the available ones InetSocketAddress address = RandomizedTest.randomFrom(addresses); return httpRequestBuilder(address); } protected CloseableHttpClient createHttpClient(Settings settings) throws IOException { SSLConnectionSocketFactory sslsf; String keystorePath = settings.get(TRUSTSTORE_PATH); if (keystorePath != null) { final String keystorePass = settings.get(TRUSTSTORE_PASSWORD); if (keystorePass == null) { throw new IllegalStateException(TRUSTSTORE_PATH + " is provided but not " + TRUSTSTORE_PASSWORD); } Path path = PathUtils.get(keystorePath); if (!Files.exists(path)) { throw new IllegalStateException(TRUSTSTORE_PATH + " is set but points to a non-existing file"); } try { KeyStore keyStore = KeyStore.getInstance("jks"); try (InputStream is = Files.newInputStream(path)) { keyStore.load(is, keystorePass.toCharArray()); } SSLContext sslcontext = SSLContexts.custom() .loadTrustMaterial(keyStore, null) .build(); sslsf = new SSLConnectionSocketFactory(sslcontext); } catch (KeyStoreException|NoSuchAlgorithmException|KeyManagementException|CertificateException e) { throw new RuntimeException(e); } } else { sslsf = SSLConnectionSocketFactory.getSocketFactory(); } Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslsf) .build(); return HttpClients.createMinimal(new PoolingHttpClientConnectionManager(socketFactoryRegistry, null, null, null, 15, TimeUnit.SECONDS)); } /** * Closes the REST client and the underlying http client */ @Override public void close() { IOUtils.closeWhileHandlingException(httpClient); } }