/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.esri.gpt.framework.http; import com.esri.gpt.catalog.context.CatalogConfiguration; import com.esri.gpt.framework.context.ApplicationContext; import com.esri.gpt.framework.http.multipart.MultiPartContentProvider; import com.esri.gpt.framework.http.multipart.PartWriter; import com.esri.gpt.framework.util.Val; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Map; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.PatternSyntaxException; import java.util.zip.GZIPInputStream; import org.apache.commons.httpclient.*; import org.apache.commons.httpclient.auth.AuthPolicy; import org.apache.commons.httpclient.auth.AuthScheme; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.auth.AuthState; import org.apache.commons.httpclient.methods.*; import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource; import org.apache.commons.httpclient.methods.multipart.FilePart; import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; import org.apache.commons.httpclient.methods.multipart.StringPart; import org.apache.commons.httpclient.params.HttpMethodParams; /** * Provides an interface for the execution of outbound HTTP requests. * <p/> * This class delegates underlying HTTP related functonality the the Apache HttpClient. * <p/> * If a forward proxy is in place, the following system properties must be * configured at the Java web server level (e.g. Tomcat - catalina.properties) * <ul> * <li>http.proxyHost, http.proxyPort, http.nonProxyHosts</li> * <li>https.proxyHost, https.proxyPort, https.nonProxyHosts</li> * </ul> * If the forward proxy requires credentials, the following system properties * are considered: * <ul> * <li>http.proxyUser, http.proxyPassword</li> * <li>https.proxyUser, https.proxyPassword</li> * </ul> * The gpt.xml file can be used to set the proxyUser/proxyPassword properties * based upon an encrypted password: * <br/><gtpConfig> * <br/> <forwardProxyAuth * <br/> username="" * <br/> password="" * <br/> encrypted="true"/> * <br/></gtpConfig> * <p/> * The above system properties are also used by the Apache Axis module for SOAP * based comminication. */ public class HttpClientRequest { /** class variables ========================================================= */ public static final int DEFAULT_CONNECTION_TIMEOUT = 2 * 60 * 1000; // two minutes public static final int DEFAULT_RESPONSE_TIMEOUT = 2 * 60 * 1000; // two minutes /** The Logger. */ private static Logger LOGGER = Logger.getLogger(HttpClientRequest.class.getName()); // methods GET PUT POST DELETE HEAD OPTIONS TRACE /** instance variables ====================================================== */ private HttpClient batchHttpClient; private CredentialProvider credentialProvider; private ContentHandler contentHandler; private ContentProvider contentProvider; private StringBuffer executionLog = new StringBuffer(); private MethodName methodName; private Map<String,String> requestHeaders = new LinkedHashMap<String,String>(); private ResponseInfo responseInfo = new ResponseInfo(); private String url; private int connectionTimeOut = DEFAULT_CONNECTION_TIMEOUT; private int responseTimeOut = DEFAULT_RESPONSE_TIMEOUT; private int retries = -1; /** constructors ============================================================ */ /** Default constructor. */ public HttpClientRequest() { this.setCredentialProvider(CredentialProvider.getThreadLocalInstance()); this.setConnectionTimeMs(getCatalogConfiguration().getConnectionTimeOutMs()); this.setResponseTimeOutMs(getCatalogConfiguration().getResponseTimeOutMs()); } /** properties **************************************************************/ /** * Gets the underlying Apache HttpClient to be used for batch requests * to the same server. * @return the batch client */ public HttpClient getBatchHttpClient() { return this.batchHttpClient; } /** * Sets the underlying Apache HttpClient to be used for batch requests * to the same server. * @param batchHttpClient the batch client */ public void setBatchHttpClient(HttpClient batchHttpClient) { this.batchHttpClient = batchHttpClient; } /** * Gets the connection time out in milliseconds. * * @return the connection time out (always >= 0) */ public int getConnectionTimeOutMs() { if(connectionTimeOut < 0) { connectionTimeOut = 0; } return connectionTimeOut; } /** * Sets the connection time out in milliseconds. * * @param connectionTimeOut the new connection time out */ public void setConnectionTimeMs(int connectionTimeOut) { this.connectionTimeOut = connectionTimeOut; } /** * Gets the response time out in milliseconds * * @return the response time out (always >= 0) */ public int getResponseTimeOutMs() { if(responseTimeOut < 0) { responseTimeOut = 0; } return responseTimeOut; } /** * Sets the response time out in milliseconds. * * @param responseTimeOut the new response time out */ public void setResponseTimeOutMs(int responseTimeOut) { this.responseTimeOut = responseTimeOut; } /** * Gets the provider for HTTP authorization credentials. * @return the credential provider */ public CredentialProvider getCredentialProvider() { return this.credentialProvider; } /** * Sets the provider for HTTP authorization credentials. * @param provider the credential provider */ public void setCredentialProvider(CredentialProvider provider) { this.credentialProvider = provider; } /** * Gets the handler for the content of the HTTP response body. * @return the response content handler */ public ContentHandler getContentHandler() { return this.contentHandler; } /** * Sets the handler for the content of the HTTP response body. * @param handler the response content handler */ public void setContentHandler(ContentHandler handler) { this.contentHandler = handler; } /** * Gets the provider for the content of the HTTP request body. * @return the request content provider */ public ContentProvider getContentProvider() { return this.contentProvider; } /** * Sets the provider for the content of the HTTP request body. * @param provider the request content provider */ public void setContentProvider(ContentProvider provider) { this.contentProvider = provider; } /** * Gets a buffer representing the loggable content of an executed HTTP request. * @return the execution log buffer */ public StringBuffer getExecutionLog() { return this.executionLog; } /** * Gets the HTTP method name. * @return the method name */ public MethodName getMethodName() { return this.methodName; } /** * Sets the HTTP method name. * @param name the method name */ public void setMethodName(MethodName name) { this.methodName = name; } /** * Sets an HTTP request header value. * @param name the header paramater name * @param value the header paramater value */ public void setRequestHeader(String name, String value) { this.requestHeaders.put(name,value); } /** * Gets information associated with an HTTP response. * @return the HTTP response information */ public ResponseInfo getResponseInfo() { return this.responseInfo; } /** * Sets information associated with an HTTP response. * @param info the HTTP response information */ protected void setResponseInfo(ResponseInfo info) { this.responseInfo = info; } /** * Gets the URL for the request. * @return the request URL */ public String getUrl() { return this.url; } /** * Sets the URL for the request. * @param url the request URL */ public void setUrl(String url) { this.url = url; } /** * Gets the retries. * * @return the retries */ public int getRetries() { return retries; } /** * Sets the retries. * * @param retries the new retries */ public void setRetries(int retries) { this.retries = retries; } /** methods ================================================================= */ /** * Adds an retry handler to an HTTP method. * @param method the HttpMethod to be executed */ private void addRetryHandler(HttpMethod method) { method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3,false)); } /** * Applies authentication and forward proxy settings if required. * @param client the Apache HttpClient * @param url the target URL * @throws MalformedURLException if the URL is malformed */ private void applyAuthAndProxySettings(HttpClient client, String url) throws MalformedURLException { // initialize the target settings URL targetURL = new URL(url); String targetHost = targetURL.getHost(); int targetPort = targetURL.getPort(); String targetProtocol = targetURL.getProtocol(); if (targetPort == -1) { if (targetProtocol.equalsIgnoreCase("https")) { targetPort = 443; } else { targetPort = 80; } } // TODO: is the host setting required? //HostConfiguration config = client.getHostConfiguration(); //config.setHost(targetHost,targetPort,targetProtocol); // establish authentication credentials if (this.getCredentialProvider() != null) { String username = this.getCredentialProvider().getUsername(); String password = this.getCredentialProvider().getPassword(); if ((username != null) && (username.length() > 0) && (password != null)) { AuthScope scope = new AuthScope(null,-1); UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username,password); client.getState().setCredentials(scope,creds); // NTLM is based upon username pattern (domain\\username) int ntDomainIdx = username.indexOf("\\"); if (ntDomainIdx > 0) { String left = username.substring(0,ntDomainIdx); String right = username.substring(ntDomainIdx+1); if ((left.length() > 0) && (right.length() > 0)) { String ntDomain = left; username = right; String ntHost = targetHost; // TODO: should this be the host sending the request? AuthScope ntScope = new AuthScope(null,-1,null,AuthPolicy.NTLM); NTCredentials ntCreds = new NTCredentials(username,password,ntHost,ntDomain); client.getState().setCredentials(ntScope,ntCreds); } } } } // initialize the proxy settings String proxyHost = Val.chkStr((String)System.getProperty(targetProtocol+".proxyHost")); int proxyPort = Val.chkInt((String)System.getProperty(targetProtocol+".proxyPort"),-1); String nonProxyHosts = Val.chkStr((String)System.getProperty(targetProtocol+".nonProxyHosts")); String proxyUser = Val.chkStr((String)System.getProperty(targetProtocol+".proxyUser")); String proxyPassword = (String)System.getProperty(targetProtocol+".proxyPassword"); if (proxyPort == -1) { proxyPort = 80; } // check for a non-proxy host match boolean isNonProxyHost = false; if ((proxyHost.length() > 0) && (nonProxyHosts.length() > 0)) { StringTokenizer tokenizer = new StringTokenizer(nonProxyHosts,"|\""); while (tokenizer.hasMoreTokens()) { String nonProxyHost = Val.chkStr(tokenizer.nextToken()); if (nonProxyHost.length() > 0) { if (nonProxyHost.indexOf("*") != -1) { StringBuffer sb = new StringBuffer(); sb.append('^'); for (int i = 0;i<nonProxyHost.length();i++) { char c = nonProxyHost.charAt(i); switch(c) { case '*': sb.append(".*"); break; case '(': case ')': case '[': case ']': case '$': case '^': case '.': case '{': case '}': case '|': case '\\': case '?': sb.append("\\").append(c); break; default: sb.append(c); break; } } sb.append('$'); nonProxyHost = sb.toString(); } try { if (targetHost.matches(nonProxyHost)) { isNonProxyHost = true; break; } } catch (PatternSyntaxException pse) { // TODO: warn if the pattern syntax is incorrect? //pse.printStackTrace(System.err); } } } } // set the proxy and authentication credentials if required if ((proxyHost.length() > 0) && !isNonProxyHost) { // configure the proxy host and port client.getHostConfiguration().setProxy(proxyHost,proxyPort); // establish proxy-authentication credentials // TODO: lookup gpt.xml proxy-auth String username = proxyUser; String password = proxyPassword; if ((username != null) && (username.length() > 0) && (password != null)) { AuthScope scope = new AuthScope(null,-1); UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username,password); client.getState().setProxyCredentials(scope,creds); // NTLM is based upon username pattern (domain\\username) int ntDomainIdx = username.indexOf("\\"); if (ntDomainIdx > 0) { String left = username.substring(0,ntDomainIdx); String right = username.substring(ntDomainIdx+1); if ((left.length() > 0) && (right.length() > 0)) { String ntDomain = left; username = right; String ntHost = proxyHost; // TODO: should this be the host sending the request? AuthScope ntScope = new AuthScope(null,-1,null,AuthPolicy.NTLM); NTCredentials ntCreds = new NTCredentials(username,password,ntHost,ntDomain); client.getState().setProxyCredentials(ntScope,ntCreds); } } } } } /** * Create the HTTP method. * <br/>A GetMethod will be created if the RequestEntity associated with * the ContentProvider is null. Otherwise, a PostMethod will be created. * @return the HTTP method */ private HttpMethodBase createMethod() throws IOException { HttpMethodBase method = null; MethodName name = this.getMethodName(); // make the method if (name == null) { if (this.getContentProvider() == null) { this.setMethodName(MethodName.GET); method = new GetMethod(this.getUrl()); } else { this.setMethodName(MethodName.POST); method = new PostMethod(this.getUrl()); } } else if (name.equals(MethodName.DELETE)) { method = new DeleteMethod(this.getUrl()); } else if (name.equals(MethodName.GET)) { method = new GetMethod(this.getUrl()); } else if (name.equals(MethodName.POST)) { method = new PostMethod(this.getUrl()); } else if (name.equals(MethodName.PUT)) { method = new PutMethod(this.getUrl()); } // write the request body if necessary if (this.getContentProvider() != null) { if (method instanceof EntityEnclosingMethod) { EntityEnclosingMethod eMethod = (EntityEnclosingMethod)method; RequestEntity eAdapter = getContentProvider() instanceof MultiPartContentProvider? new MultiPartProviderAdapter(this, eMethod, (MultiPartContentProvider)getContentProvider()): new ApacheEntityAdapter(this,this.getContentProvider()); eMethod.setRequestEntity(eAdapter); if (eAdapter.getContentType() != null) { eMethod.setRequestHeader("Content-type",eAdapter.getContentType()); } } else { // TODO: possibly will need an exception here in the future } } // set headers, add the retry method for (Map.Entry<String,String> hdr: this.requestHeaders.entrySet()) { method.addRequestHeader(hdr.getKey(),hdr.getValue()); } // declare possible gzip handling method.setRequestHeader("Accept-Encoding", "gzip"); this.addRetryHandler(method); return method; } /** * Determines basic information associted with an HTTP response. * <br/>For example: response status message, Content-Type, Content-Length * @param method the HttpMethod that was executed */ private void determineResponseInfo(HttpMethodBase method) { this.getResponseInfo().setResponseMessage(method.getStatusText()); this.getResponseInfo().setResponseHeaders(method.getResponseHeaders()); Header contentTypeHeader = method.getResponseHeader("Content-Type"); if (contentTypeHeader != null) { HeaderElement values[] = contentTypeHeader.getElements(); // Expect only one header element to be there, no more, no less if (values.length == 1) { this.getResponseInfo().setContentType(values[0].getName()); NameValuePair param = values[0].getParameterByName("charset"); if (param != null) { // If invalid, an UnsupportedEncondingException will result this.getResponseInfo().setContentEncoding(param.getValue()); } } } this.getResponseInfo().setContentLength(method.getResponseContentLength()); } /** * Executes the HTTP request. * @throws IOException if an Exception occurs */ public void execute() throws IOException { // initialize this.executionLog.setLength(0); StringBuffer log = this.executionLog; ResponseInfo respInfo = this.getResponseInfo(); respInfo.reset(); InputStream responseStream = null; HttpMethodBase method = null; try { log.append("HTTP Client Request\n").append(this.getUrl()); // make the Apache HTTPClient HttpClient client = this.batchHttpClient; if (client == null) { client = new HttpClient(); boolean alwaysClose = Val.chkBool(Val.chkStr(ApplicationContext.getInstance().getConfiguration().getCatalogConfiguration().getParameters().getValue("httpClient.alwaysClose")), false); if (alwaysClose) { client.setHttpConnectionManager(new SimpleHttpConnectionManager(true)); } } // setting timeout info client.getHttpConnectionManager().getParams().setConnectionTimeout( getConnectionTimeOutMs()); client.getHttpConnectionManager().getParams().setSoTimeout( getResponseTimeOutMs()); // setting retries int retries = this.getRetries(); // create the client and method, apply authentication and proxy settings method = this.createMethod(); //method.setFollowRedirects(true); if(retries > -1) { // TODO: not taking effect yet? DefaultHttpMethodRetryHandler retryHandler = new DefaultHttpMethodRetryHandler(retries, true); client.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, retryHandler); method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, retryHandler); } this.applyAuthAndProxySettings(client,this.getUrl()); // execute the method, determine basic information about the response respInfo.setResponseCode(client.executeMethod(method)); this.determineResponseInfo(method); // collect logging info if (LOGGER.isLoggable(Level.FINER)) { log.append("\n>>").append(method.getStatusLine()); log.append("\n--Request Header"); for (Header hdr: method.getRequestHeaders()) { log.append("\n ").append(hdr.getName()+": "+hdr.getValue()); } log.append("\n--Response Header"); for (Header hdr: method.getResponseHeaders()) { log.append("\n ").append(hdr.getName()+": "+hdr.getValue()); } //log.append(" responseCode=").append(this.getResponseInfo().getResponseCode()); //log.append(" responseContentType=").append(this.getResponseInfo().getContentType()); //log.append(" responseContentEncoding=").append(this.getResponseInfo().getContentEncoding()); //log.append(" responseContentLength=").append(this.getResponseInfo().getContentLength()); if (this.getContentProvider() != null) { String loggable = this.getContentProvider().getLoggableContent(); if (loggable != null) { log.append("\n--Request Content------------------------------------\n").append(loggable); } } } // throw an exception if an error is encountered if ((respInfo.getResponseCode() < 200) || (respInfo.getResponseCode() >= 300)) { String msg = "HTTP Request failed: " + method.getStatusLine(); if (respInfo.getResponseCode() == HttpStatus.SC_UNAUTHORIZED) { AuthState authState = method.getHostAuthState(); AuthScheme authScheme = authState.getAuthScheme(); HttpClient401Exception authException = new HttpClient401Exception(msg); authException.setUrl(this.getUrl()); authException.setRealm(authState.getRealm()); authException.setScheme(authScheme.getSchemeName()); if ((authException.getRealm() == null) || (authException.getRealm().length() == 0)) { authException.setRealm(authException.generateHostBasedRealm()); } throw authException; } else { throw new HttpClientException(respInfo.getResponseCode(),msg); } } // handle the response if (this.getContentHandler() != null) { if (getContentHandler().onBeforeReadResponse(this)) { responseStream = getResponseStream(method); if (responseStream != null) { this.getContentHandler().readResponse(this,responseStream); } } // log thre response content String loggable = this.getContentHandler().getLoggableContent(); long nBytesRead = this.getResponseInfo().getBytesRead(); long nCharsRead = this.getResponseInfo().getCharactersRead(); if ((nBytesRead >= 0) || (nCharsRead >= 0) || (loggable != null)) { log.append("\n--Response Content------------------------------------"); if (nBytesRead >= 0) log.append("\n(").append(nBytesRead).append(" bytes read)"); if (nCharsRead >= 0) log.append("\n(").append(nCharsRead).append(" characters read)"); if (loggable != null) log.append("\n").append(loggable); } } } finally { // cleanup try { if (responseStream != null) responseStream.close(); } catch (Throwable t) { LOGGER.log(Level.SEVERE,"Unable to close HTTP response stream.",t); } try { if (method != null) method.releaseConnection(); } catch (Throwable t) { LOGGER.log(Level.SEVERE,"Unable to release HttpMethod",t); } // log the request/response if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer(this.getExecutionLog().toString()); } } } /** * Gets the HTTP response stream. * @param method the HTTP method * @return the response stream * @throws IOException if an i/o exception occurs */ private InputStream getResponseStream(HttpMethodBase method) throws IOException { // Check is Content-Encoding is gzip Header hdr = method.getResponseHeader("Content-Encoding"); if (hdr!=null && "gzip".equals(hdr.getValue())) { return new GZIPInputStream(method.getResponseBodyAsStream()); } return method.getResponseBodyAsStream(); } /** * Instantiates a new HTTP client request. * @return the HTTP client request */ public static HttpClientRequest newRequest() { return new HttpClientRequest(); } /** * Instantiates a new HTTP client request. * @param name the HTTP method name * @param url the target URL * @return the HTTP client request */ public static HttpClientRequest newRequest(MethodName name, String url) { HttpClientRequest request = HttpClientRequest.newRequest(); request.setMethodName(name); request.setUrl(url); return request; } /** * Exceutes the HPPR request and returns the response as a string. * @return the HTTP response * @throws IOException if an i/o exception occurs */ public String readResponseAsCharacters() throws IOException { StringHandler handler = new StringHandler(); this.setContentHandler(handler); this.execute(); return Val.removeBOM(handler.getContent()); } /** * Gets catalog configuration. * @return catalog configuration */ private CatalogConfiguration getCatalogConfiguration() { return ApplicationContext.getInstance().getConfiguration().getCatalogConfiguration(); } /** inner classes =========================================================== */ /** The enumeration of upported HTTP method names. */ public enum MethodName { GET, DELETE, POST, PUT; } private static class PartWriterAdapter implements PartWriter { ArrayList<org.apache.commons.httpclient.methods.multipart.Part> parts; public PartWriterAdapter(ArrayList<org.apache.commons.httpclient.methods.multipart.Part> parts) { this.parts = parts; } @Override public void write(String name, String value) throws IOException { parts.add(new StringPart(name, value, "UTF-8")); } @Override public void write(String name, final File file, String fileName, String contentType, String charset, final boolean deleteAfterUpload) throws IOException { parts.add(new FilePart(name, fileName!=null && !fileName.isEmpty()? fileName: file.getName(), file, contentType, charset){ @Override protected void sendData(OutputStream out) throws IOException { super.sendData(out); if (deleteAfterUpload) { try { boolean deleted = file.delete(); if (!deleted) { LOGGER.warning("Unable to delete file: "+file.getAbsolutePath()); } } catch (SecurityException ex) { LOGGER.warning("Unable to delete file: "+file.getAbsolutePath()); } } } }); } @Override public void write(String name, byte[] bytes, String fileName, String contentType, String charset) throws IOException { parts.add(new FilePart(name, new ByteArrayPartSource(fileName, bytes), contentType, charset)); } } /** * Multi part provider adapter. */ private static class MultiPartProviderAdapter implements RequestEntity { private HttpClientRequest request; private EntityEnclosingMethod method; private MultiPartContentProvider provider; private MultipartRequestEntity entity; public MultiPartProviderAdapter(HttpClientRequest request, EntityEnclosingMethod method, MultiPartContentProvider provider) throws IOException { this.request = request; this.method = method; this.provider = provider; // ArrayList<org.apache.commons.httpclient.methods.multipart.Part> parts = new ArrayList<org.apache.commons.httpclient.methods.multipart.Part>(); // provider.writeParts(new PartWriterAdapter(parts)); // this.entity = new MultipartRequestEntity(parts.toArray(new org.apache.commons.httpclient.methods.multipart.Part[parts.size()]), method.getParams()); } private MultipartRequestEntity getEntity() throws IOException { if (entity==null) { ArrayList<org.apache.commons.httpclient.methods.multipart.Part> parts = new ArrayList<org.apache.commons.httpclient.methods.multipart.Part>(); provider.writeParts(new PartWriterAdapter(parts)); entity = new MultipartRequestEntity(parts.toArray(new org.apache.commons.httpclient.methods.multipart.Part[parts.size()]), method.getParams()); } return entity; } @Override public boolean isRepeatable() { try { return getEntity().isRepeatable(); } catch (IOException ex) { return true; } } @Override public void writeRequest(OutputStream out) throws IOException { getEntity().writeRequest(out); } @Override public long getContentLength() { try { return getEntity().getContentLength(); } catch (IOException ex) { return 0; } } @Override public String getContentType() { try { return getEntity().getContentType(); } catch (IOException ex) { return ""; } } } }