View Javadoc
1   /*
2    * Copyright (c) 2002-2017, Mairie de Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.webappcontainer.util;
35  
36  import java.io.IOException;
37  import java.net.HttpURLConnection;
38  import java.util.Iterator;
39  import java.util.Map;
40  import java.util.Set;
41  
42  import org.apache.commons.fileupload.FileItem;
43  import org.apache.commons.httpclient.Cookie;
44  import org.apache.commons.httpclient.Credentials;
45  import org.apache.commons.httpclient.Header;
46  import org.apache.commons.httpclient.HttpClient;
47  import org.apache.commons.httpclient.HttpException;
48  import org.apache.commons.httpclient.HttpMethodBase;
49  import org.apache.commons.httpclient.HttpState;
50  import org.apache.commons.httpclient.NTCredentials;
51  import org.apache.commons.httpclient.UsernamePasswordCredentials;
52  import org.apache.commons.httpclient.auth.AuthScope;
53  import org.apache.commons.httpclient.cookie.CookiePolicy;
54  import org.apache.commons.httpclient.methods.GetMethod;
55  import org.apache.commons.httpclient.methods.PostMethod;
56  import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource;
57  import org.apache.commons.httpclient.methods.multipart.FilePart;
58  import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
59  import org.apache.commons.httpclient.methods.multipart.Part;
60  import org.apache.commons.httpclient.methods.multipart.PartSource;
61  import org.apache.commons.httpclient.methods.multipart.StringPart;
62  import org.apache.commons.httpclient.params.HttpMethodParams;
63  import org.apache.commons.lang.StringUtils;
64  
65  import fr.paris.lutece.plugins.webappcontainer.business.Site;
66  import fr.paris.lutece.plugins.webappcontainer.business.WebappResponse;
67  import fr.paris.lutece.portal.service.util.AppLogService;
68  import fr.paris.lutece.portal.service.util.AppPropertiesService;
69  
70  /**
71   * Http net Object Accessor <br/>
72   */
73  public class HttpAccess
74  {
75  	// proxy authentication settings
76  	private static final String PROPERTY_PROXY_HOST = "webappcontainer.httpAccess.proxyHost";
77  
78  	private static final String PROPERTY_PROXY_PORT = "webappcontainer.httpAccess.proxyPort";
79  
80  	private static final String PROPERTY_PROXY_USERNAME = "webappcontainer.httpAccess.proxyUserName";
81  
82  	private static final String PROPERTY_PROXY_PASSWORD = "webappcontainer.httpAccess.proxyPassword";
83  
84  	private static final String PROPERTY_HOST_NAME = "webappcontainer.httpAccess.hostName";
85  
86  	private static final String PROPERTY_DOMAIN_NAME = "webappcontainer.httpAccess.domainName";
87  
88  	private static final String PROPERTY_REALM = "webappcontainer.httpAccess.realm";
89  
90  	private static final String PROPERTY_TIMEOUT_SOCKET = "webappcontainer.httpAccess.timeoutSocket";
91  
92  	private static final String PROPERTY_TIMEOUT_CONNECTION = "webappcontainer.httpAccess.timeoutConnection";
93  
94  	private static final String PROPERTY_PROTOCOL_CONTENT_CHARSET = "webappcontainer.site.default.encoding";
95  
96  	private static final String HTTP_SOCKET_TIMEOUT = "http.socket.timeout";
97  
98  	private static final String HTTP_CONNECTION_TIMEOUT = "http.connection.timeout";
99  
100 	private static final String HTTP_PROTOCOL_CONTENT_CHARSET = "http.protocol.content-charset";
101 
102 	private static final String RESPONSE_HEADER_LOCATION = "location";
103 
104 	private static final String REGEX_INT = "^[\\d]+$";
105 
106 	private static final int DEFAULT_TIMEOUT_SOCKET = 10000;
107 
108 	private static final int DEFAULT_TIMEOUT_CONNECTION = 10000;
109 
110 	private static final boolean FOLLOW_REDIRECT = true;
111 
112 	private Map<String, String[]> _parametersMap;
113 
114 	private Map<String, FileItem> _filesMap;
115 
116 	private HttpClient _client;
117 
118 	private Cookie[] _cookies = new Cookie[]
119 	{};
120 
121 	private Site _site = new Site();
122 
123 	/**
124 	 * Initialize the Http state with the previous state informations (cookies)
125 	 * 
126 	 * @param cookies The cookie array
127 	 * @param site The site requested
128 	 */
129 	public void initializeState( Cookie[] cookies, Site site )
130 	{
131 		// Save a copy to cookies to avoid malicious code
132 		if ( cookies != null )
133 		{
134 			_cookies = cookies.clone();
135 		}
136 
137 		_site = site;
138 	}
139 
140 	/**
141 	 * Send a GET HTTP request to an Url and return the response content.
142 	 * 
143 	 * @param strUrl The Url to access
144 	 * @return The response content of the Get request to the given Url
145 	 * @throws HttpAccessException if there is a problem to access to the given Url
146 	 */
147 	public WebappResponse doGet( String strUrl ) throws HttpAccessException
148 	{
149 		WebappResponse webappResponse = null;
150 
151 		try
152 		{
153 			AppLogService.debug( "HttpAccess.goGet( strUrl = " + strUrl + ")" );
154 
155 			HttpMethodBase method = new GetMethod( strUrl );
156 
157 			method.setFollowRedirects( FOLLOW_REDIRECT );
158 
159 			webappResponse = doRequest( method );
160 		}
161 		catch ( Exception e )
162 		{
163 			AppLogService.error( e.getMessage(), e );
164 			throw new HttpAccessException( e.getMessage(), e );
165 		}
166 
167 		return webappResponse;
168 	}
169 
170 	/**
171 	 * Send a POST HTTP request to an Url and return the response content.
172 	 * 
173 	 * @param strUrl The Url to access
174 	 * @param parametersMap The list of post parameters
175 	 * @param fileMap The file list
176 	 * @return The response content of the Post request to the given Url
177 	 * @throws HttpAccessException if there is a problem to access to the given Url
178 	 */
179 	public WebappResponse doPost( String strUrl, Map<String, String[]> parametersMap, Map<String, FileItem> fileMap ) throws HttpAccessException
180 	{
181 		WebappResponse webappResponse = null;
182 		_parametersMap = parametersMap;
183 		_filesMap = fileMap;
184 
185 		try
186 		{
187 			AppLogService.debug( "HttpAccess.goPost( strUrl = " + strUrl + ")" );
188 
189 			PostMethod method = new PostMethod( strUrl );
190 
191 			// If the file map is not null, then there is file to send to external site. So create the multi part request
192 			if ( fileMap != null )
193 			{
194 				setMultipartRequest( method, parametersMap, fileMap );
195 			}
196 			else
197 			{
198 				Set<String> setParametersString = parametersMap.keySet();
199 				Iterator<String> iteratorParametersString = setParametersString.iterator();
200 
201 				while (iteratorParametersString.hasNext())
202 				{
203 					String strKey = ( String ) iteratorParametersString.next();
204 					for ( String strValue : parametersMap.get( strKey ) )
205 					{
206 						method.addParameter( strKey, strValue );
207 					}
208 					// method.addParameter( strKey, ( String ) parametersMap.get( strKey )[0] );
209 				}
210 			}
211 
212 			webappResponse = doRequest( method );
213 		}
214 		catch ( Exception e )
215 		{
216 			AppLogService.error( e.getMessage(), e );
217 			throw new HttpAccessException( e.getMessage(), e );
218 		}
219 
220 		return webappResponse;
221 	}
222 
223 	/**
224 	 * In case of multipart request, set the multi parts with file list and String parameter list
225 	 * @param method The {@link PostMethod}
226 	 * @param parametersMap The list of String parameters
227 	 * @param fileMap The file list
228 	 */
229 	private void setMultipartRequest( PostMethod method, Map<String, String[]> parametersMap, Map<String, FileItem> fileMap )
230 	{
231 		Set<String> setParametersString = parametersMap.keySet();
232 		Iterator<String> iteratorParametersString = setParametersString.iterator();
233 		Set<String> setParametersFile = fileMap.keySet();
234 		Part[] parts = new Part[setParametersString.size() + setParametersFile.size()];
235 		int i = 0;
236 
237 		// Set the String parameters list
238 		while (iteratorParametersString.hasNext())
239 		{
240 			String strKey = ( String ) iteratorParametersString.next();
241 			parts[i++] = new StringPart( strKey, ( String ) parametersMap.get( strKey )[0] );
242 		}
243 
244 		method.getParams().setBooleanParameter( HttpMethodParams.USE_EXPECT_CONTINUE, true );
245 
246 		Iterator<String> iteratorParemetersFile = setParametersFile.iterator();
247 
248 		// Set the File parameters list
249 		while (iteratorParemetersFile.hasNext())
250 		{
251 			String strKey = ( String ) iteratorParemetersFile.next();
252 			FileItem fileItem = fileMap.get( strKey );
253 			PartSource partSource = new ByteArrayPartSource( fileItem.getName(), fileItem.get() );
254 			parts[i++] = new FilePart( strKey, partSource, fileItem.getContentType(), method.getRequestCharSet() );
255 		}
256 
257 		method.setRequestEntity( new MultipartRequestEntity( parts, method.getParams() ) );
258 	}
259 
260 	/**
261 	 * Send a HTTP request to an Url and return the response content.
262 	 * 
263 	 * @param method the request method
264 	 * @return The response content of the request in a {@link WebappResponse} object
265 	 * @throws HttpAccessException if there is a problem to access to the given Url
266 	 */
267 	private WebappResponse doRequest( HttpMethodBase method ) throws HttpAccessException
268 	{
269 		WebappResponse webappResponse = null;
270 		String strUrl = null;
271 
272 		try
273 		{
274 			strUrl = method.getURI().toString();
275 
276 			// Initialize the state with the previous cookies
277 			HttpState state = new HttpState();
278 			// get Cookies
279 			state.addCookies( _cookies );
280 
281 			HttpClient client = getHttpClient( method );
282 
283 			client.setState( state );
284 
285 			int nResponse = client.executeMethod( method );
286 
287 			// Show debug if debug mode is enabled
288 			if ( AppLogService.isDebugEnabled() )
289 			{
290 				showRequestDebugLogs( state );
291 
292 				AppLogService.debug( "[doRequest] Get content from " + strUrl );
293 				AppLogService.debug( "[doRequest] HTTP code : " + nResponse );
294 			}
295 
296 			// Redirections :
297 			if ( ( nResponse >= HttpURLConnection.HTTP_MULT_CHOICE ) && ( nResponse < HttpURLConnection.HTTP_BAD_REQUEST ) )
298 			{
299 				String strNewLocation = method.getResponseHeader( RESPONSE_HEADER_LOCATION ).getValue();
300 				AppLogService.debug( "[doRequest] Redirection -> " + strNewLocation );
301 
302 				// The redirection can only be caused by a postMethod (the get method have the followRedirect to true)
303 				return doPost( strNewLocation, _parametersMap, _filesMap );
304 			}
305 
306 			// Errors :
307 			if ( nResponse != HttpURLConnection.HTTP_OK )
308 			{
309 				String strError = "HttpAccess - Error getting URL : " + strUrl + " - return code : " + nResponse;
310 				throw new HttpAccessException( strError, null );
311 			}
312 
313 			// Check if the content is empty
314 			int nResponseBodyLength = method.getResponseBody().length;
315 
316 			if ( nResponseBodyLength <= 0 )
317 			{
318 				String strError = "HttpAccess - Error getting URL : No content received (" + nResponseBodyLength + " byte)";
319 				throw new HttpAccessException( strError, null );
320 			}
321 
322 			// Set the webappResponse with the response informations
323 			webappResponse = new WebappResponse();
324 
325 			// Set the content
326 			webappResponse.setContent( method.getResponseBody() );
327 
328 			// Set the content charset
329 			webappResponse.setContentCharset( _site.getEncoding() );
330 
331 			// Set the cookies list
332 			if ( client.getState().getCookies().length > 0 )
333 			{
334 				webappResponse.setCookies( client.getState().getCookies() );
335 			}
336 
337 			// Set the location
338 			if ( method.getURI() != null )
339 			{
340 				webappResponse.setLocation( method.getURI().getURI() );
341 			}
342 			else
343 			{
344 				webappResponse.setLocation( strUrl );
345 			}
346 
347 			// Show debug if debug mode is enabled
348 			if ( AppLogService.isDebugEnabled() )
349 			{
350 				showResponseDebugLogs( client, method );
351 			}
352 		}
353 		catch ( HttpException e )
354 		{
355 			String strError = "HttpAccess - Error connecting to '" + strUrl + "' : ";
356 			AppLogService.error( strError + e.getMessage(), e );
357 			throw new HttpAccessException( strError + e.getMessage(), e );
358 		}
359 		catch ( IOException e )
360 		{
361 			String strError = "HttpAccess - Error downloading '" + strUrl + "' : ";
362 			AppLogService.error( strError + e.getMessage(), e );
363 			throw new HttpAccessException( strError + e.getMessage(), e );
364 		}
365 		finally
366 		{
367 			// Release the connection.
368 			method.releaseConnection();
369 		}
370 
371 		return webappResponse;
372 	}
373 
374 	/**
375 	 * Create an HTTP client object using current configuration
376 	 * 
377 	 * @param method The method
378 	 * @return An HTTP client authenticated
379 	 * @throws HttpAccessException if there is a problem to access to the given Url
380 	 */
381 	private HttpClient getHttpClient( HttpMethodBase method ) throws HttpAccessException
382 	{
383 		String strProxyHost = AppPropertiesService.getProperty( PROPERTY_PROXY_HOST );
384 		String strProxyPort = AppPropertiesService.getProperty( PROPERTY_PROXY_PORT );
385 		String strProxyUserName = AppPropertiesService.getProperty( PROPERTY_PROXY_USERNAME );
386 		String strProxyPassword = AppPropertiesService.getProperty( PROPERTY_PROXY_PASSWORD );
387 		String strHostName = AppPropertiesService.getProperty( PROPERTY_HOST_NAME );
388 		String strDomainName = AppPropertiesService.getProperty( PROPERTY_DOMAIN_NAME );
389 		String strRealm = AppPropertiesService.getProperty( PROPERTY_REALM );
390 		String strContentCharset = _site.getEncoding();
391 		int nTimeoutSocket = getPropertyInt( PROPERTY_TIMEOUT_SOCKET, DEFAULT_TIMEOUT_SOCKET );
392 		int nTimeoutConnection = getPropertyInt( PROPERTY_TIMEOUT_CONNECTION, DEFAULT_TIMEOUT_CONNECTION );
393 
394 		if ( _client != null )
395 		{
396 			return _client;
397 		}
398 
399 		// Create an instance of HttpClient.
400 		HttpClient client = new HttpClient();
401 
402 		// Set socket and connection timeout
403 		client.getParams().setParameter( HTTP_SOCKET_TIMEOUT, nTimeoutSocket );
404 		client.getParams().setParameter( HTTP_CONNECTION_TIMEOUT, nTimeoutConnection );
405 
406 		// Set the encoding
407 		if ( StringUtils.isBlank( strContentCharset ) )
408 		{
409 			strContentCharset = AppPropertiesService.getProperty( PROPERTY_PROTOCOL_CONTENT_CHARSET );
410 		}
411 		client.getParams().setParameter( HTTP_PROTOCOL_CONTENT_CHARSET, strContentCharset );
412 
413 		// If proxy host and port are defined and site use proxy, set the corresponding elements
414 		if ( ( strProxyHost != null ) && ( !strProxyHost.equals( "" ) ) && ( strProxyPort != null ) && ( !strProxyPort.equals( "" ) ) && _site.isUseProxy() )
415 		{
416 			client.getHostConfiguration().setProxy( strProxyHost, Integer.parseInt( strProxyPort ) );
417 		}
418 
419 		Credentials cred = null;
420 
421 		// if hostname and domain name found, consider we are in NTLM authentication scheme
422 		// else if only username and password found, use simple UsernamePasswordCredentials
423 		if ( ( strHostName != null ) && ( strDomainName != null ) )
424 		{
425 			cred = new NTCredentials( strProxyUserName, strProxyPassword, strHostName, strDomainName );
426 		}
427 		else if ( ( strProxyUserName != null ) && ( strProxyPassword != null ) )
428 		{
429 			cred = new UsernamePasswordCredentials( strProxyUserName, strProxyPassword );
430 		}
431 
432 		if ( cred != null )
433 		{
434 			AuthScope authScope = new AuthScope( null, -1, strRealm, null );
435 			client.getState().setProxyCredentials( authScope, cred );
436 
437 			client.getParams().setAuthenticationPreemptive( true );
438 
439 			method.setDoAuthentication( true );
440 		}
441 
442 		client.getParams().setCookiePolicy( CookiePolicy.BROWSER_COMPATIBILITY );
443 		_client = client;
444 
445 		return client;
446 	}
447 
448 	/**
449 	 * Method used to display debug informations before requesting
450 	 * 
451 	 * @param state The {@link HttpState} object
452 	 */
453 	private void showRequestDebugLogs( HttpState state )
454 	{
455 		// [debug] Display the cookies
456 		for ( Cookie c : state.getCookies() )
457 		{
458 			AppLogService.debug( "[doRequest] setCookie = " + c.getName() + "=" + c.getValue() + " - domain : " + c.getDomain() );
459 		}
460 	}
461 
462 	/**
463 	 * Method used to display response debug informations
464 	 * 
465 	 * @param client The {@link HttpClient}
466 	 * @param method The {@link HttpMethodBase}
467 	 * @throws IOException Exception occurs when getting response body
468 	 */
469 	private void showResponseDebugLogs( HttpClient client, HttpMethodBase method ) throws IOException
470 	{
471 		// [debug] Display the cookies
472 		for ( Cookie cookie : client.getState().getCookies() )
473 		{
474 			AppLogService.debug( "[doRequest][response] getCookie = " + cookie.getName() + " " + cookie.getValue() + " - domain : " + cookie.getDomain() );
475 		}
476 
477 		AppLogService.debug( "[doRequest] Received : " + method.getResponseBody().length + " bytes" );
478 
479 		// [debug] Display the headers
480 		for ( Header header : method.getResponseHeaders() )
481 		{
482 			AppLogService.debug( "Method Header" + header.getName() + " -> " + header.getValue() );
483 		}
484 	}
485 
486 	/**
487 	 * Returns the value of a property defined in the .properties file of the application as an int
488 	 * 
489 	 * @param strPropertyName The property name
490 	 * @param nDefault The default value which is returned if no value is found for the property in the .properties file
491 	 * @return The property value read in the .properties file
492 	 * @throws HttpAccessException if there is a problem to access to the given Url
493 	 */
494 	private int getPropertyInt( String strPropertyName, int nDefault ) throws HttpAccessException
495 	{
496 		String strPropertyValue = AppPropertiesService.getProperty( strPropertyName );
497 		int nPropertyValue = nDefault;
498 
499 		try
500 		{
501 			if ( ( strPropertyValue != null ) && strPropertyValue.matches( REGEX_INT ) )
502 			{
503 				nPropertyValue = Integer.parseInt( strPropertyValue );
504 			}
505 		}
506 		catch ( NumberFormatException e )
507 		{
508 			String strError = "HttpAccess - Error retrieving property '" + strPropertyName + "' : ";
509 			AppLogService.error( strError + e.getMessage(), e );
510 			throw new HttpAccessException( strError + e.getMessage(), e );
511 		}
512 
513 		return nPropertyValue;
514 	}
515 }