All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bettercloud.vault.rest.Rest Maven / Gradle / Ivy

There is a newer version: 5.1.0
Show newest version
package com.bettercloud.vault.rest;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * 

A simple client for issuing HTTP requests. Supports the HTTP verbs:

*
    *
  • GET
  • *
  • POST
  • *
  • PUT
  • *
  • DELETE
  • *
* *

Rest uses the Builder pattern to provide a basic DSL for usage. Methods for configuring an HTTP * request (i.e. url(), parameter(), and header()) are designed to be chained * together, while methods corresponding to the HTTP verbs are terminating operations:

* *
*
{@code
 * final RestResponse getResponse = new Rest()
 *                              .url("https://httpbin.org/get")
 *                              .header("header-1", "foo")
 *                              .header("header-2", "bar")
 *                              .parameter("param-1", "up")
 *                              .parameter("param-2", "down")
 *                              .get();
 *
 * final RestResponse postResponse = new Rest()
 *                              .url("https://httpbin.org/post")
 *                              .header("header-1", "foo")
 *                              .header("header-2", "bar")
 *                              .body( jsonString.getBytes("UTF-8") )
 *                              .post();
 * }
*
* *

Header and parameter names and values are url-encoded by the Rest client prior to sending the request. The URL * string should be url-encoded by you (if necessary) prior to passing it.

*/ public class Rest { private String urlString; private byte[] body; private final Map parameters = new TreeMap(); private final Map headers = new TreeMap(); private Integer connectTimeoutSeconds; private Integer readTimeoutSeconds; private Boolean sslVerification; private String sslPemUTF8; /** *

Sets the base URL to which the HTTP request will be sent. The URL may or may not include query parameters * (e.g. http://httpbin.org/get?param-1=foo).

* *

Depending on which HTTP verb is ultimately used, than any additional parameters set via the * parameters() method may be appending to this URL.

* *

Either way, the responsibility for any url-encoding of this base URL string belongs to the caller.

* * @param urlString A URL string, with any necessary url-encoding already applied @return The Rest instance itself */ public Rest url(final String urlString) { this.urlString = urlString; return this; } /** *

Sets a binary payload that will be sent as the request body for POST or PUT requests. Any value set here * will be ignored for GET requests. Conversely, if a value IS set here... then any additional parameter values * set by parameter() will be ignored for POST or PUT requests.

* * @param body The payload to send with a POST or PUT request (e.g. a JSON string) * @return The Rest instance itself */ public Rest body(final byte[] body) { this.body = body == null ? null : Arrays.copyOf(body, body.length); return this; } /** *

Adds a parameter to be sent with the HTTP request. Depending on which HTTP verb is ultimately used, this * parameter may either be appended to the URL or else posted with the request body. Either way, both the * parameter name and value will be automatically url-encoded by the Rest client.

* *

For POST and PUT requests, these parameters will only be sent in the request body if that body is otherwise * unset. In other words, if the body() method is invoked, then parameter() invocations * will be ignored for a POST or PUT.

* *

This method may be chained together repeatedly, to pass multiple parameters with a request. When the * request is ultimately sent, the parameters will be sorted by their names.

* * @param name The raw parameter name (not url-encoded) * @param value The raw parameter value (not url-encoded) * @return The Rest instance itself * @throws RestException */ public Rest parameter(final String name, final String value) throws RestException { try { this.parameters.put(URLEncoder.encode(name, "UTF-8"), URLEncoder.encode(value, "UTF-8")); } catch (UnsupportedEncodingException e) { throw new RestException(e); } return this; } /** *

Adds a header to be send with the HTTP request.

* *

Both the header name and value will be automatically url-encoded by the Rest client.

* *

This method may be chained together repeatedly, to pass multiple headers with a request. When the request * is ultimately sent, the headers will be sorted by their names.

* * @param name The raw header name (not url-encoded) * @param value The raw header value (not url-encoded) * @return The Rest instance itself * @throws RestException */ public Rest header(final String name, final String value) throws RestException { try { this.headers.put(URLEncoder.encode(name, "UTF-8"), URLEncoder.encode(value, "UTF-8")); } catch (UnsupportedEncodingException e) { throw new RestException(e); } return this; } /** *

The number of seconds to wait before giving up on establishing an HTTP(S) connection.

* * @param connectTimeoutSeconds Number of seconds to wait for an HTTP(S) connection to successfully establish * @return */ public Rest connectTimeoutSeconds(final Integer connectTimeoutSeconds) { this.connectTimeoutSeconds = connectTimeoutSeconds; return this; } /** *

After an HTTP(S) connection has already been established, this is the number of seconds to wait for all * data to finish downloading.

* * @param readTimeoutSeconds Number of seconds to wait for all data to be retrieved from an established HTTP(S) connection * @return */ public Rest readTimeoutSeconds(final Integer readTimeoutSeconds) { this.readTimeoutSeconds = readTimeoutSeconds; return this; } /** *

Whether or not HTTPS connections should verify that the server has a valid SSL certificate. * Unless this is set to false, the default behavior is to always verify SSL certificates.

* *

SSL CERTIFICATE VERIFICATION SHOULD NOT BE DISABLED IN PRODUCTION! This feature is made available to * facilitate development or testing environments, where you might be using a self-signed cert that will not * pass verification. However, even if you are using a self-signed cert on your server, you can still leave * SSL verification enabled and have your application supply the cert using sslPemFile(), * sslPemResource(), or sslPemUTF8().

* * @param sslVerification Whether or not to verify the SSL certificate used by the server with HTTPS connections. Default is true. * @return */ public Rest sslVerification(final Boolean sslVerification) { this.sslVerification = sslVerification; return this; } /** *

An X.509 certificate, to use when communicating with Vault over HTTPS. This method accepts a string * containing the certificate data. This string should meet the following requirements:

* *
    *
  • Contain an unencrypted X.509 certificate, in PEM format.
  • *
  • Use UTF-8 encoding.
  • *
  • * Contain a line-break between the certificate header (e.g. "-----BEGIN CERTIFICATE-----") and the * rest of the certificate content. It doesn't matter whether or not there are additional line * breaks within the certificate content, or whether there is a line break before the certificate * footer (e.g. "-----END CERTIFICATE-----"). But the Java standard library will fail to properly * process the certificate without a break following the header * (see http://www.doublecloud.org/2014/03/reading-x-509-certificate-in-java-how-to-handle-format-issue/). *
  • *
* * @param pemFileContents An X.509 certificate, in unencrypted PEM format with UTF-8 encoding. * @return */ public Rest sslPemUTF8(final String pemFileContents) { this.sslPemUTF8 = pemFileContents; return this; } /** *

Executes an HTTP GET request with the settings already configured. Parameters and headers are optional, but * a RestException will be thrown if the caller has not first set a base URL with the * url() method.

* *

If a body payload has been set through the body() method, then it will be ignored when sending * a GET request.

* * @return The result of the HTTP operation * @throws RestException */ public RestResponse get() throws RestException { if (urlString == null) { throw new RestException("No URL is set"); } try { if (!parameters.isEmpty()) { // Append parameters to existing query string, or create one if (urlString.indexOf('?') == -1) { urlString = urlString + "?" + parametersToQueryString(); } else { urlString = urlString + "&" + parametersToQueryString(); } } // Initialize HTTP(S) connection, and set any header values final URLConnection connection = initURLConnection(urlString, "GET"); for (final Map.Entry header : headers.entrySet()) { connection.setRequestProperty(header.getKey(), header.getValue()); } // Get the resulting status code final int statusCode = connectionStatus(connection); // Download and parse response final String mimeType = connection.getContentType(); final byte[] body = responseBodyBytes(connection); return new RestResponse(statusCode, mimeType, body); } catch (Exception e) { throw new RestException(e); } } /** * Executes an HTTP POST request with the settings already configured. Parameters and headers are optional, but a * RestException will be thrown if the caller has not first set a base URL with the * url() method. * *

CGI parameters can always be passed via a query string on the URL. Also, parameter values set via the * parameter() method will be sent with the POST request as form data. However, if a body payload * is provided via the body() method, then that takes precedence over any parameters set via * parameter(), and those values will be discarded.

* * @return The result of the HTTP operation * @throws RestException */ public RestResponse post() throws RestException { return postOrPutImpl(true); } /** * Executes an HTTP PUT request with the settings already configured. Parameters and headers are optional, but a * RestException will be thrown if the caller has not first set a base URL with the * url() method. * *

CGI parameters can always be passed via a query string on the URL. Also, parameter values set via the * parameter() method will be sent with the PUT request as form data. However, if a body payload * is provided via the body() method, then that takes precedence over any parameters set via * parameter(), and those values will be discarded.

* * @return The result of the HTTP operation * @throws RestException */ public RestResponse put() throws RestException { return postOrPutImpl(false); } /** * Executes an HTTP DELETE request with the settings already configured. Parameters and headers are optional, * but a RestException will be thrown if the caller has not first set a base URL with the * url() method. * * @return The result of the HTTP operation * @throws RestException */ public RestResponse delete() throws RestException { // TODO: Implement throw new UnsupportedOperationException(); } /** * Since the implementations of a POST request and PUT request differ by only one line of code, they are refactored * into this private method which is turned wrapped by post() and put(). * * @param doPost If true, then a POST operation will be performed. If false, then a PUT. * @return The result of the HTTP operation * @throws RestException */ private RestResponse postOrPutImpl(final boolean doPost) throws RestException { if (urlString == null) { throw new RestException("No URL is set"); } try { // Initialize HTTP connection, and set any header values URLConnection connection; if (doPost) { connection = initURLConnection(urlString, "POST"); } else { connection = initURLConnection(urlString, "PUT"); } for (final Map.Entry header : headers.entrySet()) { connection.setRequestProperty(header.getKey(), header.getValue()); } connection.setDoOutput(true); connection.setRequestProperty("Accept-Charset", "UTF-8"); // If a body payload has been provided, then it takes precedence. Otherwise, look for any additional // parameters to send as form field values. Parameters sent via the base URL query string are left // as-is regardless. if (body != null) { final OutputStream outputStream = connection.getOutputStream(); outputStream.write(body); outputStream.close(); } else if (!parameters.isEmpty()) { connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); final OutputStream outputStream = connection.getOutputStream(); outputStream.write(parametersToQueryString().getBytes("UTF-8")); outputStream.close(); } // Get the resulting status code final int statusCode = connectionStatus(connection); // Download and parse response final String mimeType = connection.getContentType(); final byte[] body = responseBodyBytes(connection); return new RestResponse(statusCode, mimeType, body); } catch (IOException e) { throw new RestException(e); } } /** *

This helper method constructs a new HttpURLConnection or HttpsURLConnection, * configured with all of the settings that were passed in when first initializing this Rest * instance (e.g. timeout thresholds, SSL verification, SSL certificate data).

* * @param urlString The URL to which this connection will be made * @param method The applicable request method (e.g. "GET", "POST", etc) * @return * @throws RestException If the URL cannot be successfully parsed, or if there are errors processing an SSL cert, etc. */ private URLConnection initURLConnection(final String urlString, final String method) throws RestException { try { final URL url = new URL(urlString); final URLConnection connection = url.openConnection(); // Timeout settings, if applicable if (connectTimeoutSeconds != null) { connection.setConnectTimeout(connectTimeoutSeconds * 1000); } if (readTimeoutSeconds != null) { connection.setReadTimeout(readTimeoutSeconds * 1000); } // SSL settings, if applicable if (connection instanceof HttpsURLConnection) { final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection; // Cert file supplied if (sslPemUTF8 != null) { final SSLContext sslContext = initSSLContext(); httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory()); } // SSL verification disabled if (sslVerification != null && !sslVerification.booleanValue()) { final SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[]{new X509TrustManager() { @Override public void checkClientTrusted(final X509Certificate[] x509Certificates, final String s) throws CertificateException { } @Override public void checkServerTrusted(final X509Certificate[] x509Certificates, final String s) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }}, new java.security.SecureRandom()); httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory()); httpsURLConnection.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(final String s, final SSLSession sslSession) { return true; } }); } httpsURLConnection.setRequestMethod(method); } else if (connection instanceof HttpURLConnection) { final HttpURLConnection httpURLConnection = (HttpURLConnection) connection; httpURLConnection.setRequestMethod(method); } else { final String message = "URL string " + urlString + " cannot be parsed as an instance of HttpURLConnection or HttpsURLConnection"; throw new RestException(message); } return connection; } catch (Exception e) { throw new RestException(e); } } /** *

This helper method is used when a X.509 certificate PEM file has been provided, to configure the HTTPS * connection with an in-memory keystore containing that certificate.

* * @return * @throws RestException If there are any issues processing the SSL cert */ private SSLContext initSSLContext() throws RestException { try { final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); final ByteArrayInputStream pem = new ByteArrayInputStream(sslPemUTF8.getBytes("UTF-8")); final X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(pem); pem.close(); final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); keyStore.setCertificateEntry("caCert", certificate); trustManagerFactory.init(keyStore); final SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagerFactory.getTrustManagers(), null); return sslContext; } catch (Exception e) { throw new RestException(e); } } /** *

This helper method constructs a query string (e.g. param-1=foo¶m-2=bar) from any parameters * that have been set via the param() method. Parameters will be sorted by name.

* * @return A url-encoded URL query string */ private String parametersToQueryString() { final StringBuilder queryString = new StringBuilder(); final List> params = new ArrayList>(parameters.entrySet()); for (int index = 0; index < params.size(); index++) { if (index > 0) { queryString.append('&'); } final String name = params.get(index).getKey(); final String value = params.get(index).getValue(); queryString.append(name).append('=').append(value); } return queryString.toString(); } /** *

This helper method downloads the body of an HTTP response (e.g. a clob of JSON text) as binary data.

* * @param connection An active HTTP(S) connection * @return The body payload, downloaded from the HTTP connection response */ private byte[] responseBodyBytes(final URLConnection connection) { try { final InputStream inputStream = connection.getInputStream(); final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int bytesRead; final byte[] bytes = new byte[16384]; while ((bytesRead = inputStream.read(bytes, 0, bytes.length)) != -1) { byteArrayOutputStream.write(bytes, 0, bytesRead); } byteArrayOutputStream.flush(); return byteArrayOutputStream.toByteArray(); } catch (IOException e) { return new byte[0]; } } /** *

This helper method extracts the HTTP(S) status code from a URLConnection, provided * that it is an HttpURLConnection or a HttpsUrlConnection.

* * @param connection An active HTTP(S) connection * @return * @throws IOException * @throws RestException */ private int connectionStatus(final URLConnection connection) throws IOException, RestException { int statusCode; if (connection instanceof HttpsURLConnection) { final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection; statusCode = httpsURLConnection.getResponseCode(); } else if (connection instanceof HttpURLConnection) { final HttpURLConnection httpURLConnection = (HttpURLConnection) connection; statusCode = httpURLConnection.getResponseCode(); } else { final String className = connection != null ? connection.getClass().getName() : "null"; throw new RestException("Expecting a URLConnection of type " + HttpURLConnection.class.getName() + " or " + HttpsURLConnection.class.getName() + ", found " + className); } return statusCode; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy