All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gov.nist.javax.sip.parser.NioPipelineParser Maven / Gradle / Ivy

There is a newer version: 1.3.0-91
Show newest version
/*
 * Conditions Of Use
 *
 * This software was developed by employees of the National Institute of
 * Standards and Technology (NIST), an agency of the Federal Government.
 * Pursuant to title 15 Untied States Code Section 105, works of NIST
 * employees are not subject to copyright protection in the United States
 * and are considered to be in the public domain.  As a result, a formal
 * license is not needed to use the software.
 *
 * This software is provided by NIST as a service and is expressly
 * provided "AS IS."  NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED
 * OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT
 * AND DATA ACCURACY.  NIST does not warrant or make any representations
 * regarding the use of the software or the results thereof, including but
 * not limited to the correctness, accuracy, reliability or usefulness of
 * the software.
 *
 * Permission to use this software is contingent upon your acceptance
 * of the terms of this agreement
 *
 * .
 *
 */
/******************************************************************************
 * Product of NIST/ITL Advanced Networking Technologies Division (ANTD)       *
 ******************************************************************************/
package android.gov.nist.javax.sip.parser;

import android.gov.nist.core.CommonLogger;
import android.gov.nist.core.LogLevels;
import android.gov.nist.core.LogWriter;
import android.gov.nist.core.StackLogger;
import android.gov.nist.javax.sip.header.CallID;
import android.gov.nist.javax.sip.header.ContentLength;
import android.gov.nist.javax.sip.message.SIPMessage;
import android.gov.nist.javax.sip.stack.ConnectionOrientedMessageChannel;
import android.gov.nist.javax.sip.stack.QueuedMessageDispatchBase;
import android.gov.nist.javax.sip.stack.SIPTransactionStack;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;

/**
 * This is a FSM that can parse a single stream of messages with they bodies and 
 * then pass the sip message to the listeners. It accumulates bytes until end of
 * message is detected or some DoS trigger terminates it due to excessive amount
 * of bytes per message or line.
 * 
 * Once parsed it will pass the message to the SIPMessageListener
 *
 * @see SIPMessageListener
 * @author vladimirralev
 */
public class NioPipelineParser {
	
	private static StackLogger logger = CommonLogger.getLogger(NioPipelineParser.class);

	private static final String CRLF = "\r\n";

    /**
     * The message listener that is registered with this parser. (The message
     * listener has methods that can process correct and erroneous messages.)
     */
    protected SIPMessageListener sipMessageListener;
    private int maxMessageSize;
    private int sizeCounter;
    private SIPTransactionStack sipStack;
    private MessageParser smp = null;

    boolean isRunning = false;
	boolean currentStreamEnded = false;
	boolean readingMessageBodyContents = false;
	boolean readingHeaderLines = true;
	boolean partialLineRead = false; // if we didn't receive enough bytes for a full line we expect the line to end in the next batch of bytes
	String partialLine = "";
	String callId;
	

	public static class UnparsedMessage {
		String lines;
		byte[] body;
		public UnparsedMessage(String messageLines, byte[] body) {
			this.lines = messageLines;
			this.body = body;
		}
		
		public String toString() {
			return super.toString() + "\n" + lines;
		}
	}
	
    public class Dispatch implements Runnable, QueuedMessageDispatchBase{
    	String callId;
        UnparsedMessage unparsedMessage;
    	long time;
    	public Dispatch(UnparsedMessage unparsedMsg, String callId) {
    		this.unparsedMessage = unparsedMsg;
    		this.callId = callId;
    		time = System.currentTimeMillis();
    	}
        public void run() {   
            logger.logInfo("serving msg on call id " + callId);
            SIPMessage parsedSIPMessage = null;
            try {

            		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
            			logger.logDebug( "\nUnparsed message before parser is:\n" + unparsedMessage);
            		}
                    byte[] lineBytes = unparsedMessage.lines.getBytes("UTF-8");
                    parsedSIPMessage = smp.parseSIPMessage(lineBytes, false, false, null);        		
        			if(parsedSIPMessage == null) {
        				// https://java.net/jira/browse/JSIP-503
        				if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
                			logger.logDebug( "parsed message is null, probably because of end of stream, empty packets or socket closed "
                					+ "and we got CRLF to terminate cleanly, not processing message");
                		}
        			} else if(unparsedMessage.body.length > 0) {
        				parsedSIPMessage.setMessageContent(unparsedMessage.body);
        			}

            	if(sipStack.sipEventInterceptor != null
            			// https://java.net/jira/browse/JSIP-503
                		&& parsedSIPMessage != null) {
            		sipStack.sipEventInterceptor.beforeMessage(parsedSIPMessage);
            	}

            	if(parsedSIPMessage != null) { // https://java.net/jira/browse/JSIP-503
            		sipMessageListener.processMessage(parsedSIPMessage);
            	}
            } catch (ParseException e) {
            	// https://java.net/jira/browse/JSIP-499 move the ParseException here so the finally block 
            	// is called, the semaphore released and map cleaned up if need be
            	if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
            		logger.logDebug("Problem parsing message " + unparsedMessage + " " + e.getMessage());
            	}
    		}catch (Exception e) {
            	logger.logError("Error occured processing message " + message, e);
                // We do not break the TCP connection because other calls use the same socket here
            } finally {            
                if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
                	logger.logDebug("releasing semaphore for message " + parsedSIPMessage);
                }
                if(sipStack.sipEventInterceptor != null
                		// https://java.net/jira/browse/JSIP-503
                		&& parsedSIPMessage != null) {
                	sipStack.sipEventInterceptor.afterMessage(parsedSIPMessage);
                }
            }
            if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
            	logger.logDebug("dispatch task done on " + parsedSIPMessage);
            }
        }
		public long getReceptionTime() {
			return time;
		}
    };
	
	public void close() {
		
	}
	
	StringBuilder message = new StringBuilder();
	byte[] messageBody = null;
	int contentLength = 0;
	int contentReadSoFar = 0;
	
	/*
	 *  This is where we receive the bytes from the stream and we analyze the through message structure.
	 *  For TCP the key things to identify are message lines for the headers, parse the Content-Length header
	 *  and then read the message body (aka message content). For TCP the Content-Length must be 100% accurate.
	 */
	public void readStream(InputStream inputStream) throws IOException {
		boolean isPreviousLineCRLF = false;
		while(true) { // We read continiously from the bytes we receive and only break where there are no more bytes in the inputStream passed to us
			if(currentStreamEnded) break; // The stream ends when we have read all bytes in the chunk NIO passed to us
			else {
				if(readingHeaderLines) {// We are in state to read header lines right now
					isPreviousLineCRLF = readMessageSipHeaderLines(inputStream, isPreviousLineCRLF);
				}
				if(readingMessageBodyContents) { // We've already read the headers an now we are reading the Contents of the SIP message (which doesn't generally have lines)
					readMessageBody(inputStream);
				}
			}
		}
	}
	
	private boolean readMessageSipHeaderLines(InputStream inputStream, boolean isPreviousLineCRLF) throws IOException {
		boolean crlfReceived = false;
		String line = readLine(inputStream); // This gives us a full line or if it didn't fit in the byte check it may give us part of the line
		if(partialLineRead) {
			partialLine = partialLine + line; // If we are reading partial line again we must concatenate it with the previous partial line to reconstruct the full line
		} else {
			line = partialLine + line; // If we reach the end of the line in this chunk we concatenate it with the partial line from the previous buffer to have a full line
			partialLine = ""; // Reset the partial line so next time we will concatenate empty string instead of the obsolete partial line that we just took care of
			if(!line.equals(CRLF)) { // CRLF indicates END of message headers by RFC
				message.append(line); // Collect the line so far in the message buffer (line by line)
                String lineIgnoreCase = line.toLowerCase();
                // contribution from Alexander Saveliev compare to lower case as RFC 3261 states (7.3.1 Header Field Format) states that header fields are case-insensitive
				if(lineIgnoreCase.startsWith(ContentLength.NAME_LOWER)) { // naive Content-Length header parsing to figure out how much bytes of message body must be read after the SIP headers
					contentLength = Integer.parseInt(line.substring(
							ContentLength.NAME_LOWER.length()+1).trim());
				} else if(lineIgnoreCase.startsWith(CallID.NAME_LOWER)) { // naive Content-Length header parsing to figure out how much bytes of message body must be read after the SIP headers
					callId = line.substring(
							CallID.NAME_LOWER.length()+1).trim();
				}
			} else {				
				if(isPreviousLineCRLF) {
            		// Handling keepalive ping (double CRLF) as defined per RFC 5626 Section 4.4.1
                	// sending pong (single CRLF)
                	if (logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
                        logger.logDebug("KeepAlive Double CRLF received, sending single CRLF as defined per RFC 5626 Section 4.4.1");
                        logger.logDebug("~~~ setting isPreviousLineCRLF=false");
                    }

                	crlfReceived = false;

                	try {
						sipMessageListener.sendSingleCLRF();
					} catch (Exception e) {						
						logger.logError("A problem occured while trying to send a single CLRF in response to a double CLRF", e);
					}                	                	
            	} else {
            		crlfReceived = true;
                	if (logger.isLoggingEnabled(LogLevels.TRACE_DEBUG)) {
                    	logger.logDebug("Received CRLF");
                    }
                	if(sipMessageListener != null && 
                			sipMessageListener instanceof ConnectionOrientedMessageChannel) {
                		((ConnectionOrientedMessageChannel)sipMessageListener).cancelPingKeepAliveTimeoutTaskIfStarted();
                	}
            	}
				if(message.length() > 0) { // if we havent read any headers yet we are between messages and ignore CRLFs
					readingMessageBodyContents = true;
					readingHeaderLines = false;
					partialLineRead = false;
					message.append(CRLF); // the parser needs CRLF at the end, otherwise fails TODO: Is that a bug?
					if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
						logger.logDebug("Content Length parsed is " + contentLength);
					}

					contentReadSoFar = 0;
					messageBody = new byte[contentLength];
				}
			}			
		}
		return crlfReceived;
	}

	// This method must be called repeatedly until the inputStream returns -1 or some error conditions is triggered
	private void readMessageBody(InputStream inputStream) throws IOException {
		int bytesRead = 0;
		if(contentLength>0) {
			bytesRead = readChunk(inputStream, messageBody, contentReadSoFar, contentLength-contentReadSoFar);
			if(bytesRead == -1) {
				currentStreamEnded = true;
				bytesRead = 0; // avoid passing by a -1 for a one-off bug when contentReadSoFar gets wrong
			}
		}
		contentReadSoFar += bytesRead;
		if(contentReadSoFar == contentLength) { // We have read the full message headers + body
			sizeCounter = maxMessageSize;
			readingHeaderLines = true;
			readingMessageBodyContents = false;
			final String msgLines = message.toString();
			message = new StringBuilder();
			final byte[] msgBodyBytes = messageBody;			
			
			if(sipStack.getSelfRoutingThreadpoolExecutor() != null) {
				final String callId = this.callId;
				if(callId == null || callId.trim().length() < 1) {
					// http://code.google.com/p/jain-sip/issues/detail?id=18
					// NIO Message with no Call-ID throws NPE
					throw new IOException("received message with no Call-ID");
				}
                                                                                
                sipStack.getSelfRoutingThreadpoolExecutor().execute(new Dispatch(new UnparsedMessage(msgLines, msgBodyBytes), callId)); // run in executor thread
			} else {
				SIPMessage sipMessage = null;
				
					try {
                        byte[] msgBytes = msgLines.getBytes("UTF-8");
                        sipMessage = smp.parseSIPMessage(msgBytes, false, false, null);
						sipMessage.setMessageContent(msgBodyBytes);
					} catch (ParseException e) {
						this.logger.logDebug(
		                        "Parsing issue !  " + new String(msgLines.getBytes("UTF-8")) + " " + e.getMessage());
					}
				
				this.contentLength = 0;
				processSIPMessage(sipMessage);
			}
		}

	}

	public void processSIPMessage(SIPMessage message) {
		try {
			sipMessageListener.processMessage(message);
		} catch (Exception e) {
			logger.logError("Can't process message", e);
		}
	}
	
	public synchronized void addBytes(byte[] bytes)  throws Exception{
		currentStreamEnded = false;
		ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
		readStream(inputStream);
	}


    
    /**
     * default constructor.
     */
    protected NioPipelineParser() {
        super();        
    }

    /**
     * Constructor when we are given a message listener and an input stream
     * (could be a TCP connection or a file)
     *
     * @param sipMessageListener
     *            Message listener which has methods that get called back from
     *            the parser when a parse is complete
     * @param in
     *            Input stream from which to read the input.
     * @param debug
     *            Enable/disable tracing or lexical analyser switch.
     */
    public NioPipelineParser(SIPTransactionStack sipStack, SIPMessageListener sipMessageListener,
             boolean debug, int maxMessageSize) {
        this();
        this.sipStack = sipStack;
        this.smp = sipStack.getMessageParserFactory().createMessageParser(sipStack);
        this.sipMessageListener = sipMessageListener;
        this.maxMessageSize = maxMessageSize;
        this.sizeCounter = this.maxMessageSize;

    }

    /**
     * This is the constructor for the pipelined parser.
     *
     * @param mhandler
     *            a SIPMessageListener implementation that provides the message
     *            handlers to handle correctly and incorrectly parsed messages.
     * @param in
     *            An input stream to read messages from.
     */

    public NioPipelineParser(SIPTransactionStack sipStack, SIPMessageListener mhandler,
            int maxMsgSize) {
        this(sipStack, mhandler, false, maxMsgSize);
    }

    /**
     * Add a class that implements a SIPMessageListener interface whose methods
     * get called * on successful parse and error conditons.
     *
     * @param mlistener
     *            a SIPMessageListener implementation that can react to correct
     *            and incorrect pars.
     */

    public void setMessageListener(SIPMessageListener mlistener) {
        sipMessageListener = mlistener;
    }
    
	private int readChunk(InputStream inputStream, byte[] where, int offset, int length) throws IOException {
		int read =  inputStream.read(where, offset, length);
		sizeCounter -= read;
		checkLimits();
		return read;
	}
	
	private int readSingleByte(InputStream inputStream) throws IOException {
		sizeCounter --;
		checkLimits();
		return inputStream.read();
	}
	
	private void checkLimits() {
		if(maxMessageSize > 0 && sizeCounter < 0) throw new RuntimeException("Max Message Size Exceeded " + maxMessageSize);
	}

    /**
     * read a line of input. Note that we encode the result in UTF-8
     */
    private String readLine(InputStream inputStream) throws IOException {
    	partialLineRead = false;
        int counter = 0;
        int increment = 1024;
        int bufferSize = increment;
        byte[] lineBuffer = new byte[bufferSize];
        // handles RFC 5626 CRLF keepalive mechanism
        byte[] crlfBuffer = new byte[2];
        int crlfCounter = 0;
        while (true) {
            char ch;
            int i = readSingleByte(inputStream);
            if (i == -1) {
                partialLineRead = true;
                currentStreamEnded = true;
                break;
            } else
                ch = (char) ( i & 0xFF);
            
            if (ch != '\r')
                lineBuffer[counter++] = (byte) (i&0xFF);
            else if (counter == 0)            	
            	crlfBuffer[crlfCounter++] = (byte) '\r';
                       
            if (ch == '\n') {
            	if(counter == 1 && crlfCounter > 0) {
            		crlfBuffer[crlfCounter++] = (byte) '\n';            		
            	} 
            	break;            	
            }
            
            if( counter == bufferSize ) {
                byte[] tempBuffer = new byte[bufferSize + increment];
                System.arraycopy((Object)lineBuffer,0, (Object)tempBuffer, 0, bufferSize);
                bufferSize = bufferSize + increment;
                lineBuffer = tempBuffer;
                
            }
        }
        if(counter == 1 && crlfCounter > 0) {
        	return new String(crlfBuffer,0,crlfCounter,"UTF-8");
        } else {
        	String lineRead = new String(lineBuffer,0,counter,"UTF-8");
                //In case \r\n are not in the same chunk, wait for the rest
                //fixes https://github.com/RestComm/jain-sip/issues/48
                if (crlfCounter == 1) {
                    lineRead = lineRead + "\r";
                }
                return lineRead;
        }
        
    }
    

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy