org.orbeon.oxf.resources.handler.HTTPURLConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.orbeon.oxf.resources.handler.HTTPURLConnection.java

Source

/**
 * Copyright (C) 2012 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 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 Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.resources.handler;

import org.apache.http.*;
import org.apache.http.auth.*;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParamBean;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.properties.Properties;
import org.orbeon.oxf.properties.PropertySet;
import org.orbeon.oxf.util.Connection;
import org.orbeon.oxf.util.StringConversions;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.*;

public class HTTPURLConnection extends URLConnection {

    public static String STALE_CHECKING_ENABLED_PROPERTY = "oxf.http.stale-checking-enabled";
    public static String SO_TIMEOUT_PROPERTY = "oxf.http.so-timeout";
    public static String PROXY_HOST_PROPERTY = "oxf.http.proxy.host";
    public static String PROXY_PORT_PROPERTY = "oxf.http.proxy.port";
    public static String SSL_HOSTNAME_VERIFIER = "oxf.http.ssl.hostname-verifier";
    public static String SSL_KEYSTORE_URI = "oxf.http.ssl.keystore.uri";
    public static String SSL_KEYSTORE_PASSWORD = "oxf.http.ssl.keystore.password";
    public static String PROXY_SSL_PROPERTY = "oxf.http.proxy.use-ssl";
    public static String PROXY_USERNAME_PROPERTY = "oxf.http.proxy.username";
    public static String PROXY_PASSWORD_PROPERTY = "oxf.http.proxy.password";
    public static String PROXY_NTLM_HOST_PROPERTY = "oxf.http.proxy.ntlm.host";
    public static String PROXY_NTLM_DOMAIN_PROPERTY = "oxf.http.proxy.ntlm.domain";

    // Use a single shared connection manager so we can have efficient connection pooling
    private static ClientConnectionManager connectionManager;
    private static HttpParams httpParams;
    private static PreemptiveAuthHttpRequestInterceptor preemptiveAuthHttpRequestInterceptor = new PreemptiveAuthHttpRequestInterceptor();
    private static AuthState proxyAuthState = null;

    static {
        final BasicHttpParams basicHttpParams = new BasicHttpParams();
        // Remove limit on the number of connections per host
        ConnManagerParams.setMaxConnectionsPerRoute(basicHttpParams, new ConnPerRouteBean(Integer.MAX_VALUE));
        // Remove limit on the number of max connections
        ConnManagerParams.setMaxTotalConnections(basicHttpParams, Integer.MAX_VALUE);

        // Set parameters per as configured in the properties
        final HttpConnectionParamBean paramBean = new HttpConnectionParamBean(basicHttpParams);
        final PropertySet propertySet = Properties.instance().getPropertySet();
        paramBean.setStaleCheckingEnabled(propertySet.getBoolean(STALE_CHECKING_ENABLED_PROPERTY, true));
        paramBean.setSocketBufferSize(propertySet.getInteger(SO_TIMEOUT_PROPERTY, 0));

        // Create SSL context, based on a custom key store if specified
        final SSLContext sslcontext;
        try {
            final String keyStoreURI = propertySet.getStringOrURIAsString(SSL_KEYSTORE_URI);
            final String keyStorePassword = propertySet.getString(SSL_KEYSTORE_PASSWORD);
            if (keyStoreURI != null) {
                sslcontext = SSLContext.getInstance("TLS");

                final URL url = new URL(null, keyStoreURI);
                final InputStream is = url.openStream();

                sslcontext.init(null, new TrustManager[] { KeyStoreTrustManager.apply(is, keyStorePassword) },
                        null);
            } else {
                sslcontext = SSLContext.getInstance("Default");
            }
        } catch (Exception e) {
            throw new OXFException(e);
        }

        // Create SSL hostname verifier
        final String hostnameVerifierProperty = propertySet.getString(SSL_HOSTNAME_VERIFIER, "strict");
        final X509HostnameVerifier hostnameVerifier = "browser-compatible".equals(hostnameVerifierProperty)
                ? SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
                : "allow-all".equals(hostnameVerifierProperty) ? SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER
                        : SSLSocketFactory.STRICT_HOSTNAME_VERIFIER;

        // Declare schemes (though having to declare common schemes like HTTP and HTTPS seems wasteful)
        final SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));

        final SSLSocketFactory sslSocketFactory = new SSLSocketFactory(sslcontext, hostnameVerifier);
        schemeRegistry.register(new Scheme("https", 443, sslSocketFactory));

        connectionManager = new ThreadSafeClientConnManager(basicHttpParams, schemeRegistry);

        // Set proxy if defined in properties
        final String proxyHost = Properties.instance().getPropertySet().getString(PROXY_HOST_PROPERTY);
        final Integer proxyPort = Properties.instance().getPropertySet().getInteger(PROXY_PORT_PROPERTY);
        if (proxyHost != null && proxyPort != null) {
            final boolean useTLS = Properties.instance().getPropertySet().getBoolean(PROXY_SSL_PROPERTY, false);
            basicHttpParams.setParameter(ConnRoutePNames.DEFAULT_PROXY,
                    new HttpHost(proxyHost, proxyPort, useTLS ? "https" : "http"));

            // Proxy authentication
            final String proxyUsername = Properties.instance().getPropertySet().getString(PROXY_USERNAME_PROPERTY);
            final String proxyPassword = Properties.instance().getPropertySet().getString(PROXY_PASSWORD_PROPERTY);
            if (proxyUsername != null && proxyPassword != null) {
                final String ntlmHost = Properties.instance().getPropertySet().getString(PROXY_NTLM_HOST_PROPERTY);
                final String ntlmDomain = Properties.instance().getPropertySet()
                        .getString(PROXY_NTLM_DOMAIN_PROPERTY);
                final Credentials proxyCredentials = ntlmHost != null && ntlmDomain != null
                        ? new NTCredentials(proxyUsername, proxyPassword, ntlmHost, ntlmDomain)
                        : new UsernamePasswordCredentials(proxyUsername, proxyPassword);
                proxyAuthState = new AuthState();
                proxyAuthState.setCredentials(proxyCredentials);
            }
        }

        // Save HTTP parameters which we'll need when instantiating an HttpClient (even though it could get the
        // parameters from the connection manager)
        httpParams = basicHttpParams;
    }

    private CookieStore cookieStore;

    private URL url;
    private boolean connected = false;
    private HttpUriRequest method;
    private HttpResponse httpResponse = null;
    private byte[] requestBody;
    private Map<String, String[]> requestProperties = new LinkedHashMap<String, String[]>(); // LinkedHashMap<String lowercaseHeaderName, String[] headerValues>
    private HashMap<String, List<String>> responseHeaders;

    private String username;
    private String password;
    private String preemptiveAuthentication;
    private String domain;

    public HTTPURLConnection(URL url) {
        super(url);
        this.url = url;
    }

    public void setRequestMethod(String methodName) throws ProtocolException {
        if (connected)
            throw new ProtocolException("Can't reset method: already connected");
        if ("GET".equals(methodName))
            method = new HttpGet(url.toString());
        else if ("POST".equals(methodName))
            method = new HttpPost(url.toString());
        else if ("HEAD".equals(methodName))
            method = new HttpHead(url.toString());
        else if ("OPTIONS".equals(methodName))
            method = new HttpOptions(url.toString());
        else if ("PUT".equals(methodName))
            method = new HttpPut(url.toString());
        else if ("DELETE".equals(methodName))
            method = new HttpDelete(url.toString());
        else if ("TRACE".equals(methodName))
            method = new HttpTrace(url.toString());
        else
            throw new ProtocolException("Method " + methodName + " not supported");
    }

    public CookieStore getCookieStore() {
        return cookieStore;
    }

    public void setCookieStore(CookieStore cookieStore) {
        this.cookieStore = cookieStore;
    }

    public void connect() throws IOException {
        if (!connected) {
            final String userInfo = url.getUserInfo();
            final boolean isAuthenticationRequestedWithUsername = username != null && !username.equals("");

            // Create the HTTP client and HTTP context for the client (we expect this to be fairly lightweight)
            final DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, httpParams);
            final HttpContext httpContext = new BasicHttpContext();

            // Set cookie store, creating a new one if none was provided to us
            if (cookieStore == null)
                cookieStore = new BasicCookieStore();
            httpClient.setCookieStore(cookieStore);

            // Set proxy and host authentication
            if (proxyAuthState != null)
                httpContext.setAttribute(ClientContext.PROXY_AUTH_STATE, proxyAuthState);
            if (userInfo != null || isAuthenticationRequestedWithUsername) {

                // Make authentication preemptive; interceptor is added first, as the Authentication header is added
                // by HttpClient's RequestTargetAuthentication which is itself an interceptor, so our interceptor
                // needs to run before RequestTargetAuthentication, otherwise RequestTargetAuthentication won't find
                // the appropriate AuthState/AuthScheme/Credentials in the HttpContext

                // Don't add the interceptor if we don't want preemptive authentication!
                if (!"false".equals(preemptiveAuthentication)) {
                    httpClient.addRequestInterceptor(preemptiveAuthHttpRequestInterceptor, 0);
                }

                CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
                httpContext.setAttribute(ClientContext.CREDS_PROVIDER, credentialsProvider);
                final AuthScope authScope = new AuthScope(url.getHost(), url.getPort());
                final Credentials credentials;
                if (userInfo != null) {
                    // Set username and optional password specified on URL
                    final int separatorPosition = userInfo.indexOf(":");
                    String username = separatorPosition == -1 ? userInfo : userInfo.substring(0, separatorPosition);
                    String password = separatorPosition == -1 ? "" : userInfo.substring(separatorPosition + 1);
                    // If the username/password contain special character, those character will be encoded, since we
                    // are getting this from a URL. Now do the decoding.
                    username = URLDecoder.decode(username, "utf-8");
                    password = URLDecoder.decode(password, "utf-8");
                    credentials = new UsernamePasswordCredentials(username, password);
                } else {
                    // Set username and password specified externally
                    credentials = domain == null
                            ? new UsernamePasswordCredentials(username, password == null ? "" : password)
                            : new NTCredentials(username, password, url.getHost(), domain);
                }
                credentialsProvider.setCredentials(authScope, credentials);
            }

            // If method has not been set, use GET
            // This can happen e.g. when this connection handler is used from URLFactory
            if (method == null)
                setRequestMethod("GET");

            // Set all headers,
            final boolean skipAuthorizationHeader = userInfo != null || username != null;
            for (final Map.Entry<String, String[]> currentEntry : requestProperties.entrySet()) {
                final String currentHeaderName = currentEntry.getKey();
                final String[] currentHeaderValues = currentEntry.getValue();
                for (final String currentHeaderValue : currentHeaderValues) {
                    // Skip over Authorization header if user authentication specified
                    if (skipAuthorizationHeader && currentHeaderName.toLowerCase()
                            .equals(Connection.AUTHORIZATION_HEADER.toLowerCase()))
                        continue;
                    method.addHeader(currentHeaderName, currentHeaderValue);
                }
            }

            // Create request entity with body
            if (method instanceof HttpEntityEnclosingRequest) {

                // Use the body that was set directly, or the result of writing to the OutputStream
                final byte[] body = (requestBody != null) ? requestBody : (os != null) ? os.toByteArray() : null;

                if (body != null) {
                    final Header contentTypeHeader = method.getFirstHeader("Content-Type"); // Header names are case-insensitive for comparison
                    if (contentTypeHeader == null)
                        throw new ProtocolException("Can't set request entity: Content-Type header is missing");
                    final ByteArrayEntity byteArrayEntity = new ByteArrayEntity(body);
                    byteArrayEntity.setContentType(contentTypeHeader);
                    ((HttpEntityEnclosingRequest) method).setEntity(byteArrayEntity);
                }
            }

            // Make request
            httpResponse = httpClient.execute(method, httpContext);
            connected = true;
        }
    }

    public InputStream getInputStream() throws IOException {
        if (!connected)
            connect();
        final HttpEntity entity = httpResponse.getEntity();
        if (entity != null)
            return entity.getContent();
        else
            return new InputStream() {
                @Override
                public int read() throws IOException {
                    return -1;
                }
            };
    }

    public void setRequestBody(byte[] requestBody) throws IOException {
        this.requestBody = requestBody;
    }

    private ByteArrayOutputStream os = null;

    @Override
    public OutputStream getOutputStream() throws IOException {
        if (os == null)
            os = new ByteArrayOutputStream();

        return os;
    }

    private void initResponseHeaders() {
        try {
            if (!connected)
                connect();
            if (responseHeaders == null) {
                responseHeaders = new HashMap<String, List<String>>();
                for (Header header : httpResponse.getAllHeaders())
                    responseHeaders.put(header.getName().toLowerCase(),
                            Collections.singletonList(header.getValue()));
            }
        } catch (IOException e) {
            throw new OXFException(e);
        }
    }

    /**
     * This method will be called by URLConnection.getLastModified(), URLConnection.getContentLength(), etc.
     */
    @Override
    public String getHeaderField(String name) {
        initResponseHeaders();
        // We return the first header value only. This is not really right, is it? But it will work for the few calls
        // done by URLConnection.
        final List<String> values = responseHeaders.get(name);
        return (values != null) ? values.get(0) : null;
    }

    @Override
    public Map<String, List<String>> getHeaderFields() {
        initResponseHeaders();
        return responseHeaders;
    }

    @Override
    public void setRequestProperty(String key, String value) {
        super.setRequestProperty(key, value);
        requestProperties.put(key, new String[] { value });
    }

    @Override
    public void addRequestProperty(String key, String value) {
        super.addRequestProperty(key, value);
        StringConversions.addValueToStringArrayMap(requestProperties, key, value);
    }

    @Override
    public String getRequestProperty(String key) {
        // Not sure what should be returned so return the first value if any. But likely nobody is calling this method.
        final String[] values = requestProperties.get(key);
        return (values == null) ? null : values[0];
    }

    @Override
    public Map<String, List<String>> getRequestProperties() {
        return super.getRequestProperties();
    }

    public int getResponseCode() {
        return httpResponse.getStatusLine().getStatusCode();
    }

    public void disconnect() {
        try {
            // "As of version 4.1 one should be using EntityUtils#consume() instead."
            // http://mail-archives.apache.org/mod_mbox/hc-httpclient-users/201012.mbox/%3C1291222613.3781.68.camel@ubuntu%3E
            EntityUtils.consume(httpResponse.getEntity());
        } catch (IOException e) {
            throw new OXFException(e);
        }
    }

    public void setUsername(String username) {
        this.username = username.trim();
    }

    public void setPassword(String password) {
        this.password = password.trim();
    }

    public void setDomain(String domain) {
        this.domain = domain.trim();
    }

    public void setPreemptiveAuthentication(String preemptiveAuthentication) {
        this.preemptiveAuthentication = preemptiveAuthentication;
    }

    @Override
    public long getLastModified() {
        // Default implementation throws an exception if the header is not present, so optimize on calling side
        final String field = getHeaderField("last-modified");
        return (field != null) ? super.getLastModified() : 0;
    }
}