/*
* Copyright (c) 2009-2011 Lockheed Martin Corporation
*
* Licensed 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 org.eurekastreams.server.service.actions.strategies.links;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import javax.imageio.ImageIO;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Used to cache HTTP requests from the server and for testability.
*/
public class ConnectionFacade
{
/**
* Max value for timeout.
*/
private static final Integer MIN_TIMEOUT = 0;
/**
* Min value for timeout.
*/
private static final Integer MAX_TIMEOUT = 30000;
/**
* Logger.
*/
private final Log log = LogFactory.getLog(ConnectionFacade.class);
/**
* Trust manager.
*/
private final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager()
{
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers()
{
return null;
}
@Override
public void checkClientTrusted(final java.security.cert.X509Certificate[] certs, final String authType)
{
}
@Override
public void checkServerTrusted(final java.security.cert.X509Certificate[] certs, final String authType)
{
}
} };
/**
* Cache of URLs. Uses WeakHashMap to prevent memory leak.
*/
private final Map<String, URL> urlMap = new WeakHashMap<String, URL>();
/**
* Cache of Image Dimensions. Uses WeakHashMap to prevent memory leak.
*/
private final Map<String, ImageDimensions> imgMap = new WeakHashMap<String, ImageDimensions>();
/**
* Max time for connections.
*/
private int connectionTimeOut = MIN_TIMEOUT;
/**
* The proxy host.
*/
private String proxyHost = "";
/**
* The proxy port.
*/
private String proxyPort = "";
/**
* HTTP redirect codes.
*/
private List<Integer> redirectCodes = new ArrayList<Integer>();
/**
* List of decorators that can add headers to the connection.
*/
private final List<ConnectionFacadeDecorator> decorators;
/** Buffer size to use for downloading files; should be sightly larger than the typical file size. */
private int expectedDownloadFileLimit;
/** Maximum allowable size for downloaded files (to prevent DoS via out of memory). */
private int maximumDownloadFileLimit;
/**
* Constructor.
*
* @param inDecorators
* - List of ConnectionFacadeDecorator instances.
*/
public ConnectionFacade(final List<ConnectionFacadeDecorator> inDecorators)
{
decorators = inDecorators;
}
/**
* @return the redirectCodes
*/
public final List<Integer> getRedirectCodes()
{
return redirectCodes;
}
/**
* @param inRedirectCodes
* the redirectCodes to set
*/
public final void setRedirectCodes(final List<Integer> inRedirectCodes)
{
redirectCodes = inRedirectCodes;
}
/**
* @return the proxyPort
*/
public final String getProxyPort()
{
return proxyPort;
}
/**
* @param inProxyPort
* the proxyPort to set
*/
public final void setProxyPort(final String inProxyPort)
{
proxyPort = inProxyPort;
}
/**
* @return the proxyHost
*/
public final String getProxyHost()
{
return proxyHost;
}
/**
* @param inProxyHost
* the proxyHost to set
*/
public final void setProxyHost(final String inProxyHost)
{
proxyHost = inProxyHost;
}
/**
* @return the connectionTimeOut
*/
public final int getConnectionTimeOut()
{
return connectionTimeOut;
}
/**
* @param inConnectionTimeOut
* the connectionTimeOut to set
*/
public final void setConnectionTimeOut(final int inConnectionTimeOut)
{
// the following takes care of input validation
if (inConnectionTimeOut < MIN_TIMEOUT || inConnectionTimeOut > MAX_TIMEOUT)
{
throw new InvalidParameterException("Connection timeout must be between " + MIN_TIMEOUT + " and "
+ MAX_TIMEOUT);
}
connectionTimeOut = inConnectionTimeOut;
}
/**
* Download a file.
*
* @param url
* the URL as a string.
* @param inAccountId
* accountid of the user making the request.
* @return the file as a string.
* @throws IOException
* if URL can't be opened.
*/
public String downloadFile(final String url, final String inAccountId) throws IOException
{
char[] buffer = new char[expectedDownloadFileLimit];
Reader reader = getConnectionReader(url, inAccountId);
try
{
// Note: The Reader.read javadocs say that "this method will block until some input is available, an I/O
// error occurs, or the end of the stream is reached," so if it returns with less characters than would fit
// in the buffer, I can't know whether that's the end of the response (EOF) or just a delay in receiving
// packets over the network. As such, I need to read until it returns -1 (EOF) to know I have the entire
// response.
// first just read into the buffer
int charsRead = 0;
while (charsRead < buffer.length)
{
int thisRead = reader.read(buffer, charsRead, buffer.length - charsRead);
if (thisRead < 0)
{
return new String(buffer, 0, charsRead);
}
charsRead += thisRead;
}
// filled the buffer, now use a StringBuilder
StringBuilder builder = new StringBuilder();
do
{
if (builder.length() + charsRead > maximumDownloadFileLimit)
{
throw new IOException("Downloaded file too large.");
}
builder.append(buffer, 0, charsRead);
charsRead = reader.read(buffer);
}
while (charsRead >= 0);
return builder.toString();
}
finally
{
reader.close();
}
}
/**
* Returns an input reader for downloading a file from an HTTP connection. (A separate method for unit testing.)
*
* @param url
* URL from which to download a file.
* @param inAccountId
* Accountid of the user making the request.
* @return Reader for downloading a file via HTTP.
* @throws IOException
* If URL can't be opened.
*/
protected Reader getConnectionReader(final String url, final String inAccountId) throws IOException
{
return new InputStreamReader(getConnection(url, inAccountId).getInputStream());
}
/**
* Get the height of an image by URL.
*
* @param url
* the url.
* @param inAccountId
* account id of the user making the request.
* @return the image height.
* @throws IOException
* on bad url.
*/
public int getImgHeight(final String url, final String inAccountId) throws IOException
{
ImageDimensions dimensions = getImgDimensions(url, inAccountId);
return dimensions.getHeight();
}
/**
* Get the width of an image by URL.
*
* @param url
* the url.
* @param inAccountId
* account id of the user making the request.
* @return the image height.
* @throws IOException
* on bad url.
*/
public int getImgWidth(final String url, final String inAccountId) throws IOException
{
ImageDimensions dimensions = getImgDimensions(url, inAccountId);
return dimensions.getHeight();
}
/**
* Get the connection.
*
* @param inUrl
* the url.
* @param inAccountId
* account id of the user making the request.
* @return the connection.
* @throws MalformedURLException
* If the URL is invalid.
*/
protected HttpURLConnection getConnection(final String inUrl, final String inAccountId)
throws MalformedURLException
{
HttpURLConnection connection = null;
if (proxyHost.length() > 0)
{
System.setProperty("https.proxyHost", proxyHost);
System.setProperty("https.proxyPort", proxyPort);
}
log.info("Using Proxy: " + proxyHost + ":" + proxyPort);
URL url = getUrl(inUrl);
try
{
// Some sites e.g. Google Images and Digg will not respond to an unrecognized User-Agent.
if ("https".equals(url.getProtocol()))
{
log.trace("Using HTTPS");
// Install the all-trusting trust manager
try
{
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
log.trace("Installed all-trusting trust manager.");
}
catch (Exception e)
{
log.error("Error setting SSL Context");
}
connection = (HttpsURLConnection) url.openConnection();
((HttpsURLConnection) connection).setHostnameVerifier(new HostnameVerifier()
{
@Override
public boolean verify(final String hostname, final SSLSession session)
{
log.trace("Accepting host name.");
return true;
}
});
}
else
{
URLConnection plainConnection = url.openConnection();
if (plainConnection instanceof HttpURLConnection)
{
log.trace("Using HTTP");
connection = (HttpURLConnection) plainConnection;
}
else
{
log.info("Closing non-HTTP connection.");
return null;
}
}
connection.setConnectTimeout(connectionTimeOut);
// Decorate the connection.
for (ConnectionFacadeDecorator decorator : decorators)
{
decorator.decorate(connection, inAccountId);
}
}
catch (Exception e)
{
log.error("caught exception: ", e);
}
return connection;
}
/**
* Get the dimensions of an image by URL.
*
* @param url
* the url.
* @param inAccountId
* account id of the user making the request.
* @return the image height.
* @throws IOException
* on bad url.
*/
private ImageDimensions getImgDimensions(final String url, final String inAccountId) throws IOException
{
if (!imgMap.containsKey(url))
{
log.info("Downloading image: " + url);
InputStream connectionStream = getConnection(url, inAccountId).getInputStream();
if (null != connectionStream)
{
BufferedImage img = ImageIO.read(connectionStream);
imgMap.put(url, new ImageDimensions(img.getHeight(), img.getWidth()));
}
else
{
imgMap.put(url, new ImageDimensions(0, 0));
}
}
return imgMap.get(url);
}
/**
* Dimensions of an image.
*/
protected class ImageDimensions
{
/**
* Width.
*/
private final int width;
/**
* Height.
*/
private final int height;
/**
* Constructor.
*
* @param inHeight
* height.
* @param inWidth
* width.
*/
public ImageDimensions(final int inHeight, final int inWidth)
{
height = inHeight;
width = inWidth;
}
/**
* @return the height.
*/
public int getHeight()
{
return height;
}
/**
* @return the width.
*/
public int getWidth()
{
return width;
}
}
/**
* Get the file host.
*
* @param url
* the url.
* @return the host.
*
* @throws MalformedURLException
* on bad URL.
*/
public String getHost(final String url) throws MalformedURLException
{
URL theUrl = getUrl(url);
return theUrl.getHost();
}
/**
* Get the protocol of a URL.
*
* @param url
* the url.
* @return the protocol.
*
* @throws MalformedURLException
* on bad URL.
*/
public String getProtocol(final String url) throws MalformedURLException
{
URL theUrl = getUrl(url);
return theUrl.getProtocol();
}
/**
* Get the path of a URL.
*
* @param url
* the url.
* @return the protocol.
*
* @throws MalformedURLException
* on bad URL.
*/
public String getPath(final String url) throws MalformedURLException
{
URL theUrl = getUrl(url);
return theUrl.getPath();
}
/**
* Used to cache URLs.
*
* @param url
* the url as a String.
* @return the URL object.
* @throws MalformedURLException
* on bad URL.
*/
protected URL getUrl(final String url) throws MalformedURLException
{
if (!urlMap.containsKey(url))
{
urlMap.put(url, new URL(url));
}
return urlMap.get(url);
}
/**
* Get the final URL after redirect.
*
* @param url
* the initial url.
* @param inAccountId
* account id of the user making the request.
* @return the final url.
* @throws IOException
* shouldn't happen.
*/
public String getFinalUrl(final String url, final String inAccountId) throws IOException
{
HttpURLConnection connection = getConnection(url, inAccountId);
if (connection != null && redirectCodes.contains(connection.getResponseCode()))
{
String redirUrl = connection.getHeaderField("Location");
log.trace("Found redirect header to: " + redirUrl);
// Check for protocol change
if (redirUrl.startsWith("http://"))
{
log.trace("Changing protocol to HTTP");
return redirUrl;
}
}
return url;
}
/**
* @return the expectedDownloadFileLimit
*/
public int getExpectedDownloadFileLimit()
{
return expectedDownloadFileLimit;
}
/**
* @param inExpectedDownloadFileLimit
* the expectedDownloadFileLimit to set
*/
public void setExpectedDownloadFileLimit(final int inExpectedDownloadFileLimit)
{
expectedDownloadFileLimit = inExpectedDownloadFileLimit;
}
/**
* @return the maximumDownloadFileLimit
*/
public int getMaximumDownloadFileLimit()
{
return maximumDownloadFileLimit;
}
/**
* @param inMaximumDownloadFileLimit
* the maximumDownloadFileLimit to set
*/
public void setMaximumDownloadFileLimit(final int inMaximumDownloadFileLimit)
{
maximumDownloadFileLimit = inMaximumDownloadFileLimit;
}
}