PwmHttpClient.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. /*
  2. * Password Management Servlets (PWM)
  3. * http://www.pwm-project.org
  4. *
  5. * Copyright (c) 2006-2009 Novell, Inc.
  6. * Copyright (c) 2009-2018 The PWM Project
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program; if not, write to the Free Software
  20. * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  21. */
  22. package password.pwm.http.client;
  23. import org.apache.http.Header;
  24. import org.apache.http.HttpHost;
  25. import org.apache.http.HttpRequest;
  26. import org.apache.http.HttpResponse;
  27. import org.apache.http.HttpStatus;
  28. import org.apache.http.auth.AuthScope;
  29. import org.apache.http.auth.UsernamePasswordCredentials;
  30. import org.apache.http.client.CredentialsProvider;
  31. import org.apache.http.client.HttpClient;
  32. import org.apache.http.client.config.RequestConfig;
  33. import org.apache.http.client.methods.HttpDelete;
  34. import org.apache.http.client.methods.HttpGet;
  35. import org.apache.http.client.methods.HttpPatch;
  36. import org.apache.http.client.methods.HttpPost;
  37. import org.apache.http.client.methods.HttpPut;
  38. import org.apache.http.client.methods.HttpRequestBase;
  39. import org.apache.http.config.Registry;
  40. import org.apache.http.config.RegistryBuilder;
  41. import org.apache.http.conn.HttpClientConnectionManager;
  42. import org.apache.http.conn.routing.HttpRoute;
  43. import org.apache.http.conn.routing.HttpRoutePlanner;
  44. import org.apache.http.conn.socket.ConnectionSocketFactory;
  45. import org.apache.http.conn.socket.PlainConnectionSocketFactory;
  46. import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
  47. import org.apache.http.entity.StringEntity;
  48. import org.apache.http.impl.client.BasicCredentialsProvider;
  49. import org.apache.http.impl.client.HttpClientBuilder;
  50. import org.apache.http.impl.client.ProxyAuthenticationStrategy;
  51. import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
  52. import org.apache.http.protocol.HttpContext;
  53. import org.apache.http.util.EntityUtils;
  54. import password.pwm.AppProperty;
  55. import password.pwm.PwmApplication;
  56. import password.pwm.PwmConstants;
  57. import password.pwm.bean.SessionLabel;
  58. import password.pwm.config.Configuration;
  59. import password.pwm.config.PwmSetting;
  60. import password.pwm.error.ErrorInformation;
  61. import password.pwm.error.PwmError;
  62. import password.pwm.error.PwmUnrecoverableException;
  63. import password.pwm.http.HttpHeader;
  64. import password.pwm.http.HttpMethod;
  65. import password.pwm.http.PwmURL;
  66. import password.pwm.util.java.StringUtil;
  67. import password.pwm.util.java.TimeDuration;
  68. import password.pwm.util.logging.PwmLogLevel;
  69. import password.pwm.util.logging.PwmLogger;
  70. import javax.net.ssl.SSLContext;
  71. import java.io.IOException;
  72. import java.io.InputStream;
  73. import java.net.URI;
  74. import java.net.URISyntaxException;
  75. import java.net.URL;
  76. import java.security.SecureRandom;
  77. import java.time.Instant;
  78. import java.util.LinkedHashMap;
  79. import java.util.List;
  80. import java.util.Map;
  81. import java.util.concurrent.atomic.AtomicInteger;
  82. public class PwmHttpClient
  83. {
  84. private static final PwmLogger LOGGER = PwmLogger.forClass( PwmHttpClient.class );
  85. private static final AtomicInteger REQUEST_COUNTER = new AtomicInteger( 0 );
  86. private final PwmApplication pwmApplication;
  87. private final SessionLabel sessionLabel;
  88. private final PwmHttpClientConfiguration pwmHttpClientConfiguration;
  89. public PwmHttpClient( final PwmApplication pwmApplication, final SessionLabel sessionLabel )
  90. {
  91. this.pwmApplication = pwmApplication;
  92. this.sessionLabel = sessionLabel;
  93. this.pwmHttpClientConfiguration = PwmHttpClientConfiguration.builder().certificates( null ).build();
  94. }
  95. public PwmHttpClient( final PwmApplication pwmApplication, final SessionLabel sessionLabel, final PwmHttpClientConfiguration pwmHttpClientConfiguration )
  96. {
  97. this.pwmApplication = pwmApplication;
  98. this.sessionLabel = sessionLabel;
  99. this.pwmHttpClientConfiguration = pwmHttpClientConfiguration;
  100. }
  101. public static HttpClient getHttpClient( final Configuration configuration )
  102. throws PwmUnrecoverableException
  103. {
  104. return getHttpClient( configuration, PwmHttpClientConfiguration.builder().certificates( null ).build(), null );
  105. }
  106. static HttpClient getHttpClient(
  107. final Configuration configuration,
  108. final PwmHttpClientConfiguration pwmHttpClientConfiguration,
  109. final SessionLabel sessionLabel
  110. )
  111. throws PwmUnrecoverableException
  112. {
  113. final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
  114. clientBuilder.setUserAgent( PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION );
  115. try
  116. {
  117. final SSLContext sslContext = SSLContext.getInstance( "TLS" );
  118. final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( configuration, sessionLabel, pwmHttpClientConfiguration );
  119. sslContext.init(
  120. null,
  121. httpTrustManagerHelper.makeTrustManager(),
  122. new SecureRandom() );
  123. final SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( sslContext, httpTrustManagerHelper.hostnameVerifier() );
  124. final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
  125. .register( "https", sslConnectionFactory )
  126. .register( "http", PlainConnectionSocketFactory.INSTANCE )
  127. .build();
  128. final HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager( registry );
  129. clientBuilder.setSSLHostnameVerifier( httpTrustManagerHelper.hostnameVerifier() );
  130. clientBuilder.setSSLContext( sslContext );
  131. clientBuilder.setSSLSocketFactory( sslConnectionFactory );
  132. clientBuilder.setConnectionManager( ccm );
  133. }
  134. catch ( Exception e )
  135. {
  136. throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "unexpected error creating promiscuous https client: " + e.getMessage() ) );
  137. }
  138. final String proxyUrl = configuration.readSettingAsString( PwmSetting.HTTP_PROXY_URL );
  139. if ( proxyUrl != null && proxyUrl.length() > 0 )
  140. {
  141. final URI proxyURI = URI.create( proxyUrl );
  142. final String host = proxyURI.getHost();
  143. final int port = proxyURI.getPort();
  144. final HttpHost proxyHost = new HttpHost( host, port );
  145. final String userInfo = proxyURI.getUserInfo();
  146. if ( userInfo != null && userInfo.length() > 0 )
  147. {
  148. final String[] parts = userInfo.split( ":" );
  149. final String username = parts[ 0 ];
  150. final String password = ( parts.length > 1 ) ? parts[ 1 ] : "";
  151. final CredentialsProvider credsProvider = new BasicCredentialsProvider();
  152. credsProvider.setCredentials( new AuthScope( host, port ), new UsernamePasswordCredentials( username, password ) );
  153. clientBuilder.setDefaultCredentialsProvider( credsProvider );
  154. clientBuilder.setProxyAuthenticationStrategy( new ProxyAuthenticationStrategy() );
  155. }
  156. clientBuilder.setRoutePlanner( new ProxyRoutePlanner( proxyHost, configuration, sessionLabel ) );
  157. }
  158. clientBuilder.setDefaultRequestConfig( RequestConfig.copy( RequestConfig.DEFAULT )
  159. .setSocketTimeout( Integer.parseInt( configuration.readAppProperty( AppProperty.HTTP_CLIENT_SOCKET_TIMEOUT_MS ) ) )
  160. .setConnectTimeout( Integer.parseInt( configuration.readAppProperty( AppProperty.HTTP_CLIENT_CONNECT_TIMEOUT_MS ) ) )
  161. .setConnectionRequestTimeout( Integer.parseInt( configuration.readAppProperty( AppProperty.HTTP_CLIENT_REQUEST_TIMEOUT_MS ) ) )
  162. .build() );
  163. return clientBuilder.build();
  164. }
  165. String entityToDebugString(
  166. final String topLine,
  167. final Map<String, String> headers,
  168. final String body
  169. )
  170. {
  171. final StringBuilder msg = new StringBuilder();
  172. msg.append( topLine );
  173. if ( StringUtil.isEmpty( body ) )
  174. {
  175. msg.append( " (no body)" );
  176. }
  177. msg.append( "\n" );
  178. for ( final Map.Entry<String, String> headerEntry : headers.entrySet() )
  179. {
  180. final HttpHeader httpHeader = HttpHeader.forHttpHeader( headerEntry.getKey() );
  181. if ( httpHeader != null )
  182. {
  183. final boolean sensitive = httpHeader.isSensitive();
  184. msg.append( " header: " ).append( httpHeader.getHttpName() ).append( "=" );
  185. if ( sensitive )
  186. {
  187. msg.append( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
  188. }
  189. else
  190. {
  191. msg.append( headerEntry.getValue() );
  192. }
  193. }
  194. else
  195. {
  196. // We encountered a header name that doesn't have a corresponding enum in HttpHeader,
  197. // so we can't check the sensitive flag.
  198. msg.append( " header: " ).append( headerEntry.getKey() ).append( "=" ).append( headerEntry.getValue() );
  199. }
  200. msg.append( "\n" );
  201. }
  202. if ( !StringUtil.isEmpty( body ) )
  203. {
  204. msg.append( " body: " );
  205. final boolean alwaysOutput = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_CLIENT_ALWAYS_LOG_ENTITIES ) );
  206. if ( alwaysOutput || !pwmHttpClientConfiguration.isMaskBodyDebugOutput() )
  207. {
  208. msg.append( body );
  209. }
  210. else
  211. {
  212. msg.append( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
  213. }
  214. }
  215. return msg.toString();
  216. }
  217. public PwmHttpClientResponse makeRequest( final PwmHttpClientRequest request ) throws PwmUnrecoverableException
  218. {
  219. try
  220. {
  221. return makeRequestImpl( request );
  222. }
  223. catch ( URISyntaxException | IOException e )
  224. {
  225. throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_UNREACHABLE, "error while making http request: " + e.getMessage() ), e );
  226. }
  227. }
  228. private PwmHttpClientResponse makeRequestImpl( final PwmHttpClientRequest clientRequest )
  229. throws IOException, URISyntaxException, PwmUnrecoverableException
  230. {
  231. final Instant startTime = Instant.now();
  232. final int counter = REQUEST_COUNTER.getAndIncrement();
  233. if ( LOGGER.isEnabled( PwmLogLevel.TRACE ) )
  234. {
  235. final String sslDebugText;
  236. if ( clientRequest.isHttps() )
  237. {
  238. final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( pwmApplication.getConfig(), sessionLabel, pwmHttpClientConfiguration );
  239. sslDebugText = "using " + httpTrustManagerHelper.debugText();
  240. }
  241. else
  242. {
  243. sslDebugText = "";
  244. }
  245. LOGGER.trace( sessionLabel, () -> "preparing to send (id=" + counter + ") "
  246. + clientRequest.toDebugString( this, sslDebugText ) );
  247. }
  248. final HttpResponse httpResponse = executeRequest( clientRequest );
  249. final String responseBody = EntityUtils.toString( httpResponse.getEntity() );
  250. final Map<String, String> responseHeaders = new LinkedHashMap<>();
  251. if ( httpResponse.getAllHeaders() != null )
  252. {
  253. for ( final Header header : httpResponse.getAllHeaders() )
  254. {
  255. responseHeaders.put( header.getName(), header.getValue() );
  256. }
  257. }
  258. final PwmHttpClientResponse httpClientResponse = new PwmHttpClientResponse(
  259. httpResponse.getStatusLine().getStatusCode(),
  260. httpResponse.getStatusLine().getReasonPhrase(),
  261. responseHeaders,
  262. responseBody
  263. );
  264. final TimeDuration duration = TimeDuration.fromCurrent( startTime );
  265. LOGGER.trace( sessionLabel, () -> "received response (id=" + counter + ") in "
  266. + duration.asCompactString() + ": "
  267. + httpClientResponse.toDebugString( this ) );
  268. return httpClientResponse;
  269. }
  270. private HttpResponse executeRequest( final PwmHttpClientRequest clientRequest )
  271. throws IOException, PwmUnrecoverableException
  272. {
  273. final String requestBody = clientRequest.getBody();
  274. final HttpRequestBase httpRequest;
  275. switch ( clientRequest.getMethod() )
  276. {
  277. case POST:
  278. {
  279. try
  280. {
  281. httpRequest = new HttpPost( new URI( clientRequest.getUrl() ).toString() );
  282. if ( requestBody != null && !requestBody.isEmpty() )
  283. {
  284. ( ( HttpPost ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
  285. }
  286. }
  287. catch ( URISyntaxException e )
  288. {
  289. throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "malformed url: " + clientRequest.getUrl() + ", error: " + e.getMessage() );
  290. }
  291. }
  292. break;
  293. case PUT:
  294. httpRequest = new HttpPut( clientRequest.getUrl() );
  295. if ( clientRequest.getBody() != null && !clientRequest.getBody().isEmpty() )
  296. {
  297. ( ( HttpPut ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
  298. }
  299. break;
  300. case PATCH:
  301. httpRequest = new HttpPatch( clientRequest.getUrl() );
  302. if ( clientRequest.getBody() != null && !clientRequest.getBody().isEmpty() )
  303. {
  304. ( ( HttpPatch ) httpRequest ).setEntity( new StringEntity( requestBody, PwmConstants.DEFAULT_CHARSET ) );
  305. }
  306. break;
  307. case GET:
  308. httpRequest = new HttpGet( clientRequest.getUrl() );
  309. break;
  310. case DELETE:
  311. httpRequest = new HttpDelete( clientRequest.getUrl() );
  312. break;
  313. default:
  314. throw new IllegalStateException( "http method not yet implemented" );
  315. }
  316. if ( clientRequest.getHeaders() != null )
  317. {
  318. for ( final String key : clientRequest.getHeaders().keySet() )
  319. {
  320. final String value = clientRequest.getHeaders().get( key );
  321. httpRequest.addHeader( key, value );
  322. }
  323. }
  324. final HttpClient httpClient = getHttpClient( pwmApplication.getConfig(), pwmHttpClientConfiguration, sessionLabel );
  325. return httpClient.execute( httpRequest );
  326. }
  327. public InputStream streamForUrl( final String inputUrl )
  328. throws IOException, PwmUnrecoverableException
  329. {
  330. final URL url = new URL( inputUrl );
  331. if ( "file".equals( url.getProtocol() ) )
  332. {
  333. return url.openStream();
  334. }
  335. if ( "http".equals( url.getProtocol() ) || "https".equals( url.getProtocol() ) )
  336. {
  337. final PwmHttpClientRequest pwmHttpClientRequest = new PwmHttpClientRequest(
  338. HttpMethod.GET,
  339. inputUrl,
  340. null,
  341. null
  342. );
  343. final HttpResponse httpResponse = executeRequest( pwmHttpClientRequest );
  344. if ( httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK )
  345. {
  346. final String errorMsg = "error retrieving stream for url '" + inputUrl + "', remote response: " + httpResponse.getStatusLine().toString();
  347. final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_REMOTE_ERROR_VALUE, errorMsg );
  348. LOGGER.error( errorInformation );
  349. throw new PwmUnrecoverableException( errorInformation );
  350. }
  351. return httpResponse.getEntity().getContent();
  352. }
  353. throw new IllegalArgumentException( "unknown protocol type: " + url.getProtocol() );
  354. }
  355. private static class ProxyRoutePlanner implements HttpRoutePlanner
  356. {
  357. private final HttpHost proxyServer;
  358. private final Configuration configuration;
  359. private final SessionLabel sessionLabel;
  360. ProxyRoutePlanner( final HttpHost proxyServer, final Configuration configuration, final SessionLabel sessionLabel )
  361. {
  362. this.proxyServer = proxyServer;
  363. this.configuration = configuration;
  364. this.sessionLabel = sessionLabel;
  365. }
  366. public HttpRoute determineRoute(
  367. final HttpHost target,
  368. final HttpRequest request,
  369. final HttpContext context
  370. )
  371. {
  372. final String targetUri = target.toURI();
  373. final List<String> proxyExceptionUrls = configuration.readSettingAsStringArray( PwmSetting.HTTP_PROXY_EXCEPTIONS );
  374. if ( PwmURL.testIfUrlMatchesAllowedPattern( targetUri, proxyExceptionUrls, sessionLabel ) )
  375. {
  376. return new HttpRoute( target );
  377. }
  378. final boolean secure = "https".equalsIgnoreCase( target.getSchemeName() );
  379. return new HttpRoute( target, null, proxyServer, secure );
  380. }
  381. }
  382. }