/*
* Copyright 1998-2009 University Corporation for Atmospheric Research/Unidata
*
* Portions of this software were developed by the Unidata Program at the
* University Corporation for Atmospheric Research.
*
* Access and use of this software shall impose the following obligations
* and understandings on the user. The user is granted the right, without
* any fee or cost, to use, copy, modify, alter, enhance and distribute
* this software, and any derivative works thereof, and its supporting
* documentation for any purpose whatsoever, provided that this entire
* notice appears in all copies of the software, derivative works and
* supporting documentation. Further, UCAR requests that the user credit
* UCAR/Unidata in any publications that result from the use of this
* software or in any product that includes this software. The names UCAR
* and/or Unidata, however, may not be used in any advertising or publicity
* to endorse or promote any products or commercial entity unless specific
* written permission is obtained from UCAR/Unidata. The user also
* understands that UCAR/Unidata is not obligated to provide the user with
* any support, consulting, training or assistance of any kind with regard
* to the use, operation and performance of this software nor to provide
* the user with any updates, revisions, new versions or "bug fixes."
*
* THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL,
* INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
* FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
* WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package ucar.httpservices;
import net.jcip.annotations.NotThreadSafe;
import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.DeflateDecompressingEntity;
import org.apache.http.client.entity.GzipDecompressingEntity;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.params.AllClientPNames;
import org.apache.http.client.protocol.*;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.SyncBasicHttpParams;
import org.apache.http.protocol.*;
import org.apache.http.entity.StringEntity;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.util.*;
import static org.apache.http.auth.AuthScope.*;
import static ucar.httpservices.HTTPAuthScope.*;
/**
* A session is encapsulated in an instance of the class
* HTTPSession. The encapsulation is with respect to a specific url
* This means that once a session is
* specified, it is tied permanently to that url.
*
*
* It is important to note that Session objects do NOT correspond
* with the HttpClient objects of the Apache httpclient library.
* A Session does, however, encapsulate an instance of an Apache HttpClient.
*
* It is possible to specify a url when invoking, for example,
* HTTPFactory.Get. This is because the url argument to the
* HTTPSession constructor actually serves two purposes. First, if
* the method is created without specifying a url, then the session
* url is used to specify the data to be retrieved by the method
* invocation. Second, if the method is created and specifies a
* url, for example, HTTPMethod m = HTTPFactory.Get(session,url2);
* this second url is used to specify the data to be retrieved by
* the method invocation. This might (and does) occur if, for
* example, the url given to HTTPSession represented some general
* url such as http://motherlode.ucar.edu/path/file.nc and the url
* given to HTTPFactory.Get was for something more specific such as
* http://motherlode.ucar.edu/path/file.nc.dds.
*
* The important point is that in this second method, the url must
* be "compatible" with the session url. The term "compatible"
* basically means that the HTTPSession url, as a string, must be a
* prefix of the url given to HTTPFactory.Get. This maintains the
* semantics of the Session but allows flexibility in accessing data
* from the server.
*
* Note that the term legalurl means that the url has reserved
* characters within identifieers in escaped form. This is
* particularly and issue for queries. Especially: ?x[0:5] is legal
* and the square brackets need not be encoded.
*
* Finally, note that if the session was created with no url then all method
* constructions must specify a url.
*/
@NotThreadSafe
public class HTTPSession implements AutoCloseable
{
//////////////////////////////////////////////////
// Constants
// Define all the legal properties
// From class AllClientPNames
// Use aliases because in httpclietn 4.3, AllClientPNames is deprecated
static public final String ALLOW_CIRCULAR_REDIRECTS = AllClientPNames.ALLOW_CIRCULAR_REDIRECTS;
static public final String HANDLE_REDIRECTS = AllClientPNames.HANDLE_REDIRECTS;
static public final String HANDLE_AUTHENTICATION = AllClientPNames.HANDLE_AUTHENTICATION;
static public final String MAX_REDIRECTS = AllClientPNames.MAX_REDIRECTS;
static public final String SO_TIMEOUT = AllClientPNames.SO_TIMEOUT;
static public final String CONN_TIMEOUT = AllClientPNames.CONNECTION_TIMEOUT;
static public final String USER_AGENT = AllClientPNames.USER_AGENT;
static public final String PROXY = AllClientPNames.DEFAULT_PROXY;
static public final String COMPRESSION = "COMPRESSION";
// from: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
static final public String HEADER_USERAGENT = "User-Agent";
static final public String ACCEPT_ENCODING = "Accept-Encoding";
static final public String BASIC = HTTPAuthPolicy.BASIC;
static final public String DIGEST = HTTPAuthPolicy.DIGEST;
static final public String NTLM = HTTPAuthPolicy.NTLM;
static final public String SSL = HTTPAuthPolicy.SSL;
static final int DFALTTHREADCOUNT = 50;
static final int DFALTREDIRECTS = 25;
static final int DFALTCONNTIMEOUT = 1 * 60 * 1000; // 1 minutes (60000 milliseconds)
static final int DFALTSOTIMEOUT = 5 * 60 * 1000; // 5 minutes (300000 milliseconds)
static final String DFALTUSERAGENT = "/NetcdfJava/HttpClient4.3";
//////////////////////////////////////////////////////////////////////////
// Type Declarations
// Provide an alias for HttpParams
static class Settings extends SyncBasicHttpParams
{
}
static class Proxy
{
public String host = null;
public int port = -1;
}
static enum Methods
{
Get("get"), Head("head"), Put("put"), Post("post"), Options("options");
private final String name;
Methods(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
// Define a Retry Handler that supports specifiable retries
// and is optionally verbose.
static public class RetryHandler
implements org.apache.http.client.HttpRequestRetryHandler
{
static final int DFALTRETRIES = 5;
static int retries = DFALTRETRIES;
static boolean verbose = false;
public RetryHandler()
{
}
public boolean
retryRequest(IOException exception,
int executionCount,
HttpContext context)
{
if(getVerbose()) {
HTTPSession.log.debug(String.format("Retry: count=%d exception=%s", executionCount, exception.toString()));
}
synchronized (RetryHandler.class) {
if(executionCount >= retries)
return false;
}
if((exception instanceof InterruptedIOException) // Timeout
|| (exception instanceof UnknownHostException)
|| (exception instanceof ConnectException) // connection refused
|| (exception instanceof SSLException)) // ssl handshake problem
return false;
HttpRequest request
= (HttpRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST);
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if(idempotent) // Retry if the request is considered idempotent
return true;
return false;
}
static public synchronized int getRetries()
{
return RetryHandler.retries;
}
static public synchronized void setRetries(int retries)
{
RetryHandler.retries = retries;
}
static public synchronized boolean getVerbose()
{
return RetryHandler.verbose;
}
static public synchronized void setVerbose(boolean tf)
{
RetryHandler.verbose = tf;
}
}
static class GZIPResponseInterceptor implements HttpResponseInterceptor
{
public void process(final HttpResponse response, final HttpContext context)
throws HttpException, IOException
{
HttpEntity entity = response.getEntity();
if(entity != null) {
Header ceheader = entity.getContentEncoding();
if(ceheader != null) {
HeaderElement[] codecs = ceheader.getElements();
for(HeaderElement h : codecs) {
if(h.getName().equalsIgnoreCase("gzip")) {
response.setEntity(new GzipDecompressingEntity(response.getEntity()));
return;
}
}
}
}
}
}
static class DeflateResponseInterceptor implements HttpResponseInterceptor
{
public void process(final HttpResponse response, final HttpContext context)
throws HttpException, IOException
{
HttpEntity entity = response.getEntity();
if(entity != null) {
Header ceheader = entity.getContentEncoding();
if(ceheader != null) {
HeaderElement[] codecs = ceheader.getElements();
for(HeaderElement h : codecs) {
if(h.getName().equalsIgnoreCase("deflate")) {
response.setEntity(new DeflateDecompressingEntity(response.getEntity()));
return;
}
}
}
}
}
}
////////////////////////////////////////////////////////////////////////
// Static variables
static public org.slf4j.Logger log
= org.slf4j.LoggerFactory.getLogger(HTTPSession.class);
static PoolingClientConnectionManager connmgr;
// Define a set of settings to hold all the
// settable values; there will be one
// instance for global and one for local.
static Settings globalsettings;
static List reqintercepts = new ArrayList();
static List rspintercepts = new ArrayList();
static {
connmgr = new PoolingClientConnectionManager();
connmgr.getSchemeRegistry().register(
new Scheme("https", 8443,
new CustomSSLProtocolSocketFactory()));
connmgr.getSchemeRegistry().register(
new Scheme("https", 443,
new CustomSSLProtocolSocketFactory()));
globalsettings = new Settings();
setDefaults(globalsettings);
setGlobalUserAgent(DFALTUSERAGENT);
setGlobalThreadCount(DFALTTHREADCOUNT);
setGlobalConnectionTimeout(DFALTCONNTIMEOUT);
setGlobalSoTimeout(DFALTSOTIMEOUT);
getGlobalProxyD(); // get info from -D if possible
setGlobalKeyStore();
}
//////////////////////////////////////////////////////////////////////////
// Static Methods (Mostly global accessors)
/// Provide defaults for a settings map
static void setDefaults(Settings props)
{
if(false) {// turn off for now
props.setParameter(HANDLE_REDIRECTS, Boolean.TRUE);
props.setParameter(HANDLE_AUTHENTICATION, Boolean.TRUE);
}
props.setParameter(ALLOW_CIRCULAR_REDIRECTS, Boolean.TRUE);
props.setParameter(MAX_REDIRECTS, (Integer) DFALTREDIRECTS);
props.setParameter(SO_TIMEOUT, (Integer) DFALTSOTIMEOUT);
props.setParameter(CONN_TIMEOUT, (Integer) DFALTCONNTIMEOUT);
props.setParameter(USER_AGENT, DFALTUSERAGENT);
}
static synchronized public Settings getGlobalSettings()
{
return globalsettings;
}
static synchronized public void setGlobalUserAgent(String userAgent)
{
globalsettings.setParameter(USER_AGENT, userAgent);
}
static synchronized public String getGlobalUserAgent()
{
return (String) globalsettings.getParameter(USER_AGENT);
}
static synchronized public void setGlobalThreadCount(int nthreads)
{
connmgr.setMaxTotal(nthreads);
connmgr.setDefaultMaxPerRoute(nthreads);
}
// Alias
static public void setGlobalMaxConnections(int nthreads)
{
setGlobalThreadCount(nthreads);
}
static synchronized public int getGlobalThreadCount()
{
return connmgr.getMaxTotal();
}
static synchronized public List getGlobalCookies()
{
AbstractHttpClient client = new DefaultHttpClient(connmgr);
List cookies = client.getCookieStore().getCookies();
return cookies;
}
// Timeouts
static synchronized public void setGlobalConnectionTimeout(int timeout)
{
if(timeout >= 0) globalsettings.setParameter(CONN_TIMEOUT, (Integer) timeout);
}
static synchronized public void setGlobalSoTimeout(int timeout)
{
if(timeout >= 0) globalsettings.setParameter(SO_TIMEOUT, (Integer) timeout);
}
// Proxy
static synchronized public void
setGlobalProxy(String host, int port)
{
Proxy proxy = new Proxy();
proxy.host = host;
proxy.port = port;
globalsettings.setParameter(PROXY, proxy);
}
// Misc.
static synchronized public void
setGlobalCompression()
{
globalsettings.setParameter(COMPRESSION, "gzip,deflate");
HttpResponseInterceptor hrsi = new GZIPResponseInterceptor();
rspintercepts.add(hrsi);
hrsi = new DeflateResponseInterceptor();
rspintercepts.add(hrsi);
}
// Authorization
static synchronized protected void
defineCredentialsProvider(String principal, AuthScope scope, CredentialsProvider provider, HTTPAuthStore store)
{
// Add/remove entry to AuthStore
try {
if(provider == null) {//remove
store.remove(new HTTPAuthStore.Entry(principal, scope, provider));
} else { // add
store.insert(new HTTPAuthStore.Entry(principal, scope, provider));
}
} catch (HTTPException he) {
log.error("HTTPSession.setCredentialsProvider failed");
}
}
static public void
setGlobalCredentialsProvider(AuthScope scope, CredentialsProvider provider)
{
defineCredentialsProvider(ANY_PRINCIPAL, scope, provider, HTTPAuthStore.getDefault());
}
static public void |