/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.http.client;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.PwmConstants;
import password.pwm.bean.SessionLabel;
import password.pwm.config.Configuration;
import password.pwm.config.PwmSetting;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.http.HttpMethod;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.secure.X509Utils;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
public class PwmHttpClient {
private static final PwmLogger LOGGER = PwmLogger.forClass(PwmHttpClient.class);
private static int classCounter = 0;
private final PwmApplication pwmApplication;
private final SessionLabel sessionLabel;
private final PwmHttpClientConfiguration pwmHttpClientConfiguration;
public PwmHttpClient(final PwmApplication pwmApplication, final SessionLabel sessionLabel) {
this.pwmApplication = pwmApplication;
this.sessionLabel = sessionLabel;
this.pwmHttpClientConfiguration = new PwmHttpClientConfiguration.Builder().setCertificate(null).create();
}
public PwmHttpClient(final PwmApplication pwmApplication, final SessionLabel sessionLabel, final PwmHttpClientConfiguration pwmHttpClientConfiguration) {
this.pwmApplication = pwmApplication;
this.sessionLabel = sessionLabel;
this.pwmHttpClientConfiguration = pwmHttpClientConfiguration;
}
public static HttpClient getHttpClient(final Configuration configuration)
throws PwmUnrecoverableException
{
return getHttpClient(configuration, new PwmHttpClientConfiguration.Builder().setCertificate(null).create());
}
public static HttpClient getHttpClient(final Configuration configuration, final PwmHttpClientConfiguration pwmHttpClientConfiguration)
throws PwmUnrecoverableException
{
final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
clientBuilder.setUserAgent(PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION);
final boolean httpClientPromiscuousEnable = Boolean.parseBoolean(configuration.readAppProperty(AppProperty.SECURITY_HTTP_PROMISCUOUS_ENABLE));
try {
if (httpClientPromiscuousEnable || (pwmHttpClientConfiguration != null && pwmHttpClientConfiguration.isPromiscuous())) {
clientBuilder.setSSLContext(promiscuousSSLContext());
clientBuilder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
} else if (pwmHttpClientConfiguration != null && pwmHttpClientConfiguration.getCertificates() != null) {
final SSLContext sslContext = SSLContext.getInstance("SSL");
final TrustManager trustManager = new X509Utils.CertMatchingTrustManager(configuration, pwmHttpClientConfiguration.getCertificates());
sslContext.init(null, new TrustManager[]{ trustManager }, new SecureRandom());
final SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslConnectionFactory).build();
final HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager(registry);
clientBuilder.setSSLSocketFactory(sslConnectionFactory);
clientBuilder.setConnectionManager(ccm);
}
} catch (Exception e) {
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"unexpected error creating promiscuous https client: " + e.getMessage()));
}
final String proxyUrl = configuration.readSettingAsString(PwmSetting.HTTP_PROXY_URL);
if (proxyUrl != null && proxyUrl.length() > 0) {
final URI proxyURI = URI.create(proxyUrl);
final String host = proxyURI.getHost();
final int port = proxyURI.getPort();
clientBuilder.setProxy(new HttpHost(host, port));
final String userInfo = proxyURI.getUserInfo();
if (userInfo != null && userInfo.length() > 0) {
final String[] parts = userInfo.split(":");
final String username = parts[0];
final String password = (parts.length > 1) ? parts[1] : "";
final CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope(host, port), new UsernamePasswordCredentials(username, password));
clientBuilder.setDefaultCredentialsProvider(credsProvider);
clientBuilder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
}
}
clientBuilder.setDefaultRequestConfig(RequestConfig.copy(RequestConfig.DEFAULT)
.setSocketTimeout(Integer.parseInt(configuration.readAppProperty(AppProperty.HTTP_CLIENT_SOCKET_TIMEOUT_MS)))
.setConnectTimeout(Integer.parseInt(configuration.readAppProperty(AppProperty.HTTP_CLIENT_CONNECT_TIMEOUT_MS)))
.setConnectionRequestTimeout(Integer.parseInt(configuration.readAppProperty(AppProperty.HTTP_CLIENT_REQUEST_TIMEOUT_MS)))
.build());
return clientBuilder.build();
}
static String entityToDebugString(
final String topLine,
final Map<String, String> headers,
final String body
) {
final StringBuilder msg = new StringBuilder();
msg.append(topLine);
if (body == null || body.isEmpty()) {
msg.append(" (no body)");
}
msg.append("\n");
for (final String key : headers.keySet()) {
msg.append(" header: ").append(key).append("=").append(headers.get(key)).append("\n");
}
if (body != null && !body.isEmpty()) {
msg.append(" body: ").append(body);
}
return msg.toString();
}
public PwmHttpClientResponse makeRequest(final PwmHttpClientRequest request) throws PwmUnrecoverableException {
try {
return makeRequestImpl(request);
} catch (URISyntaxException | IOException e) {
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_SERVICE_UNREACHABLE, "error while making http request: " + e.getMessage()), e);
}
}
PwmHttpClientResponse makeRequestImpl(final PwmHttpClientRequest clientRequest)
throws IOException, URISyntaxException, PwmUnrecoverableException {
final Instant startTime = Instant.now();
final int counter = classCounter++;
LOGGER.trace(sessionLabel, "preparing to send (id=" + counter + ") " + clientRequest.toDebugString());
final HttpResponse httpResponse = executeRequest(clientRequest);
final String responseBody = EntityUtils.toString(httpResponse.getEntity());
final Map<String, String> responseHeaders = new LinkedHashMap<>();
if (httpResponse.getAllHeaders() != null) {
for (final Header header : httpResponse.getAllHeaders()) {
responseHeaders.put(header.getName(), header.getValue());
}
}
final PwmHttpClientResponse httpClientResponse = new PwmHttpClientResponse(
httpResponse.getStatusLine().getStatusCode(),
httpResponse.getStatusLine().getReasonPhrase(),
responseHeaders,
responseBody
);
final TimeDuration duration = TimeDuration.fromCurrent(startTime);
LOGGER.trace(sessionLabel, "received response (id=" + counter + ") in " + duration.asCompactString() + ": " + httpClientResponse.toDebugString());
return httpClientResponse;
}
private HttpResponse executeRequest(final PwmHttpClientRequest clientRequest)
throws IOException, PwmUnrecoverableException {
final String requestBody = clientRequest.getBody();
final HttpRequestBase httpRequest;
switch (clientRequest.getMethod()) {
case POST:
{
try {
httpRequest = new HttpPost(new URI(clientRequest.getUrl()).toString());
if (requestBody != null && !requestBody.isEmpty()) {
((HttpPost) httpRequest).setEntity(new StringEntity(requestBody, PwmConstants.DEFAULT_CHARSET));
}
} catch (URISyntaxException e) {
throw PwmUnrecoverableException.newException(PwmError.ERROR_UNKNOWN, "malformed url: " + clientRequest.getUrl() + ", error: " + e.getMessage());
}
}
break;
case PUT:
httpRequest = new HttpPut(clientRequest.getUrl());
if (clientRequest.getBody() != null && !clientRequest.getBody().isEmpty()) {
((HttpPut) httpRequest).setEntity(new StringEntity(requestBody, PwmConstants.DEFAULT_CHARSET));
}
break;
case PATCH:
httpRequest = new HttpPatch(clientRequest.getUrl());
if (clientRequest.getBody() != null && !clientRequest.getBody().isEmpty()) {
((HttpPatch) httpRequest).setEntity(new StringEntity(requestBody, PwmConstants.DEFAULT_CHARSET));
}
break;
case GET:
httpRequest = new HttpGet(clientRequest.getUrl());
break;
case DELETE:
httpRequest = new HttpDelete(clientRequest.getUrl());
break;
default:
throw new IllegalStateException("http method not yet implemented");
}
if (clientRequest.getHeaders() != null) {
for (final String key : clientRequest.getHeaders().keySet()) {
final String value = clientRequest.getHeaders().get(key);
httpRequest.addHeader(key, value);
}
}
final HttpClient httpClient = getHttpClient(pwmApplication.getConfig(), pwmHttpClientConfiguration);
return httpClient.execute(httpRequest);
}
protected static SSLContext promiscuousSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
return new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(final X509Certificate[] arg0, final String arg1) throws CertificateException {
return true;
}
}).build();
}
public InputStream streamForUrl(final String inputUrl)
throws IOException, PwmUnrecoverableException {
final URL url = new URL(inputUrl);
if ("file".equals(url.getProtocol())) {
return url.openStream();
}
if ("http".equals(url.getProtocol()) || "https".equals(url.getProtocol())) {
final PwmHttpClientRequest pwmHttpClientRequest = new PwmHttpClientRequest(
HttpMethod.GET,
inputUrl,
null,
null,
null
);
final HttpResponse httpResponse = executeRequest(pwmHttpClientRequest);
if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
final String errorMsg = "error retrieving stream for url '" + inputUrl + "', remote response: " + httpResponse.getStatusLine().toString();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_REMOTE_ERROR_VALUE, errorMsg);
LOGGER.error(errorInformation);
throw new PwmUnrecoverableException(errorInformation);
}
return httpResponse.getEntity().getContent();
}
throw new IllegalArgumentException("unknown protocol type: " + url.getProtocol());
}
}