// // Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s). // All rights reserved. // package openadk.library.impl; import java.util.*; import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import java.util.zip.InflaterInputStream; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import java.io.*; import java.net.*; import java.security.*; import javax.net.ssl.*; import openadk.library.*; import openadk.library.infra.*; import openadk.library.tools.HTTPUtil; /** * An protocol handler implementation for HTTP. Each zone that is registered * with a ZIS using the HTTP or HTTPS protocol has an instance of this class * as its protocol handler. It implements the HttpHandler interface to process * SIF messages received by the agent's internal Jetty HTTP Server. When a * message is received via that interface it is delegated to the zone's * MessageDispatcher. HttpProtocolHandler also implements the IProtocolHandler * interface so it can send outgoing messages received by the * MessageDispatcher. * <p> * * An instance of this class runs in a separate thread only when the agent is * registered with the ZIS in Pull mode. In this case it does not accept * messages from the HttpHandler interface but instead periodically queries the * ZIS for new messages waiting in the agent's queue. Messages are delegated to * the MessageDispatcher for processing. * <p> * * @author Eric Petersen * @version ADK 1.0 */ public abstract class BaseHttpProtocolHandler implements IProtocolHandler { /** * */ private static final long serialVersionUID = Element.CURRENT_SERIALIZE_VERSION; private String fHttpUserAgent; private String fHttpHost; protected ZoneImpl fZone; private SSLSocketFactory fSSLFactory; protected HttpTransport fTransport; private URL fURL; protected BaseHttpProtocolHandler( HttpTransport transport ) { fTransport = transport; } public String getName() { return fZone.getAgent().getId()+"@"+fZone.getZoneId() + "." + this.getClass().getSimpleName(); } /** * Creates a new SIFParser to use for this protocol handler * @return a newly-created SIFParser to use for this protocol handler */ protected SIFParser createParser() { try { return SIFParser.newInstance(); } catch( ADKException adke ) { throw new openadk.util.InternalError( adke.toString() ); } } /** * Initialize the protocol handler for a zone */ public void open( ZoneImpl zone ) throws ADKException { fZone = zone; try { // Ensure the ZIS URL is http/https fURL = fZone.getZoneUrl(); String check = fURL.getProtocol().toLowerCase(); if( !check.equals("http") && !check.equals("https") ) throw new ADKException("HttpProtocolHandler cannot handle URL: "+fZone.getZoneUrl(),fZone); // Prepare headers later used to send messages fHttpUserAgent = fZone.getAgent().getId() + " (ADK/" + ADK.getADKVersion() + ")"; fHttpHost = fURL.getHost()+":"+fURL.getPort(); } catch( Throwable thr ) { throw new ADKException("HttpProtocolHandler could not parse URL \""+fZone.getZoneUrl()+"\": "+thr,fZone); } } /** * Close this ProtocolHandler for a zone */ public abstract void start() throws ADKException; public abstract void shutdown(); /** * Get an outbound connection to the ZIS * @return Either an HttpsURLConnection or an HttpURLConnection depending * on whether the associated transport protocol is secure or not */ @SuppressWarnings("unchecked") protected URLConnection getConnection() throws ADKTransportException { try { if( !fTransport.isSecure() ) { // Use a plain HTTP connection. The URLConnection class handles // keep-alive internally so even though we ask for a new // HttpURLConnection each call to this method, prior connections // are cached and kept alive (at least this is my understanding). // HttpURLConnection conn = (HttpURLConnection)fURL.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); conn.setRequestMethod("POST"); HttpProperties properties = (HttpProperties)fTransport.getProperties(); HTTPUtil.setTimeoutsOnConnection( properties, conn ); return conn; } else { HttpsProperties https = (HttpsProperties)fTransport.getProperties(); // Use an HTTPS Connection. If the SSLFactory hasn't been setup // yet, do this first and keep the factory object around so // subsequent calls can use it. // if( fSSLFactory == null ) { SSLContext ctx; KeyManagerFactory kmf; KeyStore ks; String ksPwd = fTransport.getKeyStorePassword(); char[] passphrase = ksPwd.toCharArray(); ctx = SSLContext.getInstance("TLS"); kmf = KeyManagerFactory.getInstance("SunX509"); ks = KeyStore.getInstance("JKS"); // If no keystore was specified in the HttpTransport // configuration, assume the default .keystore file in the // user's home directory String ksF = fTransport.getKeyStore(); if( ksF == null ) ksF = System.getProperties().getProperty("user.home") + File.separator + ".keystore"; File ksFile = new File(ksF); if( !ksFile.exists() ) throw new ADKTransportException( "Keystore file not found: " + ksFile.getAbsolutePath(), fZone ); if( ks == null ) { fZone.log.debug( "Using default Java keystore" ); } else { fZone.log.debug( "Using keystore: " + ksFile.getAbsolutePath() ); } if( ksPwd.equals("changeit") ) fZone.log.debug( "Using default Java keystore password 'changeit'"); try { ks.load(new FileInputStream(ksFile), passphrase); } catch( Exception e ) { throw new ADKTransportException("Failed to load keystore "+ksFile.getAbsolutePath()+": "+e,fZone); } // TODO: Don't rely on the System property // Point JSSE at the truststore String ts = https.getTrustStore(); String tsPwd = https.getTrustStorePassword(); if( tsPwd == null ) tsPwd = "changeit"; if( ts != null ) { File tsFile = new File(ts); if( !tsFile.exists() ) throw new ADKTransportException( "Truststore file not found: " + tsFile.getAbsolutePath(), fZone ); fZone.log.debug( "Using truststore: " + tsFile.getAbsolutePath() ); System.setProperty( "javax.net.ssl.trustStore", ts ); System.setProperty( "javax.net.ssl.trustStorePassword", tsPwd ); } else fZone.log.debug( "Using default Java truststore" ); if( tsPwd.equals("changeit") ) fZone.log.debug( "Using default Java truststore password 'changeit'" ); kmf.init(ks, passphrase); ctx.init(kmf.getKeyManagers(), null, null); fSSLFactory = ctx.getSocketFactory(); HttpsURLConnection.setDefaultSSLSocketFactory(fSSLFactory); } // // The URLConnection class handles keep-alive internally so even // though we ask for a new HttpsURLConnection each call to this // method, prior connections are cached and kept alive (at least // this is my understanding). // HttpsURLConnection conn = (HttpsURLConnection)fURL.openConnection(); conn.setSSLSocketFactory(fSSLFactory); conn.setDoInput(true); conn.setDoOutput(true); conn.setRequestMethod("POST"); HTTPUtil.setTimeoutsOnConnection( https, conn ); String hostnameVerifier = https.getHostnameVerifier(); if( hostnameVerifier == null || hostnameVerifier.length() == 0 ) { conn.setHostnameVerifier( new HostnameVerifier() { public boolean verify( String hostname, SSLSession session ) { String peer = session.getPeerHost(); if( !hostname.equals(peer) ) { try { InetAddress haddr = InetAddress.getByName( hostname ); InetAddress paddr = InetAddress.getByName( peer ); if( !haddr.getHostAddress().equals( paddr.getHostAddress() ) ) { fZone.log.debug( "Hostname in certificate ("+hostname+") does not match peer address (" + paddr.getHostName() + ", " + paddr.getHostAddress() + ")" ); return false; } } catch( UnknownHostException uhe ) { fZone.log.debug( "Unable to verify hostname: " + uhe ); return false; } } return true; } } ); } else if( !hostnameVerifier.equalsIgnoreCase("JSSE") ) { // If the HostnameVerifier property was specified and is set // to 'default', do nothing; leave the JSSE verifier intact. // Otherwise the agent is specifying a fully-qualified // class name to their own HostnameVerifier implementation. // Create an instance and pass it to the HttpsURLConnection Class<HostnameVerifier> clz = null; HostnameVerifier impl = null; try { fZone.log.debug( "Using custom HostnameVerifier: " + hostnameVerifier ); clz = (Class<HostnameVerifier>)Class.forName( hostnameVerifier ); } catch( ClassNotFoundException cnfe ) { fZone.log.error( "HostnameVerifier class not found: " + hostnameVerifier ); } try { impl = clz.newInstance(); } catch( Exception ex ) { fZone.log.error( "Unable to instantiate HostnameVerifier class " + hostnameVerifier + ": " + ex ); } conn.setHostnameVerifier( impl ); } return conn; } } catch( Throwable thr ) { throw new ADKTransportException("Failed to create outgoing socket to "+fURL.toExternalForm()+": "+thr,fZone); } } /* private void logSSLCerts( HttpsURLConnection conn ) { try { System.out.println("CipherSuite: "+conn.getCipherSuite()); Certificate[] certs = conn.getLocalCertificates(); System.out.println("Local Certificates: "+( certs == null ? "<none>" : String.valueOf(certs.length) )); if( certs != null ) { for( int i = 0; i < certs.length; i++ ) System.out.println(certs[i]); } certs = conn.getServerCertificates(); System.out.println("Server Certificates: "+( certs == null ? "<none>" : String.valueOf(certs.length) )); if( certs != null ) { for( int i = 0; i < certs.length; i++ ) System.out.println(certs[i]); } } catch( Exception e ) { System.out.println(e); } } */ //////////////////////////////////////////////////////////////////////////////// // // Message Sending // // /** * Sends a SIF infrastructure message and returns the response.<p> */ public String send( String msg ) throws ADKTransportException, ADKMessagingException { return send(msg,null,-1,true); } /** * Sends a SIF infrastructure message and returns the response.<p> */ public String send( Reader msg, int length ) throws ADKTransportException, ADKMessagingException { return send(null,msg,length,true); } /** * Sends a SIF infrastructure message and returns the response.<p> * * The message content should consist of a complete <SIF_Message> element. * This method sends whatever content is passed to it without any checking * or validation of any kind. * <p> * * There are two ways to pass message content to this method: as a single * String or as an input stream encapsulated by a Reader. For short messages * that can be held in memory in a single String without affecting system * performance, callers should simply pass the String to the <code>msg</code> * parameter. For longer messages that have been cached to disk, the caller * may provide an input stream to the message and the number of bytes that * should be read from that stream. * <p> * * @param msg The message content when provided as a String (ignored when * <code>longMsg</code> is non-null) * @param longMsg Provides the message content from an arbitrary input * stream. <code>longMsgLength</code> must also be specified. * @param longMsgLength Specifies the number of bytes to read from the * input stream when <code>longMsg</code> is non-null * * @return The response from the ZIS (expected to be a <SIF_Ack> message) * * @exception ADKMessagingException is thrown if there is an error sending * the message to the Zone Integration Server */ private String send( String msg, Reader longMsg, int longMsgLength, boolean expectResponse ) throws ADKTransportException, ADKMessagingException { String response = null; byte[] msgBytes = null; if( msg != null ) { try{ // We need to UTF-8 encode the message before we know what it's binary length in bytes will be ByteArrayOutputStream buffer = new ByteArrayOutputStream(); Writer w = SIFIOFormatter.createOutputWriter( buffer ); w.write( msg ); w.flush(); msgBytes = buffer.toByteArray(); w.close(); } catch( IOException ioex ){ throw new ADKMessagingException("HttpProtocolHandler: Unexpected error encoding message: "+ioex,fZone); } } int finalContentLength = (msgBytes != null ? msgBytes.length : longMsgLength); boolean attemptCompress = fZone.getProperties().getCompressionThreshold() > -1 && finalContentLength > fZone.getProperties().getCompressionThreshold(); String contentEncoding = null; if (attemptCompress) { SIF_ZoneStatus zoneStatus = fZone.getLastReceivedSIF_ZoneStatus(false); if (zoneStatus != null) { SIF_Protocol sifProtocol = zoneStatus.getSIF_SupportedProtocols().getSIF_Protocol(fTransport.isSecure() ? "HTTPS" : "HTTP"); if (sifProtocol != null) { for (SIF_Property sifProp : sifProtocol.getSIF_Propertys()) { if (!"Accept-Encoding".equals(sifProp.getSIF_Name())) continue; if (sifProp != null && sifProp.getSIF_Value() != null) { List<String> codingPreference = HTTPUtil.derivePreferredCodingFrom(sifProp.getSIF_Value()); for (String encoding : codingPreference) { if (encoding.contains("gzip")) { // gzip or x-gzip contentEncoding = "gzip"; break; } else if (encoding.contains("compress")) { // compress or x-compress contentEncoding = "compress"; break; } else if (encoding.contains("deflate")) { // deflate or x-deflate contentEncoding = "deflate"; break; } } break; } } } } } if (contentEncoding != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { DeflaterOutputStream gzos; if (contentEncoding.equals("gzip")) { gzos = new GZIPOutputStream(baos); } else if (contentEncoding.equals("compress")) { gzos = new ZipOutputStream(baos); } else { // deflate gzos = new DeflaterOutputStream(baos); } if (msgBytes != null) { gzos.write(msgBytes); } else { char[] buf = new char[1024]; int charsRead = 0; OutputStreamWriter osw = new OutputStreamWriter(gzos); while ((charsRead = longMsg.read(buf)) > -1) { if (charsRead > 0) { osw.write(buf, 0, charsRead); } } osw.flush(); } gzos.finish(); msgBytes = baos.toByteArray(); finalContentLength = msgBytes.length; } catch (IOException e) { throw new ADKMessagingException("Error while compressing outgoing data " + e, fZone, e); } } URLConnection conn = getConnection(); conn.setRequestProperty( "Content-Length", String.valueOf(finalContentLength) ); conn.setRequestProperty( "Content-Type", SIFIOFormatter.CONTENT_TYPE ); conn.setRequestProperty( "Accept-Encoding", fZone.getProperties().getAcceptEncoding()); conn.setRequestProperty( "Host", fHttpHost ); conn.setRequestProperty( "User-Agent", fHttpUserAgent ); conn.setRequestProperty( "Connection", "Keep-Alive" ); if (contentEncoding != null) { conn.setRequestProperty("Content-Encoding", contentEncoding); } OutputStream outStream = null; try { outStream = conn.getOutputStream(); } catch( Exception ex ) { throw new ADKTransportException( "Could not establish a connection to the ZIS (" + fURL.toExternalForm() + "): " + ex, fZone, ex ); } int totalBytes = 0; int bytes = 0; char[] buf = new char[1024]; // Message content if( msgBytes != null ) { // if( ( ADK.debug & ADK.DBG_TRANSPORT ) != 0 ) fZone.log.debug("Sending message ("+msgBytes.length+" bytes)"); if( ( ADK.debug & ADK.DBG_MESSAGE_CONTENT ) != 0 ) fZone.log.debug(msg); try{ outStream.write( msgBytes ); outStream.flush(); } catch( IOException ioe ){ throw new ADKMessagingException( "HttpProtocolHandler: Unexpected error sending message: " + ioe , fZone, ioe ); } finally { if( outStream != null ){ try { outStream.close(); } catch (IOException e) { fZone.log.warn(e.getMessage(), e ); } outStream = null; } } } else { try { if( ( ADK.debug & ADK.DBG_TRANSPORT ) != 0 ) fZone.log.debug("Sending message (" + longMsgLength + " bytes)"); PrintWriter out = new PrintWriter( SIFIOFormatter.createOutputWriter( outStream ) ); while( longMsg.ready() && totalBytes < longMsgLength ) { bytes = longMsg.read(buf,0,buf.length); out.write(buf,0,bytes); totalBytes += bytes; } out.flush(); if( out.checkError() ) throw new ADKMessagingException( "HttpProtocolHandler: Unknown error reading long message content from stream", fZone ); } catch( Exception ex ) { throw new ADKMessagingException( "HttpProtocolHandler: Unexpected error sending message: "+ex, fZone, ex ); } finally { if( outStream != null ){ try { outStream.close(); } catch (IOException e) { fZone.log.warn(e.getMessage(), e ); } outStream = null; } } } totalBytes = 0; int contentLen = conn.getContentLength(); contentEncoding = String.valueOf(conn.getContentEncoding()).trim().toLowerCase(); InputStream in = null; if( contentLen != 0 ) try { if( ( ADK.debug & ADK.DBG_TRANSPORT ) != 0 ) fZone.log.debug("Expecting reply ("+contentLen+" bytes)"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); in = new BufferedInputStream(conn.getInputStream()); if (!contentEncoding.equals("null")) { if (contentEncoding.contains("gzip")) { // gzip or x-gzip in = new GZIPInputStream(in); } else if (contentEncoding.contains("compress")) { // compress or x-compress in = new ZipInputStream(in); } else if (contentEncoding.contains("deflate")) { // deflate or x-deflate in = new InflaterInputStream(in); } } byte[] byteBuf = new byte[8192]; int bytesRead = 0; while ((bytesRead = in.read(byteBuf)) > -1) { if (bytesRead > 0) { totalBytes += bytesRead; baos.write(byteBuf, 0, bytesRead); } } response = new String(baos.toByteArray(), "UTF-8"); if( ( ADK.debug & ADK.DBG_TRANSPORT ) != 0 ) fZone.log.debug("Received reply ("+totalBytes+" bytes)"); if( ( ADK.debug & ADK.DBG_MESSAGE_CONTENT ) != 0 ) fZone.log.debug(response); } catch( Throwable thr ) { throw new ADKMessagingException("HttpProtocolHandler: Error receiving response to sent message: "+thr,fZone, thr); } finally { if( in != null ){ try{ // Close the stream to free up the HTTPConnection for re-use in.close(); } catch (IOException ioe ){ fZone.log.warn( ioe.getMessage(), ioe ); } } } return response; } /** * Parse an HTTP response line */ // private int parseHttpResponse( String response ) // { // int st = response.indexOf(' '); // if( st == -1 ) // return HttpURLConnection.HTTP_OK; // // int en = response.indexOf(' ',st+1); // String code = response.substring(st+1,en); // try { // return Integer.parseInt(code); // } catch( Exception e ){ // return -1; // } // } }