/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1 * * 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 OpenEMRConnect. * * The Initial Developer of the Original Code is International Training & * Education Center for Health (I-TECH) <http://www.go2itech.org/> * * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * ***** END LICENSE BLOCK ***** */ package ke.go.moh.oec.lib; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import java.io.OutputStream; import java.security.NoSuchAlgorithmException; import java.util.logging.Level; import java.util.logging.Logger; import java.net.MalformedURLException; import java.net.UnknownHostException; import java.net.URL; import java.net.HttpURLConnection; import java.io.InputStreamReader; import java.io.BufferedReader; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.URI; import java.security.MessageDigest; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; /** * Handles HTTP requests and responses between OpenEMRConnect nodes. * * @author John Gitau * @author Jim Grace */ class HttpService { /** * {@link Mediator} class instance to which we pass any received HTTP * requests. */ private Mediator mediator = null; private int id = 0; private int port = 0; HttpServer server; MessageDigest messageDigest; Map<String, Date> unreachableIpPorts = new HashMap<String, Date>(); private static final SimpleDateFormat SIMPLE_DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private static final String HTTP_CONTENT_XML = "application/xml"; private static final String HTTP_CONTENT_ZIP = "application/zip"; private static final int HTTP_RESPONSE_OK = 200; private static final int HTTP_RESPONSE_LENGTH_REQUIRED = 411; private static final int HTTP_RESPONSE_MD5_MISMATCH = 449; // No obvious choice here, this code is Microsoft "Retry With" private static final int HTTP_RESPONSE_MD5_REQUIRED = 455; // OEC-defined code /** * Stores a partial message that is being received in segments from a given source. */ private class PartialMessage { /** ID number of the partial message in progress. */ private int id; /** Most recent segment number within the partial message. */ private int segment = 0; /** Total length of all the segments received so far. */ private int length = 0; /** Array of all segments received so far. */ private List<byte[]> messageSegments = new ArrayList<byte[]>(); } /** * Stores all partial messages that are in the process of being received in segments. * This HashMap is keyed by the IP address and (listening) port number of the sender. */ private Map<String, PartialMessage> partialMessages = new HashMap<String, PartialMessage>(); /** * Constructor to set {@link Mediator} callback object * * @param mediator {@link Mediator} callback object for listener */ HttpService(Mediator mediator) { this.mediator = mediator; try { messageDigest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException ex) { Logger.getLogger(HttpService.class.getName()).log(Level.SEVERE, "Can't get an instance of the MD5 algorithm", ex); } } /** * Sends a HTTP message. * * @param m Message to send * @return true if message was sent and HTTP response received, otherwise false */ boolean send(Message m) throws MalformedURLException, IOException { if (port == 0) { port = Integer.parseInt(Mediator.getProperty("HTTPHandler.ListenPort")); } boolean returnStatus = false; String destinationAddress = m.getDestinationAddress(); NextHop nextHop = m.getNextHop(); if (nextHop == null) { // If we are called from the QueueManager, we may not have the next hop information because they are not stored in the queue database. // If this is the case, then get the IP address and port now, from the destination address. nextHop = NextHop.getNextHopByAddress(destinationAddress); m.setNextHop(nextHop); } String ipAddressPort = nextHop.getIpAddressPort(); int maxSize = nextHop.getMaxSize(); String url = "http://" + ipAddressPort + "/oecmessage?destination=" + destinationAddress + "&tobequeued=" + m.isToBeQueued() + "&hopcount=" + m.getHopCount() + "&port=" + port; try { /*Code thats performing a task should be placed in the try catch statement especially in the try part*/ byte[] messageBytes; int messageLength; String contentType; if (nextHop.isZip()) { messageBytes = m.getCompressedXml(); messageLength = m.getCompressedXmlLength(); contentType = HTTP_CONTENT_ZIP; } else { String xml = m.getXml(); messageBytes = xml.getBytes(); messageLength = messageBytes.length; contentType = HTTP_CONTENT_XML; } int sent = 0; int toSend = messageLength; if (messageLength > maxSize) { // If we're going to split this message: // Append the next message ID onto the URL. url += "&id=" + ++id + "&segment="; } int segment = 0; while (sent < messageLength) { String thisUrl = url; if (messageLength > maxSize) { thisUrl = url + Integer.toString(++segment); if (messageLength - sent > maxSize) { toSend = maxSize; } else { toSend = messageLength - sent; thisUrl = thisUrl + "&end"; } } HttpURLConnection connection = (HttpURLConnection) new URL(thisUrl).openConnection(); String md5 = computeMd5(messageBytes, sent, toSend); connection.setRequestProperty("Content-MD5", md5); connection.setRequestProperty("Content-Type", contentType); connection.setDoOutput(true); OutputStream output = connection.getOutputStream(); output.write(messageBytes, sent, toSend); output.close(); int responseCode = connection.getResponseCode(); // // Check the response code. It may be one of the response codes that // we know we generate from the other side if the message was garbled. // If it is one of these messages, then we assume the message was garbled, // because we know we formatted it correctly. If this is the case, // then just keep retrying to send the same message over and over. // As long as something is getting through, then the whole message should go through. // // If we get any other kind of response, either it was OK or the receiver // was not us. In either case, account for the number of bytes // sent, and continue sending (or finish if everything was sent.) // if (responseCode != HTTP_RESPONSE_LENGTH_REQUIRED && responseCode != HTTP_RESPONSE_MD5_MISMATCH && responseCode != HTTP_RESPONSE_MD5_REQUIRED) { if (responseCode != HTTP_RESPONSE_OK) { Logger.getLogger(HttpService.class.getName()).log(Level.FINE, "HTTP response code {0}, sending message to {1} at {2}", new Object[]{responseCode, m.getDestinationAddress(), url}); } sent = sent + toSend; InputStreamReader inputStreamReader = new InputStreamReader(connection.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); while (br.readLine() != null) { //content not required, just acknowlegment that message was received. } br.close(); inputStreamReader.close(); } else { Logger.getLogger(HttpService.class.getName()).log(Level.FINE, "HTTP response code {0}. Retrying sending message to {1} at {2}", new Object[]{responseCode, m.getDestinationAddress(), url}); } } returnStatus = true; canReach(ipAddressPort); } catch (ConnectException ex) { cannotReach(ipAddressPort, "Can't connect to " + ipAddressPort + " for message to " + destinationAddress); } catch (UnknownHostException ex) { cannotReach(ipAddressPort, "Unknown Host " + ipAddressPort + " for message to " + destinationAddress); } catch (MalformedURLException ex) { Logger.getLogger(HttpService.class.getName()).log(Level.SEVERE, "While sending to " + m.getDestinationAddress() + " at " + url, ex); } catch (IOException ex) { String message = ex.getMessage(); if (message.equals("Premature EOF") || message.equals("Unexpected end of file from server")) { returnStatus = true; // We expect End of File at some point } else { Logger.getLogger(HttpService.class.getName()).log(Level.SEVERE, "While sending to " + m.getDestinationAddress() + " at " + url, ex); // There was some transmission error we return false. } } return returnStatus; } /** * Handles the case where we can't reach a given IP address / port. * <p> * If this is the first time we have this problem: (a) log an error message, * and (b) add this IP address / port to a list of IP addresses / ports with * whom we are having trouble communicating. * <p> * If this IP address / port is already on the list of destinations we cannot * reach, do nothing. This prevents trying to send a message to the * logging server every time we retry sending to this IP address / port. * If we did so, this could result in a lot of traffic to the logging server. * Worse yet, the message to the logging server might itself not be able to * be sent. Instead, we will send a single message to the logging server * at a later time when we can send to this IP address / port again. * * @param ipAddressPort IP Address and Port we cannot reach * @param errorMessage Error why we cannot reach this IP address / port. */ private synchronized void cannotReach(String ipAddressPort, String errorMessage) { if (!unreachableIpPorts.containsKey(ipAddressPort)) { Logger.getLogger(HttpService.class.getName()).log(Level.SEVERE, errorMessage); unreachableIpPorts.put(ipAddressPort, new Date()); } } /** * Handles the case where we can reach a given IP address / port. * <p> * If we were previously having trouble reaching the given IP address / port, * it will be on a list of destinations with which we were having trouble. * In this case, log an informational message that the trouble is now over. * Include in this message the time when the trouble started. And remove * this IP address / port combination from our trouble list. * * @param ipAddressPort IP Address and port we can reach */ private void canReach(String ipAddressPort) { if (unreachableIpPorts.containsKey(ipAddressPort)) { Date sinceDate = unreachableIpPorts.get(ipAddressPort); Logger.getLogger(HttpService.class.getName()).log(Level.INFO, "Can reach {0} for the first time since {1}", new Object[]{ipAddressPort, SIMPLE_DATE_TIME_FORMAT.format(sinceDate)}); unreachableIpPorts.remove(ipAddressPort); } } /** * Starts listening for HTTP messages. * <p> * For each message received, call mediator.processReceivedMessage() * @throws IOException */ void start() throws IOException { //throw new UnsupportedOperationException("Not supported yet."); if (port == 0) { port = Integer.parseInt(Mediator.getProperty("HTTPHandler.ListenPort")); } InetSocketAddress addr = new InetSocketAddress(port); server = HttpServer.create(addr, 0); server.createContext("/oecmessage", (HttpHandler) new Handler(mediator)); server.setExecutor(Executors.newCachedThreadPool()); server.start(); Mediator.getLogger(HttpService.class.getName()).log(Level.INFO, Mediator.getProperty("Instance.Name") + " " + Mediator.getProperty("Instance.Address") + " listening on port {0}", Integer.toString(port)); // (Explicitly convert to string to avoid "," thousands seperator formatting.) } /** * Stops listening for HTTP messages. */ void stop() { final int delaySeconds = 0; server.stop(delaySeconds); } /** * The handler class below implements the HttpHandler interface properties and is called up to process * HTTP exchanges. */ private class Handler implements HttpHandler { private Mediator mediator = null; private Handler(Mediator mediator) { this.mediator = mediator; } /** * * @param exchange * @throws IOException */ public void handle(HttpExchange exchange) throws IOException { Message m = new Message(); /* * Unpack the URL. */ URI uri = exchange.getRequestURI(); String query = uri.getQuery(); int id = 0; int segment = 0; boolean end = false; boolean zipped = false; // // Parse the URL arguments // for (String param : query.split("&")) { String[] pair = param.split("="); if (pair[0].equals("destination")) { m.setDestinationAddress(pair[1]); } else if (pair[0].equals("hopcount")) { m.setHopCount(Integer.parseInt(pair[1])); } else if (pair[0].equals("tobequeued")) { m.setToBeQueued(Boolean.parseBoolean(pair[1])); } else if (pair[0].equals("port")) { m.setSendingPort(Integer.parseInt(pair[1])); } else if (pair[0].equals("id")) { id = Integer.parseInt(pair[1]); } else if (pair[0].equals("segment")) { segment = Integer.parseInt(pair[1]); } else if (pair[0].equals("end")) { end = true; } } InetSocketAddress remoteAddress = exchange.getRemoteAddress(); String sendingIpAddress = remoteAddress.getAddress().getHostAddress(); String sendingIpAddressAndPort = sendingIpAddress; if (m.getSendingPort() != 0) { sendingIpAddressAndPort += ":" + m.getSendingPort(); } NextHop hop = NextHop.getNextHopByIpPort(sendingIpAddressAndPort); String requestMethod = exchange.getRequestMethod(); if (requestMethod.equals("POST")) { /* * Read the posted content */ Headers headers = exchange.getRequestHeaders(); int responseCode = HTTP_RESPONSE_OK; String contentType = headers.getFirst("Content-Type"); if (contentType != null && contentType.compareTo(HTTP_CONTENT_ZIP) == 0) { zipped = true; } boolean outOfSequence = false; int bufferSize = 50000; // Default buffer size if no Content-Length header is present. String contentLength = headers.getFirst("Content-Length"); if (contentLength != null) { bufferSize = Integer.parseInt(contentLength); } else if (hop != null && hop.isLengthRequired()) { responseCode = HTTP_RESPONSE_LENGTH_REQUIRED; } InputStream input = exchange.getRequestBody(); byte[] messageBytes = new byte[bufferSize]; int messageLength = input.read(messageBytes); input.close(); String md5Reported = headers.getFirst("Content-MD5"); if (md5Reported != null) { String md5Computed = computeMd5(messageBytes, 0, messageLength); if (md5Reported.compareTo(md5Computed) != 0) { responseCode = HTTP_RESPONSE_MD5_MISMATCH; Logger.getLogger(HttpService.class.getName()).log(Level.FINE, "MD5 reported as {0}, computed as {1}, length expected {2}, found {3}", new Object[]{md5Reported, md5Computed, bufferSize, messageLength}); } } else if (hop != null && hop.isMd5Required()) { responseCode = HTTP_RESPONSE_MD5_REQUIRED; } if (responseCode == HTTP_RESPONSE_OK) { boolean completeMessage = true; m.setSendingIpAddress(sendingIpAddress); m.setSegmentCount(1); m.setLongestSegmentLength(messageLength); if (id > 0) { completeMessage = false; PartialMessage pm = null; if (segment == 1) { pm = new PartialMessage(); pm.id = id; pm.segment = segment; byte[] a = Arrays.copyOf(messageBytes, messageLength); pm.messageSegments.add(a); pm.length += messageLength; partialMessages.put(sendingIpAddressAndPort, pm); } else { pm = partialMessages.get(sendingIpAddressAndPort); if (pm != null) { if (pm.id == id && ++pm.segment == segment) { byte[] a = Arrays.copyOf(messageBytes, messageLength); pm.messageSegments.add(a); pm.length += messageLength; if (end) { messageLength = pm.length; messageBytes = new byte[messageLength]; int offset = 0; int longest = 0; for (byte[] seg : pm.messageSegments) { System.arraycopy(seg, 0, messageBytes, offset, seg.length); offset += seg.length; if (seg.length > longest) { longest = seg.length; } } m.setSegmentCount(pm.messageSegments.size()); m.setLongestSegmentLength(longest); completeMessage = true; partialMessages.remove(sendingIpAddressAndPort); } } else { if (pm.id != id) { Logger.getLogger(HttpService.class.getName()).log(Level.FINE, "Message id mismatch from {0}. Expected id {1}, found {2}, expected sequence {3}, found {4}", new Object[]{sendingIpAddressAndPort, pm.id, id, pm.segment, segment}); } else { Logger.getLogger(HttpService.class.getName()).log(Level.FINE, "Message segment out of sequence from {0}, message id {1}, expected sequence {2}, found {3}", new Object[]{sendingIpAddressAndPort, id, pm.segment, segment}); } outOfSequence = true; partialMessages.remove(sendingIpAddressAndPort); } } else { Logger.getLogger(HttpService.class.getName()).log(Level.FINE, "Received segment from {0}, id {1}, segment {2} but no partial message previously stored.", new Object[]{sendingIpAddressAndPort, id, segment}); } } } if (completeMessage) { m.setSendingIpAddress(sendingIpAddress); if (zipped) { m.setCompressedXml(messageBytes); m.setCompressedXmlLength(messageLength); } else { String xml = new String(messageBytes, 0, messageLength); m.setXml(xml); } /* * Process the message. */ mediator.processReceivedMessage(m); } } if (!outOfSequence) { /* * Acknoweldge to the sender that we received the message. * (Don't acknowledge an out-of-sequence message). */ Headers responseHeaders = exchange.getResponseHeaders(); responseHeaders.set("Content-Type", "text/plain"); exchange.sendResponseHeaders(responseCode, 0); OutputStream responseBody = exchange.getResponseBody(); responseBody.close(); } exchange.close(); } } } /** * Computes the MD5 hash for an array of bytes. * * @param bytes array of bytes for which the MD5 hash will be computed * @param offset starting offset for computing the MD5 hasn * @param length length for computing the MD5 hash * @return the MD5 hash in 32 characters hexadecimal. */ String computeMd5(byte[] bytes, int offset, int length) { messageDigest.reset(); messageDigest.update(bytes, offset, length); byte[] digest = messageDigest.digest(); BigInteger bigInt = new BigInteger(1, digest); String hashtext = bigInt.toString(16); // Now we need to zero pad it to get the full 32 chars. while (hashtext.length() < 32) { hashtext = "0" + hashtext; } return hashtext; } }