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

io.sipstack.netty.codec.sip.RawMessage Maven / Gradle / Ivy

The newest version!
/**
 * 
 */
package io.sipstack.netty.codec.sip;

import io.pkts.buffer.Buffer;
import io.pkts.buffer.Buffers;

import java.io.IOException;

/**
 * Represents a raw sip message coming off of the network.
 * 
 * @author [email protected]
 */
public final class RawMessage {

    public static final byte COLON = ':';

    public static final byte CR = '\r';

    public static final byte LF = '\n';

    public static final byte SP = ' ';

    public static final byte HTAB = '\t';

    /**
     * Every SIP message has an initial line, which is either a request line or
     * a response line. We don't care which but this is that one line...
     */
    private final byte[] initialLine;
    private int initalLineIndex = 0;

    /**
     * All the headers from the SIP message will be stored in this buffer.
     */
    private final byte[] headers;
    private int headersIndex = 0;

    /**
     * The payload of the SIP message.
     */
    private byte[] payload;
    private int payloadIndex = 0;

    /**
     * The content length as stated in the SIP message itself. For
     * connectionless protocols (such as UDP),the content length actually does
     * not have to be there (even though I haven't seen an implementation that
     * actually does this).
     * 
     * Negative 1 means that we haven't set it yet (unlike zero where we have
     * actually seen a content length header in the message)
     */
    private int contentLength = -1;

    /**
     * The maximum allowed content length.
     */
    private final int maxAllowedContentLength;

    private Byte lastByte = -1;

    private boolean crlf = false;

    private State state = State.INIT;

    private boolean done = false;

    /**
     * Used for detecting the content-length header
     */
    private final Buffer contentLengthBuffer = Buffers.wrap("Content-Length");
    int contentLengthIndex = 1; // because we will trigger on 'C'

    public enum State {
        /**
         * In the INIT state we will consume any CRLF we find. This is allowed
         * for stream based protocols according to RFC 3261.
         */
        INIT,
        /**
         * While in this state, we will consume the initial line in the SIP
         * message. We will not verify it is correct that is up to later stages
         * of the processing.
         */
        GET_INITIAL_LINE,

        /**
         * Consume all headers. Apart from the Content-Length header we will in
         * this state just seek to find the double CRLF indicating the
         * separation between headers and potentially a body. The headers are
         * merely just framed at this stage.
         */
        GET_HEADERS,

        /**
         * Once we have found the separator between the headers and the payload
         * we will just blindly a byte at a time until we have read the entire
         * payload. The length of the payload was discovered during the
         * GET_HEADERS phase where we were not only just consuming all headers
         * but we were looking for the Content-Length header as well.
         */
        GET_PAYLOAD,

        /**
         * Whenever we encounter something that potentially could be the start
         * of the Content-Length header we will enter this state. While in this
         * state, we will see if the next byte is the next expected byte in the
         * token "Content-Length" (for the long version of the header name). If
         * the next byte isn't what we expect, such in the case of e.g. Call-ID
         * where we would bail on 'a' or Content-Type where we would eventually
         * bail on the 'T', then we would once again move over to the state
         * {@link #GET_HEADERS}.
         */
        IS_CONTENT_LENGTH_HEADER, CONSUME_COLON, CONSUME_LWS, EXPECT_COLON, CONSUME_DIGIT;
    }

    /**
     * Creates a new holder for a raw SIP message.
     * 
     * @param maxAllowedInitialLineSize
     *            the maximum allowed size (in bytes) of the initial line.
     * @param maxAllowedHeaderSize
     *            the maximum allowed size (in bytes) of all the headers
     *            combined.
     * @param maxAllowedContentLength
     *            the maximum allowed size (in bytes) of the payload of the
     *            message.
     */
    public RawMessage(final int maxAllowedInitialLineSize, final int maxAllowedHeaderSize,
            final int maxAllowedContentLength) {
        this.initialLine = new byte[maxAllowedInitialLineSize];
        this.headers = new byte[maxAllowedHeaderSize];
        this.maxAllowedContentLength = maxAllowedContentLength;
    }

    private boolean isCRLF(final byte b) {
        if (this.lastByte == CR && b == LF) {
            this.crlf = true;
            return true;
        }
        return false;
    }

    private boolean doubleCRLF(final byte b) {
        return this.crlf && isCRLF(b);
    }

    public void write(final byte b) throws MaxMessageSizeExceededException, IOException {

        if (this.state == State.INIT) {
            if (b == CR || isCRLF(b)) {
                // ignore
            } else {
                this.state = State.GET_INITIAL_LINE;
                this.crlf = false;
                writeToInitialLine(b);
            }
        } else if (this.state == State.GET_INITIAL_LINE) {
            if (isCRLF(b)) {
                this.state = State.GET_HEADERS;
            } else if (b == CR) {
                this.crlf = false;
                // ignore
            } else {
                this.crlf = false;
                writeToInitialLine(b);
            }
        } else if (this.state == State.GET_HEADERS) {
            writeToHeaders(b);
            if (doubleCRLF(b)) {
                if (getContentLength() > 0) {
                    this.state = State.GET_PAYLOAD;
                } else {
                    this.state = State.GET_PAYLOAD;
                    this.done = true;
                }
            } else if (isCRLF(b)) {
                // ignore
            } else if (b == CR) {
                // ignore
            } else if (b == 'C') {
                this.state = State.IS_CONTENT_LENGTH_HEADER;
            } else {
                this.crlf = false;
            }
        } else if (this.state == State.IS_CONTENT_LENGTH_HEADER) {
            writeToHeaders(b);
            if (this.contentLengthBuffer.getByte(this.contentLengthIndex++) != b) {
                this.contentLengthIndex = 1;
                this.state = State.GET_HEADERS;
            } else if (this.contentLengthIndex == this.contentLengthBuffer.capacity()) {
                this.contentLengthIndex = 1;
                this.state = State.CONSUME_COLON;
            }
        } else if (this.state == State.CONSUME_COLON) {
            writeToHeaders(b);
            if (b == SP || b == HTAB) {
                // consume
            } else if (b == COLON) {
                this.state = State.CONSUME_DIGIT;
            } else {
                throw new RuntimeException("Expected HCOLON got " + Character.toString((char) b));
            }
        } else if (this.state == State.CONSUME_DIGIT) {
            writeToHeaders(b);
            if (this.contentLength == -1 && (b == SP || b == HTAB)) {
                // consume
            } else if (isDigit(b)) {
                if (this.contentLength == -1) {
                    this.contentLength = 0;
                }
                this.contentLength = this.contentLength * 10 + b - 48;
            } else {
                setContentLength(this.contentLength);
                this.state = State.GET_HEADERS;
            }
        } else if (this.state == State.GET_PAYLOAD) {
            writeToPayload(b);
            if (payloadCompleted()) {
                this.done = true;
            }
        } else {
            throw new RuntimeException("Unknown state " + this.state);
        }

        this.lastByte = b;
    }

    public static boolean isDigit(final char ch) {
        return ch >= 48 && ch <= 57;
    }

    public static boolean isDigit(final byte b) {
        return isDigit((char) b);
    }

    public boolean isComplete() {
        return this.done;
    }

    /**
     * Get the initial line of this raw message.
     * 
     * @return
     */
    public Buffer getInitialLine() {
        return Buffers.wrap(this.initialLine, 0, this.initalLineIndex);
    }

    /**
     * Write a byte to the initial line. if the operation succeeds, true will be
     * returned but if we hit the maximum allocated length of the initial line,
     * then we will return false. The reason for this upper boundary is that you
     * don't want an attacker to have you look for the initial line forever so
     * once we hit our maximum allowed size of the initial line we will give up
     * and the connection should be dropped.
     * 
     * @param b
     * @return
     */
    public void writeToInitialLine(final byte b) throws MaxMessageSizeExceededException {
        try {
            this.initialLine[this.initalLineIndex++] = b;
        } catch (final IndexOutOfBoundsException e) {
            throw new MaxMessageSizeExceededException("Maximum initial line exceeded");
        }
    }

    /**
     * Get all the raw headers of this message
     * 
     * @return
     */
    public Buffer getHeaders() {
        return Buffers.wrap(this.headers, 0, this.headersIndex);
    }

    /**
     * Write a byte to the header section of this raw message.
     * 
     * @param b
     *            the byte to write
     * @return true if there were still space in the header section, false
     *         otherwise
     */
    public void writeToHeaders(final byte b) throws MaxMessageSizeExceededException {
        try {
            this.headers[this.headersIndex++] = b;
        } catch (final IndexOutOfBoundsException e) {
            throw new MaxMessageSizeExceededException("Maximum allowed header size exceeded");
        }
    }

    /**
     * Get the payload of this raw message.
     * 
     * @return the payload or null if there is no payload.
     */
    public Buffer getPayload() {
        if (this.payloadIndex == 0) {
            return null;
        }
        return Buffers.wrap(this.payload);
    }

    /**
     * Write the next byte to the payload section of this message.
     * 
     * @param b
     * @throws MaxMessageSizeExceededException
     */
    public void writeToPayload(final byte b) throws MaxMessageSizeExceededException {
        try {
            this.payload[this.payloadIndex++] = b;
        } catch (final IndexOutOfBoundsException e) {
            throw new MaxMessageSizeExceededException("Maximum allowed payload size exceeded");
        }
    }

    /**
     * For stream based protocols there could be another message coming directly
     * after this one so you just cant read until the {@link ChannelBuffer} runs
     * out of bytes. Therefore, you must check how much you have read "manually"
     * and based on that decide whether you are done with processing the payload
     * of this message or not.
     * 
     * @return
     */
    public int getPayloadWriteIndex() {
        // return this.payload.getWriterIndex();
        return this.payloadIndex;
    }

    /**
     * Check whether we have read the entire payload.
     * 
     * @return
     */
    public boolean payloadCompleted() {
        return this.payloadIndex == this.contentLength;
        // return !this.payload.hasWritableBytes();
    }

    /**
     * Get the content length of this message. A return value of -1 (negative 1)
     * means that we do not know how big the payload of the message is. This
     * also means that we still haven't located the Content-Length header within
     * the message (if there is one).
     * 
     * @return
     */
    public int getContentLength() {
        return this.contentLength;
    }

    /**
     * Set the expected content length of this message. This value comes from
     * the Content-Length header and if it is exceeding the maximum allowed
     * payload length then we will throw a
     * {@link MaxMessageSizeExceededException}.
     * 
     * @param contentLength
     *            the content length of the payload
     * @throws MaxMessageSizeExceededException
     *             in case the payload exceeds the maximum allowed length.
     */
    public void setContentLength(final int contentLength) throws MaxMessageSizeExceededException {
        if (contentLength > this.maxAllowedContentLength) {
            throw new MaxMessageSizeExceededException("Content length exceeds the maximum allowed length");
        }
        this.contentLength = contentLength;
        this.payload = new byte[contentLength];
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append(this.initialLine.toString());
        sb.append(this.headers.toString());
        sb.append(this.payload.toString());
        return sb.toString();
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy