Java tutorial
/* * Copyright 2016-2018 Stefan Kalscheuer * * Licensed 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 de.stklcode.jvault.connector; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import de.stklcode.jvault.connector.exception.*; import de.stklcode.jvault.connector.model.AppRole; import de.stklcode.jvault.connector.model.AppRoleSecret; import de.stklcode.jvault.connector.model.AuthBackend; import de.stklcode.jvault.connector.model.Token; import de.stklcode.jvault.connector.model.response.*; import de.stklcode.jvault.connector.model.response.embedded.AuthMethod; 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.TrustManagerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * Vault Connector implementatin using Vault's HTTP API. * * @author Stefan Kalscheuer * @since 0.1 */ public class HTTPVaultConnector implements VaultConnector { private static final String PATH_PREFIX = "/v1/"; private static final String PATH_SEAL_STATUS = "sys/seal-status"; private static final String PATH_SEAL = "sys/seal"; private static final String PATH_UNSEAL = "sys/unseal"; private static final String PATH_RENEW = "sys/leases/renew"; private static final String PATH_AUTH = "sys/auth"; private static final String PATH_TOKEN = "auth/token"; private static final String PATH_LOOKUP = "/lookup"; private static final String PATH_CREATE = "/create"; private static final String PATH_CREATE_ORPHAN = "/create-orphan"; private static final String PATH_AUTH_USERPASS = "auth/userpass/login/"; private static final String PATH_AUTH_APPID = "auth/app-id/"; private static final String PATH_AUTH_APPROLE = "auth/approle/"; private static final String PATH_AUTH_APPROLE_ROLE = "auth/approle/role/%s%s"; private static final String PATH_REVOKE = "sys/leases/revoke/"; private static final String PATH_HEALTH = "sys/health"; private static final String HEADER_VAULT_TOKEN = "X-Vault-Token"; public static final String DEFAULT_TLS_VERSION = "TLSv1.2"; private final ObjectMapper jsonMapper; private final String baseURL; // Base URL of Vault. private final String tlsVersion; // TLS version (#22). private final X509Certificate trustedCaCert; // Trusted CA certificate. private final int retries; // Number of retries on 5xx errors. private final Integer timeout; // Timeout in milliseconds. private boolean authorized = false; // Authorization status. private String token; // Current token. private long tokenTTL = 0; // Expiration time for current token. /** * Create connector using hostname and schema. * * @param hostname The hostname * @param useTLS If TRUE, use HTTPS, otherwise HTTP */ public HTTPVaultConnector(final String hostname, final boolean useTLS) { this(hostname, useTLS, null); } /** * Create connector using hostname, schema and port. * * @param hostname The hostname * @param useTLS If TRUE, use HTTPS, otherwise HTTP * @param port The port */ public HTTPVaultConnector(final String hostname, final boolean useTLS, final Integer port) { this(hostname, useTLS, port, PATH_PREFIX); } /** * Create connector using hostname, schema, port and path. * * @param hostname The hostname * @param useTLS If TRUE, use HTTPS, otherwise HTTP * @param port The port * @param prefix HTTP API prefix (default: /v1/) */ public HTTPVaultConnector(final String hostname, final boolean useTLS, final Integer port, final String prefix) { this(((useTLS) ? "https" : "http") + "://" + hostname + ((port != null) ? ":" + port : "") + prefix); } /** * Create connector using hostname, schema, port, path and trusted certificate. * * @param hostname The hostname * @param useTLS If TRUE, use HTTPS, otherwise HTTP * @param port The port * @param prefix HTTP API prefix (default: /v1/) * @param trustedCaCert Trusted CA certificate */ public HTTPVaultConnector(final String hostname, final boolean useTLS, final Integer port, final String prefix, final X509Certificate trustedCaCert) { this(hostname, useTLS, DEFAULT_TLS_VERSION, port, prefix, trustedCaCert, 0, null); } /** * Create connector using hostname, schema, port, path and trusted certificate. * * @param hostname The hostname * @param useTLS If TRUE, use HTTPS, otherwise HTTP * @param tlsVersion TLS version * @param port The port * @param prefix HTTP API prefix (default: /v1/) * @param trustedCaCert Trusted CA certificate * @param numberOfRetries Number of retries on 5xx errors * @param timeout Timeout for HTTP requests (milliseconds) */ public HTTPVaultConnector(final String hostname, final boolean useTLS, final String tlsVersion, final Integer port, final String prefix, final X509Certificate trustedCaCert, final int numberOfRetries, final Integer timeout) { this(((useTLS) ? "https" : "http") + "://" + hostname + ((port != null) ? ":" + port : "") + prefix, trustedCaCert, numberOfRetries, timeout, tlsVersion); } /** * Create connector using full URL. * * @param baseURL The URL */ public HTTPVaultConnector(final String baseURL) { this(baseURL, null); } /** * Create connector using full URL and trusted certificate. * * @param baseURL The URL * @param trustedCaCert Trusted CA certificate */ public HTTPVaultConnector(final String baseURL, final X509Certificate trustedCaCert) { this(baseURL, trustedCaCert, 0, null); } /** * Create connector using full URL and trusted certificate. * * @param baseURL The URL * @param trustedCaCert Trusted CA certificate * @param numberOfRetries Number of retries on 5xx errors */ public HTTPVaultConnector(final String baseURL, final X509Certificate trustedCaCert, final int numberOfRetries) { this(baseURL, trustedCaCert, numberOfRetries, null); } /** * Create connector using full URL and trusted certificate. * * @param baseURL The URL * @param trustedCaCert Trusted CA certificate * @param numberOfRetries Number of retries on 5xx errors * @param timeout Timeout for HTTP requests (milliseconds) */ public HTTPVaultConnector(final String baseURL, final X509Certificate trustedCaCert, final int numberOfRetries, final Integer timeout) { this(baseURL, trustedCaCert, numberOfRetries, timeout, DEFAULT_TLS_VERSION); } /** * Create connector using full URL and trusted certificate. * * @param baseURL The URL * @param trustedCaCert Trusted CA certificate * @param numberOfRetries Number of retries on 5xx errors * @param timeout Timeout for HTTP requests (milliseconds) * @param tlsVersion TLS Version. */ public HTTPVaultConnector(final String baseURL, final X509Certificate trustedCaCert, final int numberOfRetries, final Integer timeout, final String tlsVersion) { this.baseURL = baseURL; this.trustedCaCert = trustedCaCert; this.retries = numberOfRetries; this.timeout = timeout; this.tlsVersion = tlsVersion; this.jsonMapper = new ObjectMapper(); } @Override public final void resetAuth() { token = null; tokenTTL = 0; authorized = false; } @Override public final SealResponse sealStatus() throws VaultConnectorException { try { String response = requestGet(PATH_SEAL_STATUS, new HashMap<>()); return jsonMapper.readValue(response, SealResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final void seal() throws VaultConnectorException { requestPut(PATH_SEAL, new HashMap<>()); } @Override public final SealResponse unseal(final String key, final Boolean reset) throws VaultConnectorException { Map<String, String> param = new HashMap<>(); param.put("key", key); if (reset != null) param.put("reset", reset.toString()); try { String response = requestPut(PATH_UNSEAL, param); return jsonMapper.readValue(response, SealResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } } @Override public HealthResponse getHealth() throws VaultConnectorException { /* Force status code to be 200, so we don't need to modify the request sequence. */ Map<String, String> param = new HashMap<>(); param.put("standbycode", "200"); // Default: 429. param.put("sealedcode", "200"); // Default: 503. param.put("uninitcode", "200"); // Default: 501. try { String response = requestGet(PATH_HEALTH, param); /* Parse response */ return jsonMapper.readValue(response, HealthResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException e) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final boolean isAuthorized() { return authorized && (tokenTTL == 0 || tokenTTL >= System.currentTimeMillis()); } @Override public final List<AuthBackend> getAuthBackends() throws VaultConnectorException { try { String response = requestGet(PATH_AUTH, new HashMap<>()); /* Parse response */ AuthMethodsResponse amr = jsonMapper.readValue(response, AuthMethodsResponse.class); return amr.getSupportedMethods().values().stream().map(AuthMethod::getType) .collect(Collectors.toList()); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final TokenResponse authToken(final String token) throws VaultConnectorException { /* set token */ this.token = token; this.tokenTTL = 0; try { String response = requestPost(PATH_TOKEN + PATH_LOOKUP, new HashMap<>()); TokenResponse res = jsonMapper.readValue(response, TokenResponse.class); authorized = true; return res; } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } } @Override public final AuthResponse authUserPass(final String username, final String password) throws VaultConnectorException { final Map<String, String> payload = new HashMap<>(); payload.put("password", password); return queryAuth(PATH_AUTH_USERPASS + username, payload); } @Override @Deprecated public final AuthResponse authAppId(final String appID, final String userID) throws VaultConnectorException { final Map<String, String> payload = new HashMap<>(); payload.put("app_id", appID); payload.put("user_id", userID); return queryAuth(PATH_AUTH_APPID + "login", payload); } @Override public final AuthResponse authAppRole(final String roleID, final String secretID) throws VaultConnectorException { final Map<String, String> payload = new HashMap<>(); payload.put("role_id", roleID); if (secretID != null) payload.put("secret_id", secretID); return queryAuth(PATH_AUTH_APPROLE + "login", payload); } /** * Query authorization request to given backend. * * @param path The path to request * @param payload Payload (credentials) * @return The AuthResponse * @throws VaultConnectorException on errors */ private AuthResponse queryAuth(final String path, final Map<String, String> payload) throws VaultConnectorException { try { /* Get response */ String response = requestPost(path, payload); /* Parse response */ AuthResponse auth = jsonMapper.readValue(response, AuthResponse.class); /* verify response */ this.token = auth.getAuth().getClientToken(); this.tokenTTL = System.currentTimeMillis() + auth.getAuth().getLeaseDuration() * 1000L; this.authorized = true; return auth; } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } } @Override @Deprecated public final boolean registerAppId(final String appID, final String policy, final String displayName) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); Map<String, String> payload = new HashMap<>(); payload.put("value", policy); payload.put("display_name", displayName); /* Get response */ String response = requestPost(PATH_AUTH_APPID + "map/app-id/" + appID, payload); /* Response should be code 204 without content */ if (!response.isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); return true; } @Override @Deprecated public final boolean registerUserId(final String appID, final String userID) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); Map<String, String> payload = new HashMap<>(); payload.put("value", appID); /* Get response */ String response = requestPost(PATH_AUTH_APPID + "map/user-id/" + userID, payload); /* Response should be code 204 without content */ if (!response.isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); return true; } @Override public final boolean createAppRole(final AppRole role) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Get response */ String response = requestPost(String.format(PATH_AUTH_APPROLE_ROLE, role.getName(), ""), role); /* Response should be code 204 without content */ if (!response.isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); /* Set custom ID if provided */ return !(role.getId() != null && !role.getId().isEmpty()) || setAppRoleID(role.getName(), role.getId()); } @Override public final AppRoleResponse lookupAppRole(final String roleName) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and parse Secret */ try { String response = requestGet(String.format(PATH_AUTH_APPROLE_ROLE, roleName, ""), new HashMap<>()); return jsonMapper.readValue(response, AppRoleResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final boolean deleteAppRole(final String roleName) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and expect empty result */ String response = requestDelete(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "")); /* Response should be code 204 without content */ if (!response.isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); return true; } @Override public final String getAppRoleID(final String roleName) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and parse Secret */ try { String response = requestGet(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/role-id"), new HashMap<>()); return jsonMapper.readValue(response, RawDataResponse.class).getData().get("role_id").toString(); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final boolean setAppRoleID(final String roleName, final String roleID) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and parse Secret */ Map<String, String> payload = new HashMap<>(); payload.put("role_id", roleID); String response = requestPost(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/role-id"), payload); /* Response should be code 204 without content */ if (!response.isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); return true; } @Override public final AppRoleSecretResponse createAppRoleSecret(final String roleName, final AppRoleSecret secret) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Get response */ String response; if (secret.getId() != null && !secret.getId().isEmpty()) response = requestPost(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/custom-secret-id"), secret); else response = requestPost(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/secret-id"), secret); try { /* Extract the secret ID from response */ return jsonMapper.readValue(response, AppRoleSecretResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE); } } @Override public final AppRoleSecretResponse lookupAppRoleSecret(final String roleName, final String secretID) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and parse Secret */ try { String response = requestPost(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/secret-id/lookup"), new AppRoleSecret(secretID)); return jsonMapper.readValue(response, AppRoleSecretResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } } @Override public final boolean destroyAppRoleSecret(final String roleName, final String secretID) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and expect empty result */ String response = requestPost(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/secret-id/destroy"), new AppRoleSecret(secretID)); /* Response should be code 204 without content */ if (!response.isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); return true; } @Override public final List<String> listAppRoles() throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); try { String response = requestGet(PATH_AUTH_APPROLE + "role?list=true", new HashMap<>()); SecretListResponse secrets = jsonMapper.readValue(response, SecretListResponse.class); return secrets.getKeys(); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final List<String> listAppRoleSecrets(final String roleName) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); try { String response = requestGet(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/secret-id?list=true"), new HashMap<>()); SecretListResponse secrets = jsonMapper.readValue(response, SecretListResponse.class); return secrets.getKeys(); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final SecretResponse read(final String key) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and parse Secret */ try { String response = requestGet(key, new HashMap<>()); return jsonMapper.readValue(response, SecretResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final List<String> list(final String path) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); try { String response = requestGet(path + "/?list=true", new HashMap<>()); SecretListResponse secrets = jsonMapper.readValue(response, SecretListResponse.class); return secrets.getKeys(); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } @Override public final void write(final String key, final Map<String, Object> data) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); if (key == null || key.isEmpty()) throw new InvalidRequestException("Secret path must not be empty."); if (!requestPost(key, data).isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); } @Override public final void delete(final String key) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and expect empty result */ String response = requestDelete(key); /* Response should be code 204 without content */ if (!response.isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); } @Override public final void revoke(final String leaseID) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and expect empty result */ String response = requestPut(PATH_REVOKE + leaseID, new HashMap<>()); /* Response should be code 204 without content */ if (!response.isEmpty()) throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE); } @Override public final SecretResponse renew(final String leaseID, final Integer increment) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); Map<String, String> payload = new HashMap<>(); payload.put("lease_id", leaseID); if (increment != null) payload.put("increment", increment.toString()); /* Request HTTP response and parse Secret */ try { String response = requestPut(PATH_RENEW, payload); return jsonMapper.readValue(response, SecretResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } } @Override public final AuthResponse createToken(final Token token) throws VaultConnectorException { return createTokenInternal(token, PATH_TOKEN + PATH_CREATE); } @Override public final AuthResponse createToken(final Token token, final boolean orphan) throws VaultConnectorException { return createTokenInternal(token, PATH_TOKEN + PATH_CREATE_ORPHAN); } @Override public final AuthResponse createToken(final Token token, final String role) throws VaultConnectorException { if (role == null || role.isEmpty()) throw new InvalidRequestException("No role name specified."); return createTokenInternal(token, PATH_TOKEN + PATH_CREATE + "/" + role); } @Override public final void close() { authorized = false; token = null; tokenTTL = 0; } /** * Create token. * Centralized method to handle different token creation requests. * * @param token the token * @param path request path * @return the response * @throws VaultConnectorException on error */ private AuthResponse createTokenInternal(final Token token, final String path) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); if (token == null) throw new InvalidRequestException("Token must be provided."); String response = requestPost(path, token); try { return jsonMapper.readValue(response, AuthResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } } @Override public final TokenResponse lookupToken(final String token) throws VaultConnectorException { if (!isAuthorized()) throw new AuthorizationRequiredException(); /* Request HTTP response and parse Secret */ try { String response = requestGet(PATH_TOKEN + "/lookup/" + token, new HashMap<>()); return jsonMapper.readValue(response, TokenResponse.class); } catch (IOException e) { throw new InvalidResponseException(Error.PARSE_RESPONSE, e); } catch (URISyntaxException ignored) { /* this should never occur and may leak sensible information */ throw new InvalidRequestException(Error.URI_FORMAT); } } /** * Execute HTTP request using POST method. * * @param path URL path (relative to base) * @param payload Map of payload values (will be converted to JSON) * @return HTTP response * @throws VaultConnectorException on connection error */ private String requestPost(final String path, final Object payload) throws VaultConnectorException { /* Initialize post */ HttpPost post = new HttpPost(baseURL + path); /* generate JSON from payload */ StringEntity input; try { input = new StringEntity(jsonMapper.writeValueAsString(payload), StandardCharsets.UTF_8); } catch (JsonProcessingException e) { throw new InvalidRequestException(Error.PARSE_RESPONSE, e); } input.setContentEncoding("UTF-8"); input.setContentType("application/json"); post.setEntity(input); /* Set X-Vault-Token header */ if (token != null) post.addHeader(HEADER_VAULT_TOKEN, token); return request(post, retries); } /** * Execute HTTP request using PUT method. * * @param path URL path (relative to base) * @param payload Map of payload values (will be converted to JSON) * @return HTTP response * @throws VaultConnectorException on connection error */ private String requestPut(final String path, final Map<String, String> payload) throws VaultConnectorException { /* Initialize put */ HttpPut put = new HttpPut(baseURL + path); /* generate JSON from payload */ StringEntity entity = null; try { entity = new StringEntity(jsonMapper.writeValueAsString(payload)); } catch (UnsupportedEncodingException | JsonProcessingException e) { throw new InvalidRequestException("Payload serialization failed", e); } /* Parse parameters */ put.setEntity(entity); /* Set X-Vault-Token header */ if (token != null) put.addHeader(HEADER_VAULT_TOKEN, token); return request(put, retries); } /** * Execute HTTP request using DELETE method. * * @param path URL path (relative to base) * @return HTTP response * @throws VaultConnectorException on connection error */ private String requestDelete(final String path) throws VaultConnectorException { /* Initialize delete */ HttpDelete delete = new HttpDelete(baseURL + path); /* Set X-Vault-Token header */ if (token != null) delete.addHeader(HEADER_VAULT_TOKEN, token); return request(delete, retries); } /** * Execute HTTP request using GET method. * * @param path URL path (relative to base) * @param payload Map of payload values (will be converted to JSON) * @return HTTP response * @throws VaultConnectorException on connection error * @throws URISyntaxException on invalid URI syntax */ private String requestGet(final String path, final Map<String, String> payload) throws VaultConnectorException, URISyntaxException { /* Add parameters to URI */ URIBuilder uriBuilder = new URIBuilder(baseURL + path); payload.forEach(uriBuilder::addParameter); /* Initialize request */ HttpGet get = new HttpGet(uriBuilder.build()); /* Set X-Vault-Token header */ if (token != null) get.addHeader(HEADER_VAULT_TOKEN, token); return request(get, retries); } /** * Execute prepared HTTP request and return result. * * @param base Prepares Request * @param retries number of retries * @return HTTP response * @throws VaultConnectorException on connection error */ private String request(final HttpRequestBase base, final int retries) throws VaultConnectorException { /* Set JSON Header */ base.addHeader("accept", "application/json"); CloseableHttpResponse response = null; try (CloseableHttpClient httpClient = HttpClientBuilder.create() .setSSLSocketFactory(createSSLSocketFactory()).build()) { /* Set custom timeout, if defined */ if (this.timeout != null) base.setConfig(RequestConfig.copy(RequestConfig.DEFAULT).setConnectTimeout(timeout).build()); /* Execute request */ response = httpClient.execute(base); /* Check if response is valid */ if (response == null) throw new InvalidResponseException("Response unavailable"); switch (response.getStatusLine().getStatusCode()) { case 200: return handleResult(response); case 204: return ""; case 403: throw new PermissionDeniedException(); default: if (response.getStatusLine().getStatusCode() >= 500 && response.getStatusLine().getStatusCode() < 600 && retries > 0) { /* Retry on 5xx errors */ return request(base, retries - 1); } else { /* Fail on different error code and/or no retries left */ handleError(response); /* Throw exception withoud details, if response entity is empty. */ throw new InvalidResponseException(Error.RESPONSE_CODE, 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. } } } /** * Handle successful result. * * @param response The raw HTTP response (assuming status code 200) * @return Complete response body as String * @throws InvalidResponseException on reading errors */ private String handleResult(final HttpResponse response) throws InvalidResponseException { try (BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))) { return br.lines().collect(Collectors.joining("\n")); } catch (IOException ignored) { throw new InvalidResponseException(Error.READ_RESPONSE, 200); } } /** * Handle unsuccessful response. Throw detailed exception if possible. * * @param response The raw HTTP response (assuming status code 5xx) * @throws VaultConnectorException Expected exception with details to throw */ private void handleError(final HttpResponse response) throws VaultConnectorException { if (response.getEntity() != null) { try (BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))) { String responseString = br.lines().collect(Collectors.joining("\n")); ErrorResponse er = jsonMapper.readValue(responseString, ErrorResponse.class); /* Check for "permission denied" response */ if (!er.getErrors().isEmpty() && er.getErrors().get(0).equals("permission denied")) throw new PermissionDeniedException(); throw new InvalidResponseException(Error.RESPONSE_CODE, response.getStatusLine().getStatusCode(), er.toString()); } catch (IOException ignored) { // Exception ignored. } } } /** * Create a custom socket factory from trusted CA certificate. * * @return The factory. * @throws TlsException An error occured during initialization of the SSL context. * @since 0.8.0 */ private SSLConnectionSocketFactory createSSLSocketFactory() throws TlsException { try { // Create Keystore with trusted certificate. KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setCertificateEntry("trustedCert", trustedCaCert); // Initialize TrustManager. TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keyStore); // Create context usint this TrustManager. SSLContext context = SSLContext.getInstance(tlsVersion); context.init(null, tmf.getTrustManagers(), new SecureRandom()); return new SSLConnectionSocketFactory(context, null, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier()); } catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException | KeyManagementException e) { throw new TlsException(Error.INIT_SSL_CONTEXT, e); } } /** * Inner class to bundle common error messages. */ private static final class Error { private static final String READ_RESPONSE = "Unable to read response"; private static final String PARSE_RESPONSE = "Unable to parse response"; private static final String UNEXPECTED_RESPONSE = "Received response where none was expected"; private static final String URI_FORMAT = "Invalid URI format"; private static final String RESPONSE_CODE = "Invalid response code"; private static final String INIT_SSL_CONTEXT = "Unable to intialize SSLContext"; /** * Constructor hidden, this class should not be instantiated. */ private Error() { } } }