/* * 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; } }