package openadk.library.impl; import openadk.library.*; import openadk.library.infra.SIF_Ack; import openadk.library.infra.SIF_Header; import openadk.library.tools.HTTPUtil; import openadk.util.GUIDGenerator; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Reader; import java.util.Calendar; import java.util.List; 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; /** * Created by IntelliJ IDEA. * User: UBAINDA * Date: 9/13/11 * Time: 9:09 AM * To change this template use File | Settings | File Templates. */ public class HttpPushProtocolContextHandler extends AbstractHandler { private ZoneImpl fZone; private static final int SLASH_ZONES_SLASH_OFFSET = "/zone/".length(); private static final String TRAILING_SLASH = "/"; Server fServer; public HttpPushProtocolContextHandler(ZoneImpl zone) { fZone = zone; } /** * Process an http request from Jetty. */ public void handle( String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response ) throws IOException, ServletException { if( ( ADK.debug & ADK.DBG_MESSAGING ) != 0 ) fZone.log.debug("Received push message from "+request.getRemoteAddr()+" ("+request.getScheme()+")" ); SIF_Ack ack = null; SIFMessagePayload parsed = null; // Check request length and type if( request.getContentLength() < 1 ) { try { response.sendError(HttpServletResponse.SC_BAD_REQUEST); } catch (IOException e) { fZone.log.error("Problem sending Bad request error"); } } /* if( !request.getContentType().equalsIgnoreCase( SIFIOFormatter.CONTENT_TYPE ) ) throw new HttpException( HttpResponse.__415_Unsupported_Media_Type ); */ String contentTest = ""; if(request.getContentType() != null) { contentTest = request.getContentType().toLowerCase(); } if ( (!contentTest.contains(SIFIOFormatter.CONTENT_TYPE_BASE)) || (!contentTest.contains(SIFIOFormatter.CONTENT_TYPE_UTF8)) ) { try { response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); } catch (IOException e) { fZone.log.error("Problem sending UNSUPPORTED_MEDIA_TYPE error"); } } // Read raw content StringBuffer xml = readPush(request,response); if( xml == null ){ // Shouldn't happen xml = new StringBuffer(); } if( ( ADK.debug & ADK.DBG_MESSAGE_CONTENT ) != 0 ){ fZone.log.debug("Received "+xml.length()+" bytes:\r\n"+xml ); } Throwable parseEx = null; boolean reparse = false; boolean cancelled = false; SIFParser parser; try { parser = SIFParser.newInstance(); } catch( ADKException adke ) { throw new openadk.util.InternalError( adke.toString() ); } int reparsed = 0; do { try { parseEx = null; // Parse content parsed = (SIFMessagePayload)parser.parse(xml.toString(),fZone); parsed.LogRecv(fZone.log); } catch( ADKParsingException adke ) { parseEx = adke; } catch( Throwable ex ) { parseEx = ex; } // // Notify listeners... // // If we're asked to reparse the message, do so but do not notify // listeners the second time around. // if( reparsed == 0 ) { List<MessagingListener> msgList = MessageDispatcher.getMessagingListeners( fZone ); for( MessagingListener ml : msgList ) { try { byte pload = ADK.DTD().getElementType( parsed.getElementDef().name() ); byte code = ml.onMessageReceived( pload, xml ); switch( code ) { case MessagingListener.RX_DISCARD: cancelled = true; break; case MessagingListener.RX_REPARSE: reparse = true; break; } } catch( ADKException adke ) { parseEx = adke; } catch(Throwable e) { if(parseEx == null) { parseEx = e; } } } } if( cancelled ) return; reparsed++; } while( reparse ); if( parseEx != null ) { // TODO: Handle the case where SIF_OriginalSourceId and SIF_OriginalMsgId // are not available because parsing failed. See SIFInfra // Resolution #157. if( parseEx instanceof SIFException && parsed != null ) { // Specific SIF error already provided to us by SIFParser ack = parsed.ackError( (SIFException)parseEx ); } else{ String errorMessage = null; if( parseEx instanceof ADKException ) { errorMessage = parseEx.getMessage(); } else { // Unchecked Throwable errorMessage = "Could not parse message"; } if( parsed == null ) { SIFException sifError = null; if( parseEx instanceof SIFException ){ sifError = (SIFException) parseEx; }else { sifError = new SIFException(SIFErrorCategory.XML_VALIDATION, SIFErrorCodes.XML_GENERIC_ERROR_1, "Could not parse message" , parseEx.toString(), fZone ); } ack = SIFPrimitives.ackError( xml.toString(), sifError, fZone ); } else { ack = parsed.ackError( SIFErrorCategory.GENERIC, SIFErrorCodes.GENERIC_GENERIC_ERROR_1, errorMessage, parseEx.toString() ); } } if( ( ADK.debug & ADK.DBG_MESSAGING ) != 0 ) fZone.log.warn("Failed to parse push message from zone \"" + fZone + "\": " + parseEx ); if( ack != null ) { // Ack messages in the same version of SIF as the original message if( parsed != null ){ ack.setSIFVersion( parsed.getSIFVersion() ); } ackPush(ack,request,response); } else { // If we couldn't build a SIF_Ack, returning an HTTP 500 is // probably the best we can do to let the server know that // we didn't get the message. Note this should cause the ZIS // to resend the message, which could result in a deadlock // condition. The administrator would need to manually remove // the offending message from the agent's queue. if( ( ADK.debug & ADK.DBG_MESSAGING ) != 0 ) fZone.log.debug("Could not generate SIF_Ack for failed push message (returning HTTP/1.1 500)"); try { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } catch (IOException e) { fZone.log.error("Problem sending INTERNAL_SERVER_ERROR error"); } } return; } // Check SourceId to see if it matches this agent's SourceId String destId = parsed.getDestinationId(); if( destId != null && !destId.equals( fZone.getAgent().getId() ) ) { fZone.log.warn("Received push message for DestinationId \""+destId+"\", but agent is registered as \""+fZone.getAgent().getId()+"\"" ); ack = parsed.ackError( SIFErrorCategory.TRANSPORT, SIFErrorCodes.WIRE_GENERIC_ERROR_1, "Message not intended for this agent (SourceId of agent does not match DestinationId of message)", "Message intended for \"" + destId + "\" but this agent is registered as \"" + fZone.getAgent().getId() + "\"" ); ackPush(ack,request,response); return; } // // Check Zone ID. // Extract the zone ID from the path. The path will be the context // string "/zone/{zondId}/" and the pathInContext should be "/" (the // trailing slash) unless the ZIS specified additional information // on URI path for some reason. Extract the zone name // from that. // String zone = request.getContextPath(); zone = zone.substring(SLASH_ZONES_SLASH_OFFSET, zone.length()); if( !zone.equals( fZone.getZoneId() ) ) { fZone.log.warn("Received push message from zone \""+zone+"\", but agent is expecting messages from zone \""+fZone.getZoneId() ); ack = parsed.ackError( SIFErrorCategory.SYSTEM, SIFErrorCodes.SYS_GENERIC_ERROR_1, "Unexpected Zone", "Agent not expecting messages from zone: "+zone); ackPush(ack,request,response); return; } // Convert content to SIF message object and dispatch it ack = processPush(parsed); // Send SIF_Ack reply ackPush(ack,request,response); } private SIF_Ack processPush( SIFMessagePayload parsed ) { try { // Dispatch. When the result is an Integer it is an ack code to // return; otherwise it is ack data to return and the code is assumed // to be 1 for an immediate acknowledgement. int ackStatus = fZone.getFDispatcher().dispatch(parsed); // Ask the original message to generate a SIF_Ack for itself return parsed.ackStatus(ackStatus); } catch( SIFException se ) { return parsed.ackError( se ); } catch( ADKException adke ) { return parsed.ackError( SIFErrorCategory.GENERIC, SIFErrorCodes.GENERIC_GENERIC_ERROR_1, adke.getMessage() ); } catch( Throwable thr ) { if( ( ADK.debug & ADK.DBG_MESSAGING ) != 0 ) fZone.log.debug("Uncaught exception dispatching push message: "+thr); return parsed.ackError( SIFErrorCategory.GENERIC, SIFErrorCodes.GENERIC_GENERIC_ERROR_1, "An unexpected error has occurred", thr.toString() ); } } private StringBuffer readPush( HttpServletRequest request, HttpServletResponse response ) { Reader in = null; int expected = request.getContentLength(), totalRead = 0; try { // NOTE: Keep this a StringBuffer for now because it is used externally StringBuffer strbuf = new StringBuffer(expected > 64 ? expected : 64); char buf[] = new char[expected < 1024 ? expected : 1024]; String contentEncoding = request.getHeader("Content-Encoding"); if (contentEncoding == null) { in = SIFIOFormatter.createInputReader( request.getInputStream() ); } else { String encoding = contentEncoding.toLowerCase(); if (encoding.contains("gzip")) { // gzip or x-gzip in = SIFIOFormatter.createInputReader(new GZIPInputStream(request.getInputStream())); } else if (encoding.contains("compress")) { // compress or x-compress in = SIFIOFormatter.createInputReader(new ZipInputStream(request.getInputStream())); } else if (encoding.contains("deflate")) { // deflate or x-deflate in = SIFIOFormatter.createInputReader(new InflaterInputStream(request.getInputStream())); } else { fZone.log.error("HttpProtocolHandler push message failure : Unsuported Content-Encoding of '" + contentEncoding + "'" ); try { response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); } catch (IOException e) { fZone.log.error("Problem sending UNSUPPORTED_MEDIA_TYPE error"); } return null; } } int charsRead = 0; while ((charsRead = in.read(buf)) > -1) { if (charsRead > 0) { strbuf.append(buf, 0, charsRead); totalRead += charsRead; } } return strbuf; } catch( Throwable thr ) { fZone.log.error("HttpProtocolHandler failed to read push message (approximately "+ totalRead+" of "+expected+" bytes read; zone="+fZone.getZoneId()+"): "+thr); try { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } catch (IOException e) { fZone.log.error("Problem sending INTERNAL_SERVER_ERROR error"); } return null; } finally { if( in != null ) { try { in.close(); } catch( IOException ignored ) { fZone.log.warn( ignored.getMessage(), ignored ); } } } } private void ackPush( SIF_Ack ack, HttpServletRequest request, HttpServletResponse response ) { try { // Set SIF_Ack / SIF_Header fields SIF_Header hdr = ack.getHeader(); hdr.setSIF_Timestamp( Calendar.getInstance() ); hdr.setSIF_MsgId(GUIDGenerator.makeGUID()); hdr.setSIF_SourceId(fZone.getAgent().getId()); ack.LogSend(fZone.log); // Convert message to a string ByteArrayOutputStream raw = new ByteArrayOutputStream(); SIFWriter out = new SIFWriter( raw, fZone ); out.write(ack); out.close(); raw.close(); byte[] realData = raw.toByteArray(); boolean compressed = false; raw.reset(); if (fZone.getProperties().getCompressionThreshold() > -1 && realData.length > fZone.getProperties().getCompressionThreshold()) { String acceptEncoding = request.getHeader("Accept-Encoding"); if (acceptEncoding != null) { List<String> tokens = HTTPUtil.derivePreferredCodingFrom(acceptEncoding); if (tokens.contains("gzip")) { GZIPOutputStream gzos = new GZIPOutputStream(raw); gzos.write(realData); gzos.flush(); gzos.finish(); realData = raw.toByteArray(); compressed = true; } } } // Send reply response.setContentType( SIFIOFormatter.CONTENT_TYPE ); response.setContentLength(realData.length); if (compressed) { response.setHeader("Content-Encoding", "gzip"); } response.getOutputStream().write(realData); response.getOutputStream().flush(); // reply.close(); } catch( Throwable thr ) { fZone.log.error("HttpProtocolHandler failed to send SIF_Ack for pushed message (zone="+ fZone.getZoneId()+"): "+thr); try { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } catch (IOException e) { fZone.log.error("Problem sending INTERNAL_SERVER_ERROR error"); } } } }