//: "The contents of this file are subject to the Mozilla Public License //: Version 1.1 (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.mozilla.org/MPL/ //: //: Software distributed under the License is distributed on an "AS IS" //: basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the //: License for the specific language governing rights and limitations //: under the License. //: //: The Original Code is Guanxi (http://www.guanxi.uhi.ac.uk). //: //: The Initial Developer of the Original Code is Alistair Young alistair@codebrane.com //: All Rights Reserved. //: package org.guanxi.common; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.log4j.Logger; import org.apache.xml.security.utils.Base64; import org.guanxi.common.security.ssl.GuanxiHostVerifier; import org.guanxi.common.security.ssl.SSL; /** * Wraps either an HttpsURLConnection or HttpsURLConnection. The HttpsURLConnection * uses the org.guanxi.common.security.ssl.SSL defined custom SSL layer. * * @author Alistair Young alistair@smo.uhi.ac.uk * @author matthew */ public class EntityConnection { private static final Logger logger = Logger.getLogger(EntityConnection.class.getName()); /** * For passing to EntityConnection when we want to probe a remote entity for it's X509 Certificate */ public static final boolean PROBING_ON = true; /** * For passing to EntityConnection when we want full HTTPS functionality */ public static final boolean PROBING_OFF = false; /** * This indicates if the server certificate is being retrieved using this connection * (or if the connection is checking to ensure that the certificate is valid). */ private boolean probing = false; /** * This is the HTTP(S) connection that has been made. * To determine if this is a secure connection call * connection instanceof HttpsURLConnection. */ private HttpURLConnection connection = null; /** * Sets up a connection object with custom SSL management if required. * * @param endpoint Address of the entity to connect to. If this starts with https * the custom SSL management will be used. * @param localEntityID The ID of the local entity which will be represented by the * custom SSL layer. If the connection is not secure, this parameter can be null. * @param entityKeystore The full path of the entity's keystore * @param entityKeystorePassword Password for the entity's keystore * @param trustStore The full path to the Engine's truststore * @param trustStorePassword The password for the Engine's truststore * @param probeForServerCert TRUE if the connection is only going to be used to obtain an entity's SSL certificate * @throws GuanxiException If an error occurred */ public EntityConnection(String endpoint, String localEntityID, String entityKeystore, String entityKeystorePassword, String trustStore, String trustStorePassword, boolean probeForServerCert) throws GuanxiException { URL url; try { url = new URL(endpoint); if ( url.getProtocol().equals("https") ) { // getProtocol always returns the lower case protocol name HttpsURLConnection connection; SSLContext context; context = SSLContext.getInstance("SSL"); context.init(SSL.getKeyManagers(localEntityID, entityKeystore, entityKeystorePassword), SSL.getTrustManagers(trustStore, trustStorePassword, probeForServerCert), null); connection = (HttpsURLConnection)url.openConnection(); connection.setSSLSocketFactory(context.getSocketFactory()); connection.setHostnameVerifier(new GuanxiHostVerifier()); this.connection = connection; } else { connection = (HttpURLConnection)url.openConnection(); } probing = probeForServerCert; } catch (Exception e) { throw new GuanxiException(e); } } /** * Set the method for the URL request, one of: * GET * POST * HEAD * OPTIONS * PUT * DELETE * TRACE * are legal, subject to protocol restrictions. The default method is GET * * @param requestMethod The HTTP method * @throws GuanxiException If the connection has already been opened then this exception will be thrown. */ public void setRequestMethod(String requestMethod) throws GuanxiException { try { connection.setRequestMethod(requestMethod); } catch (ProtocolException e) { throw new GuanxiException(e); } } /** * Sets the value of the doOutput field for this EntityConnection to the specified value. * A URL connection can be used for input and/or output. Set the DoOutput flag to true * if you intend to use the EntityConnection for output, false if not. The default is false. * * @param doIt The new value * @throws GuanxiException If the connection has already been opened then this exception will be thrown. */ public void setDoOutput(boolean doIt) throws GuanxiException { try { connection.setDoOutput(doIt); } catch (IllegalStateException e) { // already connected throw new GuanxiException(e); } } /** * Opens a communications link to the resource referenced by this URL, if such a connection * has not already been established. * If the connect method is called when the connection has already been opened (indicated by * the connected field having the value true), the call is ignored. * EntityConnection objects go through two phases: first they are created, then they are connected. * After being created, and before being connected, various options can be specified * (e.g., doInput and UseCaches). After connecting, it is an error to try to set them. * Operations that depend on being connected, like getContentLength, will implicitly perform the connection, * if necessary * * @throws GuanxiException If the connection times out before being established, or if an IO issue occurs. */ public void connect() throws GuanxiException { try { connection.connect(); } catch (IOException e) { // SocketTimeoutException is a subclass of IOException throw new GuanxiException(e); } } /** * Returns an input stream that reads from this open connection. A SocketTimeoutException can be * thrown when reading from the returned input stream if the read timeout expires before data is * available for read * * @return An input stream that reads from this open connection * @throws GuanxiException If an IO issue occurs when creating the input stream * (or if the protocol does not support input, which should not happen). */ public InputStream getInputStream() throws GuanxiException { try { return connection.getInputStream(); } catch (IOException e) { logErrorStream(e, connection); throw new GuanxiException(e); } } /** * Returns an output stream that writes to this connection * * @return An output stream that writes to this connection * @throws GuanxiException If an IO issue occurs when creating the output stream * (or if the protocol does not support output, which should not happen) */ public OutputStream getOutputStream() throws GuanxiException { try { return connection.getOutputStream(); } catch (IOException e) { logErrorStream(e, connection); throw new GuanxiException(e); } } /** * Sets the general request property. If a property with the key already exists, overwrite its * value with the new value. * NOTE: HTTP requires all request properties which can legally have multiple instances with the * same key to use a comma-separated list syntax which enables multiple properties to be appended * into a single property. While this means that multiple values can be added, this will not append * values when repeatedly called with the same key. * * @param key The keyword by which the request is known (e.g., "accept"). * @param value The value associated with it * @throws GuanxiException if an error occurs */ public void setRequestProperty(String key, String value) throws GuanxiException { try { connection.setRequestProperty(key, value); } catch (Exception e) { // IllegalStateException = already connected // NullPointerException = key is null throw new GuanxiException(e); } } /** * Returns the server's certificate chain which was established as part of defining the session. * Note: This method can be used only when using certificate-based cipher suites; using it with * non-certificate-based cipher suites, such as Kerberos, will cause it to return null. * * @return An ordered array of server certificates, with the peer's own certificate first followed * by any certificate authorities. If an error occurred it will return null. * @throws GuanxiException if an error occurred */ public Certificate[] getServerCertificates() throws GuanxiException { if ( connection instanceof HttpsURLConnection ) { try { return ((HttpsURLConnection)connection).getServerCertificates(); } catch (SSLPeerUnverifiedException e) { // SSLPeerUnverifiedException = peer not verified return null; } catch (IllegalStateException e) { // IllegalStateException = called before the connection is made throw new GuanxiException(e); } } return null; } /** * Indicates that other requests to the server are unlikely in the near future. * Calling disconnect() should not imply that this HttpURLConnection instance * can be reused for other requests */ public void disconnect() { connection.disconnect(); } /** * When in probing mode, connects to the remote entity and extracts it's certificate chain * * @return An array of X509Certificate representing the remote entity's certificate chain * @throws GuanxiException If an error occurred. Will be thrown if the EntityConnection is * not in probing mode. */ public X509Certificate[] getServerCertChain() throws GuanxiException { Certificate[] certificateChain; ArrayList<X509Certificate> convertedChain; CertificateFactory factory; // We can only do this in probing mode if (!probing) { throw new GuanxiException("EntityConnection not in probing mode"); } // Get the certificate or chain the server is using... connect(); certificateChain = getServerCertificates(); disconnect(); // Did we get any certificates? if (certificateChain.length == 0) { throw new GuanxiException("No server certificates available"); } try { factory = CertificateFactory.getInstance("X509"); convertedChain = new ArrayList<X509Certificate>(); // Cycle through the server's certificate chain, converting to X509 format for ( Certificate current : certificateChain ) { convertedChain.add((X509Certificate)factory.generateCertificate(new ByteArrayInputStream(current.getEncoded()))); } return convertedChain.toArray(new X509Certificate[convertedChain.size()]); } catch(CertificateException ce) { throw new GuanxiException(ce); } } /** * When in probing mode, connects to the remote entity and extracts it's certificate. This is the * certificate in the chain that identifies the remote entity. The certificate chain is not processed. * * @return X509Certificate representing the remote entity's identity * @throws GuanxiException if an error occurred. Will be thrown if the EntityConnection is * not in probing mode. */ public X509Certificate getServerCertificate() throws GuanxiException { // According to the javadocs, the server's own certificate is first in the chain return getServerCertChain()[0]; } /** * Returns the length of the content available from the connection * * @return length of content available in bytes */ public int getContentLength() { return connection.getContentLength(); } /** * Returns content from a connection as a String * * @return String containing the content from the connection * @throws GuanxiException if an error occurs */ public String getContentAsString() throws GuanxiException { try { return new String(Utils.read(connection.getInputStream())); } catch(IOException ioe) { throw new GuanxiException(ioe); } } /** * Adds authentication information to the request. * * @param username Username * @param password Password */ public void setAuthentication(String username, String password) { String authentication; authentication = Base64.encode((username + ':' + password).getBytes()); connection.setRequestProperty("Authorization", "Basic " + authentication); } /** * Logs the message available via the error stream, if any * * @param originalException the original exception that caused the error stream to be of interest * @param connection the connection that caused the error */ private void logErrorStream(Exception originalException, HttpURLConnection connection) { InputStream errorStream = connection.getErrorStream(); if (errorStream != null) { try { String errorStreamText = new String(Utils.read(errorStream)); logger.error("==========================================================="); logger.error(connection.getURL(), originalException); logger.error(errorStreamText); logger.error("==========================================================="); errorStream.close(); } catch(Exception ex) { logger.error(ex); } } } /** * Sets the timeout for connecting to an entity * * @param milliseconds the number of milliseconds to wait before timing out */ public void setConnectTimeout(int milliseconds) { connection.setConnectTimeout(milliseconds); } /** * Sets the timeout for reading from an entity * * @param milliseconds the number of milliseconds to wait before timing out */ public void setReadTimeout(int milliseconds) { connection.setReadTimeout(milliseconds); } }