/* * Copyright (C) 2011-2012 Intel Corporation * All rights reserved. */ package com.intel.mtwilson; import com.intel.dcsg.cpg.i18n.LocaleUtil; import com.intel.mtwilson.api.*; import com.intel.dcsg.cpg.crypto.SimpleKeystore; import com.intel.dcsg.cpg.tls.policy.ProtocolSelector; import com.intel.mtwilson.security.http.apache.ApacheHttpAuthorization; import com.intel.dcsg.cpg.x509.repository.KeystoreCertificateRepository; import com.intel.dcsg.cpg.tls.policy.TlsPolicy; import com.intel.dcsg.cpg.tls.policy.impl.ConfigurableProtocolSelector; import com.intel.dcsg.cpg.tls.policy.impl.FirstCertificateTrustDelegate; import com.intel.dcsg.cpg.tls.policy.impl.InsecureTlsPolicy; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.util.Locale; import javax.net.ssl.SSLContext; import javax.net.ssl.X509TrustManager; import javax.ws.rs.core.MediaType; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.SystemConfiguration; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.params.ClientPNames; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import com.fasterxml.jackson.databind.ObjectMapper; import com.intel.dcsg.cpg.rfc822.Headers; import com.intel.dcsg.cpg.rfc822.Rfc822Date; import com.intel.dcsg.cpg.tls.policy.TlsUtil; import com.intel.dcsg.cpg.tls.policy.impl.CertificateTlsPolicy; import com.intel.dcsg.cpg.tls.policy.impl.PublicKeyTlsPolicy; import com.intel.dcsg.cpg.x509.repository.PublicKeyCertificateRepository; import java.util.Calendar; import java.util.Date; import org.apache.http.client.methods.HttpRequestBase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @since 0.5.2 * @author jbuhacoff */ public class ApacheHttpClient implements java.io.Closeable { private final Logger log = LoggerFactory.getLogger(getClass()); private final String ACCEPT_LANGUAGE = "Accept-Language"; // private SchemeRegistry sr; private ClientConnectionManager connectionManager; private HttpClient httpClient; // private SimpleKeystore keystore; private String protocol = "https"; // private String tlsProtocol = "TLS"; // issue #870 possible values are SSL, SSLv2, SSLv3, TLS, TLSv1, TLSv1.1, and TLSv1.2, and we use TLS as the default private int port = 443; private TlsPolicy apacheTlsPolicy; // private boolean requireTrustedCertificate = true; // private boolean verifyHostname = true; private Locale locale = null; private ApacheHttpAuthorization authority = null; // can be any implementation - Hmac256 or RSA protected static final ObjectMapper mapper = new ObjectMapper(); private int timeDeltaMs = 0; // the difference in time between the client and the server, computed timeDeltaMs = serverDate - clientDate; when client sends a request to server, client sends requestDate = clientDate + timeDeltaMs which should match serverDate; the timeDeltaMs is updated after every response received from the server /** * If you don't have a specific configuration, you can pass in * SystemConfiguration() so that users can set system properties and have * them passed through to this object. * * @param baseURL for the server to access (all requests are based on this * URL) * @param credentials to use when signing HTTP requests, or null if you want * to skip the Authorization header * @param sslKeystore containing trusted SSL certificates * @param config with parameters requireTrustedCertificates and * verifyHostname; if null a SystemConfiguration will be used. * @throws NoSuchAlgorithmException * @throws KeyManagementException */ public ApacheHttpClient(URL baseURL, ApacheHttpAuthorization credentials, SimpleKeystore sslKeystore, Configuration config) throws NoSuchAlgorithmException, KeyManagementException { authority = credentials; // keystore = sslKeystore; protocol = baseURL.getProtocol(); port = baseURL.getPort(); if (port == -1) { port = baseURL.getDefaultPort(); } //log.debug("ApacheHttpClient: Protocol: {}", protocol); //log.debug("ApacheHttpClient: Port: {}", port); if (config == null) { config = new SystemConfiguration(); log.debug("ApacheHttpClient: using system configuration"); } // requireTrustedCertificate = config.getBoolean("mtwilson.api.ssl.requireTrustedCertificate", true); // verifyHostname = config.getBoolean("mtwilson.api.ssl.verifyHostname", true); apacheTlsPolicy = createTlsPolicy(config, sslKeystore); // tlsProtocol = config.getString("mtwilson.api.ssl.protocol", "TLS"); //log.debug("ApacheHttpClient: TLS Policy Name: {}", tlsPolicy.getClass().getName()); initHttpClient(); } /** * Uses default protocol "TLS" , if you want to use something else such as * SSLv3 or TLSv1.2 then call setTlsProtocol("TLSv1.2") after instantiating * the ApacheHttpClient * * @param baseURL * @param credentials * @param sslKeystore * @param tlsPolicy * @throws NoSuchAlgorithmException * @throws KeyManagementException */ public ApacheHttpClient(URL baseURL, ApacheHttpAuthorization credentials, SimpleKeystore sslKeystore, TlsPolicy tlsPolicy) throws NoSuchAlgorithmException, KeyManagementException { authority = credentials; // keystore = sslKeystore; protocol = baseURL.getProtocol(); port = baseURL.getPort(); if (port == -1) { port = baseURL.getDefaultPort(); } //log.debug("ApacheHttpClient: Protocol: {}", protocol); //log.debug("ApacheHttpClient: Port: {}", port); // requireTrustedCertificate = config.getBoolean("mtwilson.api.ssl.requireTrustedCertificate", true); // verifyHostname = config.getBoolean("mtwilson.api.ssl.verifyHostname", true); apacheTlsPolicy = tlsPolicy; // createApacheTlsPolicy(tlsPolicy/*, sslKeystore*/); // tlsProtocol = "TLS"; // the default //log.debug("ApacheHttpClient: TLS Policy Name: {}", tlsPolicy.getClass().getName()); initHttpClient(); } private void initHttpClient() throws KeyManagementException, NoSuchAlgorithmException { // log.debug("Using TLS protocol {}", tlsProtocol); SchemeRegistry sr = initSchemeRegistryWithPolicy(protocol, port, apacheTlsPolicy/*, tlsProtocol*/); connectionManager = new PoolingClientConnectionManager(sr); // the http client is re-used for all the requests HttpParams httpParams = new BasicHttpParams(); httpParams.setParameter(ClientPNames.HANDLE_REDIRECTS, false); httpClient = new DefaultHttpClient(connectionManager, httpParams); } /* public void setTlsProtocol(String protocol) throws KeyManagementException, NoSuchAlgorithmException { // tlsProtocol = protocol; initHttpClient(); // reset the client with the updated protocol } */ /** * Used in Mt Wilson 1.1 * * If the configuration mentions a specific TLS Policy (new in 1.1) that one * is used, otherwise the trusted certificate and verify hostname settings * used in 1.0-RC2 are used to choose an appropriate TLS Policy. * * @param config * @param sslKeystore * @return */ private TlsPolicy createTlsPolicy(Configuration config, SimpleKeystore sslKeystore) { String tlsProtocol = config.getString("mtwilson.api.ssl.protocol", "TLS"); ProtocolSelector pSelector = new ConfigurableProtocolSelector(tlsProtocol); String tlsPolicyName = config.getString("mtwilson.api.ssl.policy", ""); if (tlsPolicyName == null || tlsPolicyName.isEmpty()) { // no 1.1 policy name, so use 1.0-RC2 settings to pick a policy boolean requireTrustedCertificate = config.getBoolean("mtwilson.api.ssl.requireTrustedCertificate", true); boolean verifyHostname = config.getBoolean("mtwilson.api.ssl.verifyHostname", true); if (requireTrustedCertificate && verifyHostname) { log.debug("Using TLS Policy TRUST_CA_VERIFY_HOSTNAME"); return new CertificateTlsPolicy(sslKeystore.getRepository(), pSelector); } else if (requireTrustedCertificate && !verifyHostname) { // two choices: trust first certificate or trust known certificate; we choose trust first certificate as a usability default // furthermore we assume that the api client keystore is a server-specific keystore (it's a client configured for a specific mt wilson server) // that either has a server instance ssl cert or a cluster ssl cert. either should work. log.debug("Using TLS Policy TRUST_FIRST_CERTIFICATE"); KeystoreCertificateRepository repository = sslKeystore.getRepository(); return new PublicKeyTlsPolicy(new PublicKeyCertificateRepository(repository), new FirstCertificateTrustDelegate(repository), pSelector); } else { // !requireTrustedCertificate && (verifyHostname || !verifyHostname) log.warn("Using TLS Policy INSECURE"); return new InsecureTlsPolicy(); } } else if (tlsPolicyName.equals("TRUST_CA_VERIFY_HOSTNAME")) { log.debug("TLS Policy: TRUST_CA_VERIFY_HOSTNAME"); return new CertificateTlsPolicy(sslKeystore.getRepository(), pSelector); } else if (tlsPolicyName.equals("TRUST_FIRST_CERTIFICATE")) { log.debug("TLS Policy: TRUST_FIRST_CERTIFICATE"); KeystoreCertificateRepository repository = sslKeystore.getRepository(); return new PublicKeyTlsPolicy(new PublicKeyCertificateRepository(repository), new FirstCertificateTrustDelegate(repository), pSelector); } else if (tlsPolicyName.equals("TRUST_KNOWN_CERTIFICATE")) { log.debug("TLS Policy: TRUST_KNOWN_CERTIFICATE"); return new PublicKeyTlsPolicy(new PublicKeyCertificateRepository(sslKeystore.getRepository()), pSelector); } else if (tlsPolicyName.equals("INSECURE")) { log.warn("TLS Policy: INSECURE"); return new InsecureTlsPolicy(); } else { // unrecognized 1.1 policy defined, so use a secure default log.warn("Unknown TLS Policy Name: {}", tlsPolicyName); return new CertificateTlsPolicy(sslKeystore.getRepository(), pSelector); // issue #871 default should be secure } } /* public final void setBaseURL(URL baseURL) { this.baseURL = baseURL; } public final void setKeystore(SimpleKeystore keystore) { this.keystore = keystore; } public final void setRequireTrustedCertificate(boolean value) { requireTrustedCertificate = value; } public final void setVerifyHostname(boolean value) { verifyHostname = value; } * */ /** * Used in Mt Wilson 1.0-RC2 * * Base URL and other configuration must already be set before calling this * method. * * @param protocol either "http" or "https" * @param port such as 80 for http, 443 for https * @throws KeyManagementException * @throws NoSuchAlgorithmException */ /* private SchemeRegistry initSchemeRegistry(String protocol, int port) throws KeyManagementException, NoSuchAlgorithmException { SchemeRegistry sr = new SchemeRegistry(); if( "http".equals(protocol) ) { Scheme http = new Scheme("http", port, PlainSocketFactory.getSocketFactory()); sr.register(http); } if( "https".equals(protocol) ) { X509HostnameVerifier hostnameVerifier; // secure by default (default verifyHostname = true) X509TrustManager trustManager; // secure by default, using Java's implementation which verifies the peer and using java's trusted keystore as default if user does not provide a specific keystore if( verifyHostname ) { hostnameVerifier = SSLSocketFactory.STRICT_HOSTNAME_VERIFIER; } else { // if( !config.getBoolean("mtwilson.api.ssl.verifyHostname", true) ) { hostnameVerifier = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; } if( requireTrustedCertificate && keystore != null ) { trustManager = SslUtil.createX509TrustManagerWithKeystore(keystore); } else if( requireTrustedCertificate ) { // config.getBoolean("mtwilson.api.ssl.requireTrustedCertificate", true) ) { //String truststore = config.getString("mtwilson.api.keystore", System.getProperty("javax.net.ssl.trustStorePath")); // if null use default java trust store... //String truststorePassword = config.getString("mtwilson.api.keystore.password", System.getProperty("javax.net.ssl.trustStorePassword")); // String truststore = System.getProperty("javax.net.ssl.trustStorePath"); String truststore = System.getProperty("javax.net.ssl.trustStore"); String truststorePassword = System.getProperty("javax.net.ssl.trustStorePassword"); // create a trust manager using only our trusted ssl certificates if( truststore == null || truststorePassword == null ) { throw new IllegalArgumentException("Require trusted certificates is enabled but truststore is not configured"); } keystore = new SimpleKeystore(new File(truststore), truststorePassword); trustManager = SslUtil.createX509TrustManagerWithKeystore(keystore); } else { // user does not want to ensure certificates are trusted, so use a no-op trust manager trustManager = new NopX509TrustManager(); } SSLContext sslcontext = SSLContext.getInstance("TLS"); sslcontext.init(null, new X509TrustManager[] { trustManager }, null); // key manager, trust manager, securerandom SSLSocketFactory sf = new SSLSocketFactory( sslcontext, hostnameVerifier ); Scheme https = new Scheme("https", port, sf); // URl defaults to 443 for https but if user specified a different port we use that instead sr.register(https); } return sr; } */ /** * Used in Mt Wilson 1.1 * * @param protocol * @param port * @param policy * @param tlsProtocol like SSL, SSLv2, SSLv3, TLS, TLSv1.1, TLSv1.2 * @return * @throws KeyManagementException * @throws NoSuchAlgorithmException */ private SchemeRegistry initSchemeRegistryWithPolicy(String protocol, int port, TlsPolicy policy /*, String tlsProtocol*/) throws KeyManagementException, NoSuchAlgorithmException { SchemeRegistry sr = new SchemeRegistry(); if ("http".equals(protocol)) { Scheme http = new Scheme("http", port, PlainSocketFactory.getSocketFactory()); sr.register(http); } if ("https".equals(protocol)) { log.debug("Initializing {} connection", policy.getProtocolSelector().preferred()); SSLContext sslcontext = SSLContext.getInstance(TlsUtil.getSafeContextName(policy.getProtocolSelector().preferred()) /*tlsProtocol*/); // issue #870 allow client to configure TLS protocol version with mtwilson.api.ssl.protocol sslcontext.init(null, new X509TrustManager[]{policy.getTrustManager()}, null); // key manager, trust manager, securerandom SSLSocketFactory sf = new SSLSocketFactory( sslcontext, policy.getHostnameVerifier()); Scheme https = new Scheme("https", port, sf); // URL defaults to 443 for https but if user specified a different port we use that instead sr.register(https); } return sr; } /** * Call this to ensure that all HTTP connections and files are closed when * your are done using the API Client. */ @Override public void close() { connectionManager.shutdown(); } private MediaType createMediaType(HttpResponse response) { if (response.getFirstHeader("Content-Type") != null) { String contentType = response.getFirstHeader("Content-Type").getValue(); log.debug("We got Content-Type: " + contentType); return MediaType.valueOf(contentType); } log.warn("Missing content type header from server, assuming application/octet-stream"); return MediaType.APPLICATION_OCTET_STREAM_TYPE; } /** * * Typically the HttpEntity is NOT chunked, is streaming, and is not repeatable: * <pre> 2014-06-01 23:42:11,041 DEBUG [http-bio-8443-exec-17] c.i.m.ApacheHttpClient [ApacheHttpClient.java:354] We got Content-Type: text/plain 2014-06-01 23:42:11,041 DEBUG [http-bio-8443-exec-17] c.i.m.ApacheHttpClient [ApacheHttpClient.java:371] HttpEntity Content Length = 2 2014-06-01 23:42:11,041 DEBUG [http-bio-8443-exec-17] c.i.m.ApacheHttpClient [ApacheHttpClient.java:372] HttpEntity is chunked? false 2014-06-01 23:42:11,041 DEBUG [http-bio-8443-exec-17] c.i.m.ApacheHttpClient [ApacheHttpClient.java:373] HttpEntity is streaming? true 2014-06-01 23:42:11,041 DEBUG [http-bio-8443-exec-17] c.i.m.ApacheHttpClient [ApacheHttpClient.java:374] HttpEntity is repeatable? false * </pre> * * @param response * @return * @throws IOException */ private ApiResponse readResponse(HttpResponse response) throws IOException { calculateTimeDelta(response); MediaType contentType = createMediaType(response); byte[] content = null; HttpEntity entity = response.getEntity(); if (entity != null) { InputStream contentStream = entity.getContent(); if (contentStream != null) { content = IOUtils.toByteArray(contentStream); contentStream.close(); } log.debug("HttpEntity Content Length = {}", entity.getContentLength()); log.debug("HttpEntity is chunked? {}", entity.isChunked()); log.debug("HttpEntity is streaming? {}", entity.isStreaming()); log.debug("HttpEntity is repeatable? {}", entity.isRepeatable()); } return new ApiResponse(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), contentType, content); } private void calculateTimeDelta(HttpResponse response) { Calendar now = Calendar.getInstance(); Date serverTime = Rfc822Date.parse(response.getFirstHeader("Date").getValue()); timeDeltaMs = (int)(serverTime.getTime() - now.getTimeInMillis()); log.debug("calculated time delta {} = server time {} - client time {}", timeDeltaMs, serverTime, now.getTime()); } private void addHeaders(HttpRequestBase request, Headers headers) { for (String name : headers.names()) { // skip "content-length" as org.apache.http.client.HttpClient.execute method needs to set it and will cause an error if it's already set if (name.equalsIgnoreCase("content-length")) { continue; } for (String value : headers.getAll(name)) { request.addHeader(name, value); } } } private void addLocaleHeader(HttpRequestBase request) { if (locale != null) { request.addHeader(ACCEPT_LANGUAGE, LocaleUtil.toAcceptHeader(locale)); } } private void setDateHeader(HttpRequestBase request) { log.debug("current time is {}", new Date()); Calendar now = Calendar.getInstance(); now.add(Calendar.MILLISECOND, timeDeltaMs); request.setHeader("Date", Rfc822Date.format(now.getTime())); log.debug("set date header to {} using time delta {}", request.getFirstHeader("Date").getValue(), timeDeltaMs); } public ApiResponse get(String requestURL) throws IOException, ApiException, SignatureException { return get(requestURL, null); } public ApiResponse get(String requestURL, Headers headers) throws IOException, ApiException, SignatureException { log.debug("GET url: {}", requestURL); HttpGet request = new HttpGet(requestURL); addLocaleHeader(request); setDateHeader(request); if (headers != null) { addHeaders(request, headers); } if (authority != null) { authority.addAuthorization(request); // add authorization header } // send the request and print the response HttpResponse httpResponse = httpClient.execute(request); ApiResponse apiResponse = readResponse(httpResponse); request.releaseConnection(); return apiResponse; } public ApiResponse delete(String requestURL) throws IOException, SignatureException { return delete(requestURL, null); } public ApiResponse delete(String requestURL, Headers headers) throws IOException, SignatureException { log.debug("DELETE url: {}", requestURL); HttpDelete request = new HttpDelete(requestURL); addLocaleHeader(request); setDateHeader(request); if (headers != null) { addHeaders(request, headers); } if (authority != null) { authority.addAuthorization(request); // add authorization header } // send the request and print the response HttpResponse httpResponse = httpClient.execute(request); ApiResponse apiResponse = readResponse(httpResponse); request.releaseConnection(); return apiResponse; } public ApiResponse put(String requestURL, ApiRequest message) throws IOException, SignatureException { return put(requestURL, message, null); } public ApiResponse put(String requestURL, ApiRequest message, Headers headers) throws IOException, SignatureException { log.debug("PUT url: {}", requestURL); //log.debug("PUT content: {}", message == null ? "(empty)" : message.content); HttpPut request = new HttpPut(requestURL); if (message != null && message.content != null) { request.setEntity(new StringEntity(message.content, ContentType.create(message.contentType.toString(), "UTF-8"))); } addLocaleHeader(request); setDateHeader(request); if (headers != null) { addHeaders(request, headers); } if (authority != null) { authority.addAuthorization((HttpEntityEnclosingRequest) request); // add authorization header } HttpResponse httpResponse = httpClient.execute(request); ApiResponse apiResponse = readResponse(httpResponse); request.releaseConnection(); return apiResponse; } public ApiResponse post(String requestURL, ApiRequest message) throws IOException, SignatureException { return post(requestURL, message, null); } public ApiResponse post(String requestURL, ApiRequest message, Headers headers) throws IOException, SignatureException { log.debug("POST url: {}", requestURL); //log.debug("POST content-type: {}", message == null ? "(empty)" : message.content.toString()); //log.debug("POST content: {}", message == null ? "(empty)" : message.content); HttpPost request = new HttpPost(requestURL); if (message != null && message.content != null) { request.setEntity(new StringEntity(message.content, ContentType.create(message.contentType.toString(), "UTF-8"))); } //System.out.println("debug|HTTP POST message content: " + message.content); addLocaleHeader(request); setDateHeader(request); if (headers != null) { addHeaders(request, headers); } if (authority != null) { authority.addAuthorization((HttpEntityEnclosingRequest) request); // add authorization header } HttpResponse httpResponse = httpClient.execute(request); ApiResponse apiResponse = readResponse(httpResponse); //System.out.println("debug|HTTP Response content: " + new String(apiResponse.content, Charset.forName("UTF-8"))); request.releaseConnection(); return apiResponse; } public Locale getLocale() { return locale; } public void setLocale(Locale locale) { this.locale = locale; } }