refactor RequestHelper use Java 11 instead of Apache HTTPClient

This commit is contained in:
Stefan Kalscheuer 2021-01-24 11:54:00 +01:00
parent c45dbf014e
commit 8dfcf02a0a
3 changed files with 99 additions and 110 deletions

View File

@ -105,11 +105,6 @@
</build> </build>
<dependencies> <dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>

View File

@ -29,6 +29,7 @@ final class Error {
static final String URI_FORMAT = "Invalid URI format"; static final String URI_FORMAT = "Invalid URI format";
static final String RESPONSE_CODE = "Invalid response code"; static final String RESPONSE_CODE = "Invalid response code";
static final String INIT_SSL_CONTEXT = "Unable to initialize SSLContext"; static final String INIT_SSL_CONTEXT = "Unable to initialize SSLContext";
static final String CONNECTION = "Unable to connect to Vault server";
/** /**
* Constructor hidden, this class should not be instantiated. * Constructor hidden, this class should not be instantiated.

View File

@ -4,27 +4,29 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.stklcode.jvault.connector.exception.*; import de.stklcode.jvault.connector.exception.*;
import de.stklcode.jvault.connector.model.response.ErrorResponse; import de.stklcode.jvault.connector.model.response.ErrorResponse;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import java.io.*; import java.io.*;
import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets; import java.net.URLEncoder;
import java.security.*; import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
/** /**
* Helper class to bundle Vault HTTP requests. * Helper class to bundle Vault HTTP requests.
* *
@ -74,26 +76,24 @@ public final class RequestHelper implements Serializable {
* @since 0.8 Added {@code token} parameter. * @since 0.8 Added {@code token} parameter.
*/ */
public String post(final String path, final Object payload, final String token) throws VaultConnectorException { public String post(final String path, final Object payload, final String token) throws VaultConnectorException {
/* Initialize post */ // Initialize POST.
HttpPost post = new HttpPost(baseURL + path); HttpRequest.Builder req = HttpRequest.newBuilder(URI.create(baseURL + path));
/* generate JSON from payload */ // Generate JSON from payload.
StringEntity input;
try { try {
input = new StringEntity(jsonMapper.writeValueAsString(payload), StandardCharsets.UTF_8); req.POST(HttpRequest.BodyPublishers.ofString(jsonMapper.writeValueAsString(payload), UTF_8));
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new InvalidRequestException(Error.PARSE_RESPONSE, e); throw new InvalidRequestException(Error.PARSE_RESPONSE, e);
} }
input.setContentEncoding("UTF-8");
input.setContentType("application/json");
post.setEntity(input);
/* Set X-Vault-Token header */ req.setHeader("Content-Type", "application/json; charset=utf-8");
// Set X-Vault-Token header.
if (token != null) { if (token != null) {
post.addHeader(HEADER_VAULT_TOKEN, token); req.setHeader(HEADER_VAULT_TOKEN, token);
} }
return request(post, retries); return request(req, retries);
} }
/** /**
@ -144,26 +144,24 @@ public final class RequestHelper implements Serializable {
* @since 0.8 Added {@code token} parameter. * @since 0.8 Added {@code token} parameter.
*/ */
public String put(final String path, final Map<String, String> payload, final String token) throws VaultConnectorException { public String put(final String path, final Map<String, String> payload, final String token) throws VaultConnectorException {
/* Initialize put */ // Initialize PUT.
HttpPut put = new HttpPut(baseURL + path); HttpRequest.Builder req = HttpRequest.newBuilder(URI.create(baseURL + path));
/* generate JSON from payload */ // Generate JSON from payload.
StringEntity entity = null;
try { try {
entity = new StringEntity(jsonMapper.writeValueAsString(payload)); req.PUT(HttpRequest.BodyPublishers.ofString(jsonMapper.writeValueAsString(payload), UTF_8));
} catch (UnsupportedEncodingException | JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new InvalidRequestException("Payload serialization failed", e); throw new InvalidRequestException("Payload serialization failed", e);
} }
/* Parse parameters */ req.setHeader("Content-Type", "application/json; charset=utf-8");
put.setEntity(entity);
/* Set X-Vault-Token header */ // Set X-Vault-Token header.
if (token != null) { if (token != null) {
put.addHeader(HEADER_VAULT_TOKEN, token); req.setHeader(HEADER_VAULT_TOKEN, token);
} }
return request(put, retries); return request(req, retries);
} }
/** /**
@ -214,15 +212,15 @@ public final class RequestHelper implements Serializable {
* @since 0.8 Added {@code token} parameter. * @since 0.8 Added {@code token} parameter.
*/ */
public String delete(final String path, final String token) throws VaultConnectorException { public String delete(final String path, final String token) throws VaultConnectorException {
/* Initialize delete */ // Initialize DELETE.
HttpDelete delete = new HttpDelete(baseURL + path); HttpRequest.Builder req = HttpRequest.newBuilder(URI.create(baseURL + path)).DELETE();
/* Set X-Vault-Token header */ // Set X-Vault-Token header.
if (token != null) { if (token != null) {
delete.addHeader(HEADER_VAULT_TOKEN, token); req.setHeader(HEADER_VAULT_TOKEN, token);
} }
return request(delete, retries); return request(req, retries);
} }
/** /**
@ -251,25 +249,31 @@ public final class RequestHelper implements Serializable {
*/ */
public String get(final String path, final Map<String, String> payload, final String token) public String get(final String path, final Map<String, String> payload, final String token)
throws VaultConnectorException { throws VaultConnectorException {
HttpGet get; // Add parameters to URI.
try { StringBuilder uriBuilder = new StringBuilder(baseURL + path);
/* Add parameters to URI */
URIBuilder uriBuilder = new URIBuilder(baseURL + path);
payload.forEach(uriBuilder::addParameter);
/* Initialize request */ if (!payload.isEmpty()) {
get = new HttpGet(uriBuilder.build()); uriBuilder.append("?").append(
payload.entrySet().stream().map(
par -> URLEncoder.encode(par.getKey(), UTF_8) + "=" + URLEncoder.encode(par.getValue(), UTF_8)
).collect(Collectors.joining("&"))
);
}
// Initialize GET.
try {
HttpRequest.Builder req = HttpRequest.newBuilder(new URI(uriBuilder.toString()));
// Set X-Vault-Token header.
if (token != null) {
req.setHeader(HEADER_VAULT_TOKEN, token);
}
return request(req, retries);
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
/* this should never occur and may leak sensible information */ /* this should never occur and may leak sensible information */
throw new InvalidRequestException(Error.URI_FORMAT); throw new InvalidRequestException(Error.URI_FORMAT);
} }
/* Set X-Vault-Token header */
if (token != null) {
get.addHeader(HEADER_VAULT_TOKEN, token);
}
return request(get, retries);
} }
/** /**
@ -297,34 +301,40 @@ public final class RequestHelper implements Serializable {
/** /**
* Execute prepared HTTP request and return result. * Execute prepared HTTP request and return result.
* *
* @param base Prepares Request * @param requestBuilder Prepared request.
* @param retries number of retries * @param retries Number of retries.
* @return HTTP response * @return HTTP response
* @throws VaultConnectorException on connection error * @throws VaultConnectorException on connection error
*/ */
private String request(final HttpRequestBase base, final int retries) throws VaultConnectorException { private String request(final HttpRequest.Builder requestBuilder, final int retries) throws VaultConnectorException {
/* Set JSON Header */ // Set JSON Header.
base.addHeader("accept", "application/json"); requestBuilder.setHeader("accept", "application/json");
CloseableHttpResponse response = null; HttpClient.Builder clientBuilder = HttpClient.newBuilder();
try (CloseableHttpClient httpClient = HttpClientBuilder.create() // Set custom timeout, if defined.
.setSSLSocketFactory(createSSLSocketFactory())
.build()) {
/* Set custom timeout, if defined */
if (this.timeout != null) { if (this.timeout != null) {
base.setConfig(RequestConfig.copy(RequestConfig.DEFAULT).setConnectTimeout(timeout).build()); clientBuilder.connectTimeout(Duration.ofMillis(timeout));
} }
/* Execute request */ // Set custom SSL context.
response = httpClient.execute(base); clientBuilder.sslContext(createSSLContext());
HttpClient client = clientBuilder.build();
// Execute request.
try {
HttpResponse<InputStream> response = client.sendAsync(
requestBuilder.build(),
HttpResponse.BodyHandlers.ofInputStream()
).join();
/* Check if response is valid */ /* Check if response is valid */
if (response == null) { if (response == null) {
throw new InvalidResponseException("Response unavailable"); throw new InvalidResponseException("Response unavailable");
} }
switch (response.getStatusLine().getStatusCode()) { switch (response.statusCode()) {
case 200: case 200:
return handleResult(response); return handleResult(response);
case 204: case 204:
@ -332,42 +342,33 @@ public final class RequestHelper implements Serializable {
case 403: case 403:
throw new PermissionDeniedException(); throw new PermissionDeniedException();
default: default:
if (response.getStatusLine().getStatusCode() >= 500 if (response.statusCode() >= 500 && response.statusCode() < 600 && retries > 0) {
&& response.getStatusLine().getStatusCode() < 600 && retries > 0) { // Retry on 5xx errors.
/* Retry on 5xx errors */ return request(requestBuilder, retries - 1);
return request(base, retries - 1);
} else { } else {
/* Fail on different error code and/or no retries left */ // Fail on different error code and/or no retries left.
handleError(response); handleError(response);
/* Throw exception without details, if response entity is empty. */ // Throw exception without details, if response entity is empty.
throw new InvalidResponseException(Error.RESPONSE_CODE, throw new InvalidResponseException(Error.RESPONSE_CODE, response.statusCode());
response.getStatusLine().getStatusCode());
}
}
} catch (IOException e) {
throw new InvalidResponseException(Error.READ_RESPONSE, e);
} finally {
if (response != null && response.getEntity() != null) {
try {
EntityUtils.consume(response.getEntity());
} catch (IOException ignored) {
// Exception ignored.
} }
} }
} catch (CompletionException e) {
throw new ConnectionException(Error.CONNECTION, e.getCause());
} }
} }
/** /**
* Create a custom socket factory from trusted CA certificate. * Create a custom SSL context from trusted CA certificate.
* *
* @return The factory. * @return The context.
* @throws TlsException An error occurred during initialization of the SSL context. * @throws TlsException An error occurred during initialization of the SSL context.
* @since 0.8.0 * @since 0.8.0
* @since 0.10 Generate {@link SSLContext} instead of Apache {@code SSLConnectionSocketFactory}
*/ */
private SSLConnectionSocketFactory createSSLSocketFactory() throws TlsException { private SSLContext createSSLContext() throws TlsException {
try { try {
// Create context.. // Create context.
SSLContext context = SSLContext.getInstance(tlsVersion); SSLContext context = SSLContext.getInstance(tlsVersion);
if (trustedCaCert != null) { if (trustedCaCert != null) {
@ -384,12 +385,7 @@ public final class RequestHelper implements Serializable {
context.init(null, null, null); context.init(null, null, null);
} }
return new SSLConnectionSocketFactory( return context;
context,
null,
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier()
);
} catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException | KeyManagementException e) { } catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException | KeyManagementException e) {
throw new TlsException(Error.INIT_SSL_CONTEXT, e); throw new TlsException(Error.INIT_SSL_CONTEXT, e);
} }
@ -402,9 +398,8 @@ public final class RequestHelper implements Serializable {
* @return Complete response body as String * @return Complete response body as String
* @throws InvalidResponseException on reading errors * @throws InvalidResponseException on reading errors
*/ */
private String handleResult(final HttpResponse response) throws InvalidResponseException { private String handleResult(final HttpResponse<InputStream> response) throws InvalidResponseException {
try (BufferedReader br = new BufferedReader( try (BufferedReader br = new BufferedReader(new InputStreamReader(response.body()))) {
new InputStreamReader(response.getEntity().getContent()))) {
return br.lines().collect(Collectors.joining("\n")); return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ignored) { } catch (IOException ignored) {
throw new InvalidResponseException(Error.READ_RESPONSE, 200); throw new InvalidResponseException(Error.READ_RESPONSE, 200);
@ -417,18 +412,16 @@ public final class RequestHelper implements Serializable {
* @param response The raw HTTP response (assuming status code 5xx) * @param response The raw HTTP response (assuming status code 5xx)
* @throws VaultConnectorException Expected exception with details to throw * @throws VaultConnectorException Expected exception with details to throw
*/ */
private void handleError(final HttpResponse response) throws VaultConnectorException { private void handleError(final HttpResponse<InputStream> response) throws VaultConnectorException {
if (response.getEntity() != null) { if (response.body() != null) {
try (BufferedReader br = new BufferedReader( try (BufferedReader br = new BufferedReader(new InputStreamReader(response.body()))) {
new InputStreamReader(response.getEntity().getContent()))) {
String responseString = br.lines().collect(Collectors.joining("\n")); String responseString = br.lines().collect(Collectors.joining("\n"));
ErrorResponse er = jsonMapper.readValue(responseString, ErrorResponse.class); ErrorResponse er = jsonMapper.readValue(responseString, ErrorResponse.class);
/* Check for "permission denied" response */ /* Check for "permission denied" response */
if (!er.getErrors().isEmpty() && er.getErrors().get(0).equals("permission denied")) { if (!er.getErrors().isEmpty() && er.getErrors().get(0).equals("permission denied")) {
throw new PermissionDeniedException(); throw new PermissionDeniedException();
} }
throw new InvalidResponseException(Error.RESPONSE_CODE, throw new InvalidResponseException(Error.RESPONSE_CODE, response.statusCode(), er.toString());
response.getStatusLine().getStatusCode(), er.toString());
} catch (IOException ignored) { } catch (IOException ignored) {
// Exception ignored. // Exception ignored.
} }