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

io.pkts.packet.sip.impl.SipParser Maven / Gradle / Ivy

There is a newer version: 3.0.10
Show newest version
/**
 *
 */
package io.pkts.packet.sip.impl;

import io.pkts.buffer.Buffer;
import io.pkts.buffer.Buffers;
import io.pkts.packet.sip.SipMessage;
import io.pkts.packet.sip.SipParseException;
import io.pkts.packet.sip.header.CSeqHeader;
import io.pkts.packet.sip.header.CallIdHeader;
import io.pkts.packet.sip.header.ContactHeader;
import io.pkts.packet.sip.header.ContentLengthHeader;
import io.pkts.packet.sip.header.ContentTypeHeader;
import io.pkts.packet.sip.header.ExpiresHeader;
import io.pkts.packet.sip.header.FromHeader;
import io.pkts.packet.sip.header.MaxForwardsHeader;
import io.pkts.packet.sip.header.RecordRouteHeader;
import io.pkts.packet.sip.header.RouteHeader;
import io.pkts.packet.sip.header.SipHeader;
import io.pkts.packet.sip.header.ToHeader;
import io.pkts.packet.sip.header.ViaHeader;
import io.pkts.packet.sip.header.impl.SipHeaderImpl;

import java.io.IOException;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * Basic sip parser that contains most (all?) of the different grammar rules for SIP as defined by
 * RFC 3261. View these various methods as building blocks for building up a complete SIP parser
 * (perhaps I should rename this class?).
 *
 * All of these functions work in the following way:
 * 
    *
  • consumeXXXX - will (in general) simply try and consume whatever it is supposed to * consume and the function will return true of false depending on whether is was able to consume * anything from the {@link Buffer}. The consume-functions will (if successful) move the reader * index of the {@link Buffer}. If unsuccessful, the reader index will be left untouched.
  • *
  • expectXXX - will (in general) expect that the next thing is whatever it is supposed to * expect. These functions are really the same as the consumeXXXX ones but instead of returning true * or false the expect-functions will throw a {@link SipParseException} to indicate that things * didn't turn out as we were hoping for. Also, remember that the {@link SipParseException} contains * the error offset into the {@link Buffer} where things broke. As with the consume-functions, the * expect-functions will (if successful) move the reader index of the {@link Buffer}
  • *
  • *
* * @author [email protected] */ public class SipParser { private static final String UNABLE_TO_READ_FROM_STREAM = "Unable to read from stream"; /** * There are many situations where you are looking to frame something but * you cannot find the terminating condition. Since an attacker could easily * just have you read a never ending stream we need at some point give up * and abort. */ public static final int MAX_LOOK_AHEAD = 1024; public static final Buffer INVITE = Buffers.wrap("INVITE"); public static final Buffer ACK = Buffers.wrap("ACK"); public static final Buffer CANCEL = Buffers.wrap("CANCEL"); public static final Buffer BYE = Buffers.wrap("BYE"); public static final Buffer SUBSCRIBE = Buffers.wrap("SUBSCRIBE"); public static final Buffer NOTIFY = Buffers.wrap("NOTIFY"); public static final Buffer PUBLISH = Buffers.wrap("PUBLISH"); public static final Buffer INFO = Buffers.wrap("INFO"); public static final Buffer OPTIONS = Buffers.wrap("OPTIONS"); public static final Buffer REGISTER = Buffers.wrap("REGISTER"); public static final Buffer PRACK = Buffers.wrap("PRACK"); public static final Buffer REFER = Buffers.wrap("REFER"); public static final Buffer MESSAGE = Buffers.wrap("MESSAGE"); public static final Buffer UPDATE = Buffers.wrap("UPDATE"); public static final Buffer TAG = Buffers.wrap("tag"); public static final Buffer USER = Buffers.wrap("user"); public static final Buffer TTL = Buffers.wrap("ttl"); public static final Buffer MADDR = Buffers.wrap("maddr"); public static final Buffer METHOD = Buffers.wrap("method"); public static final Buffer TRANSPORT = Buffers.wrap("transport"); public static final Buffer TRANSPORT_EQ = Buffers.wrap("transport="); public static final Buffer SIP2_0 = Buffers.wrap("SIP/2.0"); public static final Buffer SIP2_0_SLASH = Buffers.wrap("SIP/2.0/"); public static final Buffer SCHEME_SIP = Buffers.wrap("sip"); public static final Buffer SCHEME_SIP_COLON = Buffers.wrap("sip:"); public static final Buffer SCHEME_SIPS = Buffers.wrap("sips"); public static final Buffer SCHEME_SIPS_COLON = Buffers.wrap("sips:"); public static final Buffer SCHEME_TEL = Buffers.wrap("tel"); public static final Buffer SCHEME_TEL_COLON = Buffers.wrap("tel:"); public static final byte AT = '@'; public static final byte COLON = ':'; public static final byte SEMI = ';'; public static final byte DOUBLE_QOUTE = '"'; public static final byte CR = '\r'; public static final byte LF = '\n'; public static final byte SP = ' '; public static final byte HTAB = '\t'; public static final byte DASH = '-'; public static final byte PERIOD = '.'; public static final byte COMMA = ','; public static final byte EXCLAMATIONPOINT = '!'; public static final byte PERCENT = '%'; public static final byte STAR = '*'; public static final byte UNDERSCORE = '_'; public static final byte QUESTIONMARK = '?'; public static final byte PLUS = '+'; public static final byte BACKTICK = '`'; public static final byte TICK = '\''; public static final byte TILDE = '~'; public static final byte EQ = '='; public static final byte SLASH = '/'; public static final byte BACK_SLASH = '\\'; /** * Left parenthesis */ public static final byte LPAREN = '('; /** * Right parenthesis */ public static final byte RPAREN = ')'; /** * Right angle quote */ public static final byte RAQUOT = '>'; /** * Left angle quote */ public static final byte LAQUOT = '<'; /** * Double quotation mark */ public static final byte DQUOT = '"'; public static final Buffer UDP = Buffers.wrap("udp"); public static final Buffer TCP = Buffers.wrap("tcp"); public static final Buffer TLS = Buffers.wrap("tls"); public static final Buffer SCTP = Buffers.wrap("sctp"); public static final Buffer WS = Buffers.wrap("ws"); public static final Buffer WSS = Buffers.wrap("wss"); public static final Map> framers = new HashMap<>(); static { framers.put(CallIdHeader.NAME, header -> CallIdHeader.frame(header.getValue())); framers.put(CallIdHeader.COMPACT_NAME, header -> CallIdHeader.frameCompact(header.getValue())); framers.put(ContactHeader.NAME, header -> ContactHeader.frame(header.getValue())); framers.put(ContactHeader.COMPACT_NAME, header -> ContactHeader.frame(header.getValue())); framers.put(ContentTypeHeader.NAME, header -> ContentTypeHeader.frame(header.getValue())); framers.put(ContentTypeHeader.COMPACT_NAME, header -> ContentTypeHeader.frame(header.getValue())); framers.put(ContentLengthHeader.NAME, header -> ContentLengthHeader.frame(header.getValue())); framers.put(ContentLengthHeader.COMPACT_NAME, header -> ContentLengthHeader.frame(header.getValue())); framers.put(CSeqHeader.NAME, header -> CSeqHeader.frame(header.getValue())); framers.put(ExpiresHeader.NAME, header -> ExpiresHeader.frame(header.getValue())); framers.put(FromHeader.NAME, header -> FromHeader.frame(header.getValue())); framers.put(FromHeader.COMPACT_NAME, header -> FromHeader.frame(header.getValue())); framers.put(MaxForwardsHeader.NAME, header -> MaxForwardsHeader.frame(header.getValue())); framers.put(RecordRouteHeader.NAME, header -> RecordRouteHeader.frame(header.getValue())); framers.put(RouteHeader.NAME, header -> RouteHeader.frame(header.getValue())); framers.put(ToHeader.NAME, header -> ToHeader.frame(header.getValue())); framers.put(ToHeader.COMPACT_NAME, header -> ToHeader.frame(header.getValue())); framers.put(ViaHeader.NAME, header -> ViaHeader.frame(header.getValue())); framers.put(ViaHeader.COMPACT_NAME, header -> ViaHeader.frame(header.getValue())); } /** * For the given header name, return a function that will convert a generic header instance * into one with the correct subtype. */ public static Function getFramer(final Buffer b) { // For headers that have the expected capitalization, do a quick case-sensitive // search. If that fails do a slower case-insensitive search. final Function framer = framers.get(b); if (framer != null) { return framer; } for (final Map.Entry> entry : framers.entrySet()) { if (entry.getKey().equalsIgnoreCase(b)) { return entry.getValue(); } } return null; } // ---------------------------------------------------------------------- // ---------------------------------------------------------------------- // -------- Expect methods expects something to be true and if not ------ // -------- they will throw an exception of some sort ------------------- // ---------------------------------------------------------------------- // ---------------------------------------------------------------------- /** * Expect that the next set of bytes is "SIP/2.0" and if not then we will * throw a {@link SipParseException} * * @param buffer */ public static void expectSIP2_0(final Buffer buffer) throws SipParseException { expect(buffer, 'S'); expect(buffer, 'I'); expect(buffer, 'P'); expect(buffer, '/'); expect(buffer, '2'); expect(buffer, '.'); expect(buffer, '0'); } /** * * @return */ public static Buffer expectMethod(final Buffer buffer) { return null; } /** * Check whether the buffer is exactly three bytes long and has the bytes "UDP" in it. * * @param t * @return */ public static boolean isUDP(final Buffer t) { try { return t.capacity() == 3 && t.getByte(0) == 'U' && t.getByte(1) == 'D' && t.getByte(2) == 'P'; } catch (final IOException e) { return false; } } public static boolean isTCP(final Buffer t) { try { return t.capacity() == 3 && t.getByte(0) == 'T' && t.getByte(1) == 'C' && t.getByte(2) == 'P'; } catch (final IOException e) { return false; } } public static boolean isTLS(final Buffer t) { try { return t.capacity() == 3 && t.getByte(0) == 'T' && t.getByte(1) == 'L' && t.getByte(2) == 'S'; } catch (final IOException e) { return false; } } public static boolean isWS(final Buffer t) { try { return t.capacity() == 2 && t.getByte(0) == 'W' && t.getByte(1) == 'S'; } catch (final IOException e) { return false; } } public static boolean isWSS(final Buffer t) { try { return t.capacity() == 3 && t.getByte(0) == 'W' && t.getByte(1) == 'S' && t.getByte(2) == 'S'; } catch (final IOException e) { return false; } } public static boolean isSCTP(final Buffer t) { try { return t.capacity() == 4 && t.getByte(0) == 'S' && t.getByte(1) == 'C' && t.getByte(2) == 'T' && t.getByte(3) == 'P'; } catch (final IOException e) { return false; } } /** * Check whether the buffer is exactly three bytes long and has the * bytes "udp" in it. Note, in SIP there is a different between transport * specified in a Via-header and a transport-param specified in a SIP URI. * One is upper case, one is lower case. Another really annoying thing * with SIP. * * @param t * @return */ public static boolean isUDPLower(final Buffer t) { try { return t.capacity() == 3 && t.getByte(0) == 'u' && t.getByte(1) == 'd' && t.getByte(2) == 'p'; } catch (final IOException e) { return false; } } public static boolean isTCPLower(final Buffer t) { try { return t.capacity() == 3 && t.getByte(0) == 't' && t.getByte(1) == 'c' && t.getByte(2) == 'p'; } catch (final IOException e) { return false; } } public static boolean isTLSLower(final Buffer t) { try { return t.capacity() == 3 && t.getByte(0) == 't' && t.getByte(1) == 'l' && t.getByte(2) == 's'; } catch (final IOException e) { return false; } } public static boolean isWSLower(final Buffer t) { try { return t.capacity() == 2 && t.getByte(0) == 'w' && t.getByte(1) == 's'; } catch (final IOException e) { return false; } } public static boolean isWSSLower(final Buffer t) { try { return t.capacity() == 3 && t.getByte(0) == 'w' && t.getByte(1) == 's' && t.getByte(2) == 's'; } catch (final IOException e) { return false; } } public static boolean isSCTPLower(final Buffer t) { try { return t.capacity() == 4 && t.getByte(0) == 's' && t.getByte(1) == 'c' && t.getByte(2) == 't' && t.getByte(3) == 'p'; } catch (final IOException e) { return false; } } public static void expectTel(final Buffer buffer) throws SipParseException, IOException { expect(buffer, 't'); expect(buffer, 'e'); expect(buffer, 'l'); expect(buffer, SipParser.COLON); } public static boolean isSips(final Buffer buffer) throws SipParseException, IndexOutOfBoundsException, IOException { SipParser.expect(buffer, 's'); SipParser.expect(buffer, 'i'); SipParser.expect(buffer, 'p'); final byte b = buffer.readByte(); if (b == SipParser.COLON) { return false; } if (b != 's') { throw new SipParseException(buffer.getReaderIndex() - 1, "Expected 's' since the only schemes accepted are \"sip\" and \"sips\""); } SipParser.expect(buffer, SipParser.COLON); return true; } /** * Consumes all generic-params it can find. If there are no generic params * to be consumed it will return an empty list. The list of generic-params * are found on many SIP headers such as the To, From etc. In general, the * BNF looks like so: * * *
     *  *( SEMI generic-param )
     * 
* * E.g. in To: * *
     * To        =  ( "To" / "t" ) HCOLON ( name-addr / addr-spec ) *( SEMI to-param )
     * to-param  =  tag-param / generic-param
     * 
* * @param buffer * @return * @throws IOException * @throws IndexOutOfBoundsException */ public static List consumeGenericParams(final Buffer buffer) throws IndexOutOfBoundsException, IOException { final List params = new ArrayList(); while (buffer.hasReadableBytes() && buffer.peekByte() == SEMI) { buffer.readByte(); // consume the SEMI params.add(consumeGenericParam(buffer)); } return params; } /** * Consumes a generic param, which according to RFC 3261 section 25.1 is: * *
     * generic-param  =  token [ EQUAL gen-value ]
     * gen-value      =  token / host / quoted-string
     * 
* * The return value is two buffers returned as an array and if the second * element is null (so make sure you check it!) then there was no value for * this parameter. * * Also note that due to poor implementations out there, the following is * accepted as valid input: * * foo= * * I.e., foo is a flag parameter and as such there should not be an equal * sign following it but there are many servers out there that does this * anyway. * * @param buffer * the buffer from which we will consume a generic-param * @return a buffer array of size two. The first element is the name of the * parameter and the second element is the value of that parameter * or null if the parameter was a flag parameter (didn't have a * value) * @throws IOException * @throws IndexOutOfBoundsException */ public static Buffer[] consumeGenericParam(final Buffer buffer) throws IndexOutOfBoundsException, IOException { final Buffer key = consumeToken(buffer); Buffer value = null; if (key == null) { return new Buffer[2]; } if (consumeEQUAL(buffer) > 0) { // TODO: consume host and quoted string if (isNext(buffer, DOUBLE_QOUTE)) { value = consumeQuotedString(buffer); } else { value = consumeToken(buffer); } } return new Buffer[]{ key, value}; } /** * Will check whether the next readable byte in the buffer is a certain byte * * @param buffer * the buffer to peek into * @param b * the byte we are checking is the next byte in the buffer to be * read * @return true if the next byte to read indeed is what we hope it is */ public static boolean isNext(final Buffer buffer, final byte b) throws IOException { if (buffer.hasReadableBytes()) { final byte actual = buffer.peekByte(); return actual == b; } return false; } /** * Check whether the next byte is a digit or not * * @param buffer * @retur * @throws IOException * @throws IndexOutOfBoundsException */ public static boolean isNextDigit(final Buffer buffer) throws IndexOutOfBoundsException, IOException { if (buffer.hasReadableBytes()) { final char next = (char) buffer.peekByte(); return next >= 48 && next <= 57; } return false; } /** * Will expect at least 1 digit and will continue consuming bytes until a * non-digit is encountered * * @param buffer * the buffer to consume the digits from * @return the buffer containing digits only * @throws SipParseException * in case there is not one digit at the first position in the * buffer (where the current reader index is pointing to) * @throws IOException * @throws IndexOutOfBoundsException */ public static Buffer expectDigit(final Buffer buffer) throws SipParseException { final int start = buffer.getReaderIndex(); try { while (buffer.hasReadableBytes() && isNextDigit(buffer)) { // consume it buffer.readByte(); } if (start == buffer.getReaderIndex()) { throw new SipParseException(start, "Expected digit"); } return buffer.slice(start, buffer.getReaderIndex()); } catch (final IndexOutOfBoundsException e) { throw new SipParseException(start, "Expected digit but no more bytes to read"); } catch (final IOException e) { throw new SipParseException(start, "Expected digit unable to read from underlying stream"); } } /** * Convenience method for expecting (and consuming) a HCOLON, which is * defined as: * * HCOLON = *( SP / HTAB ) ":" SWS * * See RFC3261 section 25.1 Basic Rules */ public static int expectHCOLON(final Buffer buffer) throws SipParseException { final int consumed = expectHCOLONStreamFriendly(buffer); if (consumed == -1) { // -1 means we ran out of bytes in the stream but in those // cases where we are not really dealing with a stream we // would expect an IndexOutOfBoundsException so let's make // sure to still honor that... throw new IndexOutOfBoundsException(); } return consumed; } /** * A problem with stream based protocols is that you don't have everything * available right away so e.g. when expecting HCOLON you may consume white spaces * but then you run out of bytes because you haven't received them yet * so therefore the expect(buffer, COLON) will blow up for the wrong reasons. * It may very well be that the next byte on the wire is indeed a ':' and as * such you should rather signal that you didn't have enough bytes to do your * job. This one does that. * * Note, still some issues with this one though. Let's say the header * we are trying to parse is "X-Hello : whatever" and we have * only received "X-Hello :" so far. So first off we consume "X-Hello" * then call this method, which will consume " :" but ideally we also * want to consume the white space after the colon. I.e., when we * go off and consume the value of this header we will end up with * white space which normally should have been consumed by this method. * * Hence, whenever dealing with streams you will run into this issue * and will have to keep extra state around to solve it. See the * {@link SipMessageStreamBuilder} for how it is dealing with this. * * @param buffer * @return * @throws SipParseException */ public static int expectHCOLONStreamFriendly(final Buffer buffer) throws SipParseException { try { int consumed = consumeWS(buffer); if (buffer.hasReadableBytes()) { expect(buffer, COLON); } else { // normally the expect(buffer, COLON) would have thrown // an IndexOutOfBoundsException but let's signal that as // -1 return -1; } ++consumed; consumed += consumeSWSAfterHColon(buffer); return consumed; } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), UNABLE_TO_READ_FROM_STREAM, e); } } public static int consumeSWSAfterHColon(final Buffer buffer) throws IOException { // Part of the fix for empty header values. Which based on the BNF I don't // think is legal but it is certainly happening in the wild // so need to accept it. int consumed = consumeWS(buffer); if (!isNext(buffer, CR)) { consumed += consumeSWS(buffer); } return consumed; } /** * Convenience method for expecting (and consuming) a SLASH, which is * defined as: * * * See RFC3261 section 25.1 Basic Rules * * @param buffer * @throws SipParseException */ public static void expectSLASH(final Buffer buffer) throws SipParseException { try { final int count = consumeSLASH(buffer); if (count == 0) { throw new SipParseException(buffer.getReaderIndex(), "Expected SLASH"); } } catch (final IndexOutOfBoundsException e) { throw new SipParseException(buffer.getReaderIndex(), "Expected SLASH but nothing more to read", e); } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), "Expected SLASH but problem reading from stream", e); } } /** * Consume an slash (SLASH), which according to RFC3261 section 25.1 Basic * Rules is: * * SLASH = SWS "/" SWS ; slash * * @param buffer * @return true if we indeed did consume a SLASH * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeSLASH(final Buffer buffer) throws SipParseException, IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, SLASH); } /** * Expect the next byte to be a white space * * @param buffer */ public static int expectWS(final Buffer buffer) throws SipParseException { int consumed = 0; try { if (buffer.hasReadableBytes()) { final byte b = buffer.getByte(buffer.getReaderIndex()); if (b == SP || b == HTAB) { // ok, it was a WS so consume the byte buffer.readByte(); ++consumed; } else { throw new SipParseException(buffer.getReaderIndex(), "Expected WS"); } } else { throw new SipParseException(buffer.getReaderIndex(), "Expected WS but nothing more to read in the buffer"); } } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), UNABLE_TO_READ_FROM_STREAM, e); } return consumed; } /** * Consume sep (separating?) whitespace (LWS), which according to RFC3261 * section 25.1 Basic Rules is: * * SWS = [LWS] ; sep whitespace * * "The SWS construct is used when linear white space is optional, generally * between tokens and separators." (RFC3261) * * @param buffer * @return */ public static int consumeSWS(final Buffer buffer) { try { return consumeLWS(buffer); } catch (final SipParseException e) { // ignore since currently the consumeLWS will ONLY // throw this exception when it expected a WS at a // particular place and since SWS is all optional // we will silently just consume it, but return // false however return 0; } } /** * Consume an asterisk/star (STAR), which according to RFC3261 section 25.1 * Basic Rules is: * * STAR = SWS "*" SWS ; asterisk * * @param buffer * @return true if we indeed did consume a STAR * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeSTAR(final Buffer buffer) throws SipParseException, IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, STAR); } /** * Consume equal sign (EQUAL), which according to RFC3261 section 25.1 Basic * Rules is: * * EQUAL = SWS "=" SWS ; equal * * @param buffer * @return * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeEQUAL(final Buffer buffer) throws IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, EQ); } /** * Consume left parenthesis (LPAREN), which according to RFC3261 section * 25.1 Basic Rules is: * * LPAREN = SWS "(" SWS ; left parenthesis * * @param buffer * @return * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeLPAREN(final Buffer buffer) throws IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, LPAREN); } /** * Consume right parenthesis (RPAREN), which according to RFC3261 section * 25.1 Basic Rules is: * * RPAREN = SWS ")" SWS ; right parenthesis * * @param buffer * @return * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeRPAREN(final Buffer buffer) throws IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, RPAREN); } /** * Consume right angle quote (RAQUOT), which according to RFC3261 section * 25.1 Basic Rules is: * * RAQUOT = SWS ">"; left angle quote * * @param buffer * @return * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeRAQUOT(final Buffer buffer) throws IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, RAQUOT); } /** * Consume left angle quote (LAQUOT), which according to RFC3261 section * 25.1 Basic Rules is: * * LAQUOT = SWS "<"; left angle quote * * @param buffer * @return * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeLAQUOT(final Buffer buffer) throws IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, LAQUOT); } /** * Consume comma (COMMA), which according to RFC3261 section 25.1 Basic * Rules is: * * COMMA = SWS "," SWS ; comma * * @param buffer * @return true if we indeed did consume a COMMA * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeCOMMA(final Buffer buffer) throws SipParseException, IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, COMMA); } /** * Consume semicolon (SEMI), which according to RFC3261 section 25.1 Basic * Rules is: * * SEMI = SWS ";" SWS ; semicolon * * @param buffer * @return true if we indeed did consume a SEMI * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeSEMI(final Buffer buffer) throws SipParseException, IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, SEMI); } /** * Consume colon (COLON), which according to RFC3261 section 25.1 Basic * Rules is: * * COLON = SWS ":" SWS ; colon * * @param buffer * @return true if we indeed did consume a COLON * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeCOLON(final Buffer buffer) throws SipParseException, IndexOutOfBoundsException, IOException { return consumeSeparator(buffer, COLON); } /** * Consume open double quotation mark (LDQUT), which according to RFC3261 * section 25.1 Basic Rules is: * * LDQUOT = SWS DQUOTE; open double quotation mark * * @param buffer * @return true if we indeed did consume a LDQUOT * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeLDQUOT(final Buffer buffer) throws SipParseException, IndexOutOfBoundsException, IOException { buffer.markReaderIndex(); int consumed = consumeSWS(buffer); if (isNext(buffer, DQUOT)) { buffer.readByte(); ++consumed; } else { buffer.resetReaderIndex(); return 0; } return consumed; } /** * Consume close double quotation mark (RDQUT), which according to RFC3261 * section 25.1 Basic Rules is: * * RDQUOT = DQUOTE SWS ; close double quotation mark * * @param buffer * @return true if we indeed did consume a LDQUOT * @throws IOException * @throws IndexOutOfBoundsException */ public static int consumeRDQUOT(final Buffer buffer) throws SipParseException, IndexOutOfBoundsException, IOException { buffer.markReaderIndex(); int consumed = 0; if (isNext(buffer, DQUOT)) { buffer.readByte(); ++consumed; } else { buffer.resetReaderIndex(); return 0; } consumed += consumeSWS(buffer); return consumed; } /** * Helper function for checking stuff as described below. It is all the same pattern so... * (from rfc 3261 section 25.1) * * When tokens are used or separators are used between elements, * whitespace is often allowed before or after these characters: * * STAR = SWS "*" SWS ; asterisk * SLASH = SWS "/" SWS ; slash * EQUAL = SWS "=" SWS ; equal * LPAREN = SWS "(" SWS ; left parenthesis * RPAREN = SWS ")" SWS ; right parenthesis * RAQUOT = ">" SWS ; right angle quote * LAQUOT = SWS "<"; left angle quote * COMMA = SWS "," SWS ; comma * SEMI = SWS ";" SWS ; semicolon * COLON = SWS ":" SWS ; colon * LDQUOT = SWS DQUOTE; open double quotation mark * RDQUOT = DQUOTE SWS ; close double quotation mark * @param buffer * @param b * @return the number of bytes that was consumed. * @throws IOException * @throws IndexOutOfBoundsException */ private static int consumeSeparator(final Buffer buffer, final byte b) throws IndexOutOfBoundsException, IOException { buffer.markReaderIndex(); int consumed = consumeSWS(buffer); if (isNext(buffer, b)) { buffer.readByte(); ++consumed; } else { buffer.resetReaderIndex(); return 0; } consumed += consumeSWS(buffer); return consumed; } /** * Expects a token, which according to RFC3261 section 25.1 Basic Rules is: * * token = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+" / "`" / "'" * / "~" ) * * @param buffer * @return the buffer containing the expected token * @throws IOException * @throws IndexOutOfBoundsException * @throws SipParseException * in case there is no token */ public static Buffer expectToken(final Buffer buffer) throws IndexOutOfBoundsException, IOException, SipParseException { final Buffer token = consumeToken(buffer); if (token == null) { throw new SipParseException(buffer.getReaderIndex(), "Expected TOKEN"); } return token; } /** * Consumes a quoted-string, which is defined as: * *
     * quoted-string  =  SWS DQUOTE *(qdtext / quoted-pair ) DQUOTE
     * qdtext         =  LWS / %x21 / %x23-5B / %x5D-7E / UTF8-NONASCII
     * quoted-pair    =  "\" (%x00-09 / %x0B-0C / %x0E-7F)
     * 
* * Note, this is a somewhat simplified version and we'll * * @param buffer * @return * @throws IOException * @throws SipParseException */ public static Buffer consumeQuotedString(final Buffer buffer) throws SipParseException, IOException { final int start = buffer.getReaderIndex(); expect(buffer, DQUOT); while (buffer.hasReadableBytes()) { final byte b = buffer.readByte(); if (b == DQUOT) { break; } else if (b == BACK_SLASH) { buffer.readByte(); } } final int stop = buffer.getReaderIndex(); buffer.setReaderIndex(start + 1); final Buffer result = buffer.readBytes(stop - start - 2); buffer.setReaderIndex(stop); return result; } /** * The display name in SIP is a little tricky since it may or may not be * there and the stuff following it (whether or not it was there to begin * with) can easily be confused with being a display name. * * Note, a display name in SIP is only part of of the "name-addr" construct * and this function assumes that (even though it actually would work like a * regular {@link #consumeToken(Buffer)} in some cases) * *
     * name-addr      =  [ display-name ] LAQUOT addr-spec RAQUOT
     * display-name   =  *(token LWS)/ quoted-string
     * 
* * @param buffer * @return the display name or null if there was none * @throws IOException * @throws SipParseException */ public static Buffer consumeDisplayName(final Buffer buffer) throws IOException, SipParseException { if (isNext(buffer, DQUOT)) { return consumeQuotedString(buffer); } final int count = getTokenCount(buffer); if (count == 0) { return Buffers.EMPTY_BUFFER; } // now, if the next thing after the count is a ':' then // we mistook the scheme for a token so bail out. buffer.markReaderIndex(); final Buffer potentialDisplayName = buffer.readBytes(count); if (isNext(buffer, COLON)) { buffer.resetReaderIndex(); return Buffers.EMPTY_BUFFER; } // all good... return potentialDisplayName; } public static Buffer consumeAddressSpec(final Buffer buffer) throws IndexOutOfBoundsException, IOException, SipParseException { return consumeAddressSpec(false, buffer); } /** * Consumes addr-spec, which according to RFC3261 section 25.1 is: * *
     * addr-spec      =  SIP-URI / SIPS-URI / absoluteURI
     * SIP-URI          =  "sip:" [ userinfo ] hostport
     *                      uri-parameters [ headers ]
     * SIPS-URI         =  "sips:" [ userinfo ] hostport
     *                      uri-parameters [ headers ]
     *
     * absoluteURI    =  scheme ":" ( hier-part / opaque-part )
     * 
*

* And as you can see, it gets complicated. Also, these consume-functions * are not to validate the exact grammar but rather to find the boundaries * so the strategy for consuming the addr-spec is: *

    *
  • If '>' is encountered, then we assume that this addr-spec is within a * name-addr so we stop here
  • *
  • If a white space or end-of-line is encountered, we also assume we are * done UNLESS the calling code knows that we are in a protected "< >" then * we should allow for white space. See issue-106
  • *
* *

* Because of issue-106 the isProtected * boolean was introduced to allow for spaces after the initial ";' when we know the entire * addr-spec is e.g. "protected" inside of angle brackets "< >", which is often the case * when we parse out a name-addr, as we do in {@link io.pkts.packet.sip.address.Address#frame(Buffer)}. *

* * @return * @throws IOException * @throws IndexOutOfBoundsException * @throws SipParseException in case we cannot successfully frame the addr-spec. */ public static Buffer consumeAddressSpec(final boolean isProtected, final Buffer buffer) throws IndexOutOfBoundsException, IOException, SipParseException { final int startIndex = buffer.getReaderIndex(); int count = 0; int state = 0; // zero is to look for colon, everything else is to find the end boolean done = false; while (buffer.hasReadableBytes() && !done) { ++count; final byte b = buffer.readByte(); // emergency breaks... if (state == 0 && count > 99) { throw new SipParseException(buffer.getReaderIndex(), "No scheme found after 100 bytes, giving up"); } else if (count > MAX_LOOK_AHEAD) { throw new SipParseException(buffer.getReaderIndex(), "Have not been able to find the entire addr-spec after " + count + " bytes, giving up"); } else if (state == 0 && b == COLON) { state = 1; } else if (state == 1 && (b == RAQUOT || b == CR || b == LF)) { done = true; --count; } else if (!isProtected && state == 1 && (b == SP || b == HTAB)) { // https://github.com/aboutsip/pkts/issues/106 done = true; --count; } } buffer.setReaderIndex(startIndex); // didn't find the scheme portion if (state == 0) { throw new SipParseException(buffer.getReaderIndex(), "No scheme found"); } if (count > 0) { return buffer.readBytes(count); } return null; } /** * Consume the userinfo and hostport. * * The reason why this method does both is because the userinfo portion is * optional and as such you actually don't know whether the stuff you are * currently going over is the hostport or the userinfo. Also, unfortunately * the userinfo is allowed to have characters that normally are reserved * (such as ';'), which complicates things as well. * * *
     * SIP-URI  =  "sip:" [ userinfo ] hostport
     *             uri-parameters [ headers ]
     * SIPS-URI =  "sips:" [ userinfo ] hostport
     *             uri-parameters [ headers ]
     *
     * userinfo         =  ( user / telephone-subscriber ) [ ":" password ] "@"
     * user             =  1*( unreserved / escaped / user-unreserved )
     * user-unreserved  =  "&" / "=" / "+" / "$" / "," / ";" / "?" / "/"
     * password         =  *( unreserved / escaped / "&" / "=" / "+" / "$" / "," )
     * hostport         =  host [ ":" port ]
     * 
* * As you can see, the user-unreserved is what mess things up. it would have * been way easier and more efficient if the user-unreserved wasn't there... * * @param buffer * @return */ public static SipUserHostInfo consumeUserInfoHostPort(final Buffer buffer) throws SipParseException, IOException { return SipUserHostInfo.frame(buffer); } /** * Consume the "sent-protocol", which according to RFC 3261 is: * *
     * sent-protocol = protocol-name SLASH protocol-version SLASH transport
     * transport     =  "UDP" / "TCP" / "TLS" / "SCTP" / other-transport
     * other-transport   =  token
     * 
* * The "sent-protocol" is only present in a Via header and typically looks * like this: SIP/2.0/UDP * * The consume method will make sure that "SIP/2.0/" is present and if not, * complain. The transport can really be anything and as such that is what * you will get back as a return value. Hence, in the above example you * would get back a buffer consisting of "UDP". * * Also note that the transport can be "other-transport" which translates to * a "token" so we allow really anything and as such we will just consume * and return the token after verifying that it start with the SIP/2.0 * stuff. * * @param buffer * @return the transport part of the "sent-protocol". Typically this will be * one of UDP, TCP or TLS. * @throws IOException * @throws SipParseException * in case anything goes wrong while parsing including if the * protocol-name isn't SIP and the version isn't 2.0 */ public static Buffer consumeSentProtocol(final Buffer buffer) throws IOException, SipParseException { expectSIP2_0(buffer); expect(buffer, SipParser.SLASH); final Buffer protocol = consumeToken(buffer); if (protocol == null || protocol.isEmpty()) { throw new SipParseException(buffer.getReaderIndex(), "Expected transport"); } return protocol; } /** * Consume a token, which according to RFC3261 section 25.1 Basic Rules is: * * token = 1*(alphanum / "-" / "." / "!" / "%" / "*" / "_" / "+" / "`" / "'" * / "~" ) * * @param buffer * @return the buffer containing the token we consumed or null if nothing * was consumed. * @throws IOException * @throws IndexOutOfBoundsException */ public static Buffer consumeToken(final Buffer buffer) throws IndexOutOfBoundsException, IOException { final int count = getTokenCount(buffer); if (count == 0) { return null; } return buffer.readBytes(count); } /** * Consumes a alphanum. * * @param buffer * @return * @throws IOException */ public static Buffer consumeAlphaNum(final Buffer buffer) throws IOException { final int count = getAlphaNumCount(buffer); if (count == 0) { return null; } return buffer.readBytes(count); } /** * Consume a Via-header, which according to RFC3261 is: * *
     * Via               =  ( "Via" / "v" ) HCOLON via-parm *(COMMA via-parm)
     * via-parm          =  sent-protocol LWS sent-by *( SEMI via-params )
     * via-params        =  via-ttl / via-maddr / via-received / via-branch / via-extension
     * via-ttl           =  "ttl" EQUAL ttl
     * via-maddr         =  "maddr" EQUAL host
     * via-received      =  "received" EQUAL (IPv4address / IPv6address)
     * via-branch        =  "branch" EQUAL token
     * via-extension     =  generic-param
     * sent-protocol     =  protocol-name SLASH protocol-version SLASH transport
     * 
* * Note, this method assumes that you have already stripped off the "Via" or * "v". I.e., the header name. * * The return value is ALWAYS an array of objects of size 4. However, the * port can be null (index 2 = the third value) so make sure to check it. * Also, the 4th value (index 3) contains a List containing all * the via-parameters. Each entry in the list is a two dimensional array * where the first element is the key and the second is the value. For flag * parameters, the value will be null so make sure to check. For more info * regarding how parameters are parsed, see * {@link #consumeGenericParams(Buffer)}. * *
    *
  • result[0] - the protocol. Will never ever be null
  • *
  • result[1] - the sent-by host. Will never ever be null
  • *
  • result[2] - the sent-by port. May be null if port wasn't specified
  • *
  • result[3] - the via-parameters as a List. Will always have * elements in it since a via without a branch parameter is illegal.
  • *
* * @param buffer * @return returns an array of 4 elements. See above for details * @throws SipParseException * @throws IOException */ public static Object[] consumeVia(final Buffer buffer) throws SipParseException, IOException { final Object[] result = new Object[4]; // start off by just finding the ';'. A Via-header MUST have a branch // parameter and as such // there must be a ';' present. If there isn't one, then bail out and // complain. int count = 0; int indexOfSemi = 0; int countOfColons = 0; int indexOfLastColon = 0; int readerIndexOfLastColon = 0; // for reporting consumeSWS(buffer); result[0] = consumeSentProtocol(buffer); consumeLWS(buffer); final int index = buffer.getReaderIndex(); while (indexOfSemi == 0 && buffer.hasReadableBytes() && ++count < MAX_LOOK_AHEAD) { final byte b = buffer.readByte(); if (b == SipParser.SEMI) { indexOfSemi = count; } else if (b == SipParser.COLON) { ++countOfColons; indexOfLastColon = count; readerIndexOfLastColon = buffer.getReaderIndex(); } } if (count == 0) { return null; } if (count == MAX_LOOK_AHEAD) { throw new SipParseException(buffer.getReaderIndex(), "Unable to find the parameters part of the Via-header " + "even after searching for " + MAX_LOOK_AHEAD + " bytes."); } if (indexOfSemi == 0) { // well, we don't check if the branch parameter is there but // if there are no parameters present at all then there // is no chance of it being present. throw new SipParseException(buffer.getReaderIndex(), "No via-parameters found. The Via-header MUST contain at least the branch parameter."); } buffer.setReaderIndex(index); if (countOfColons == 0 || countOfColons == 7) { // no port, just host result[1] = buffer.readBytes(indexOfSemi - 1); } else if (indexOfLastColon != 0 && (countOfColons == 1 || countOfColons == 8)) { // found either a single colon or 8 colons where 8 colons indicates // that we have an ipv6 address in front of us. result[1] = buffer.readBytes(indexOfLastColon - 1); buffer.readByte(); // consume ':' result[2] = buffer.readBytes(indexOfSemi - indexOfLastColon - 1); // consume // port if (result[2] == null || ((Buffer) result[2]).isEmpty()) { throw new SipParseException(readerIndexOfLastColon + 1, "Expected port after colon"); } } else { // indication an strange number of colons. May be the strange // ipv4 address after ipv6 thing which we currently dont handle throw new SipParseException(indexOfLastColon, "Found " + countOfColons + " which seems odd." + " Expecting 0, 1, 7 or 8 colons in the Via-host:port portion. Please check your traffic"); } final List params = consumeGenericParams(buffer); result[3] = params; return result; } /** * Consume a sent-by which according to 3261 is: * *
     * sent-by           =  host [ COLON port ]
     * host             =  hostname / IPv4address / IPv6reference
     * hostname         =  *( domainlabel "." ) toplabel [ "." ]
     * domainlabel      =  alphanum
     * toplabel         =  ALPHA / ALPHA *( alphanum / "-" ) alphanum
     * IPv4address    =  1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
     * IPv6reference  =  "[" IPv6address "]"
     * IPv6address    =  hexpart [ ":" IPv4address ]
     * hexpart        =  hexseq / hexseq "::" [ hexseq ] / "::" [ hexseq ]
     * hexseq         =  hex4 *( ":" hex4)
     * hex4           =  1*4HEXDIG
     * port           =  1*DIGIT
     * 
* * Now, since we want to do things as fast as possible and all the * consumeXXX methods only do framing we will implement something a little * simpler, faster but not 100% according to the BNF. * * So, consume everything until we either hit a ';' signifying parameters or * a ':' which then forces us to start checking the port. Also, white space * will stop parsing. * * However, a colon could also signify an IPv6 address, which is not handled * right now. * * @param buffer * @return * @throws IOException */ public static Buffer[] consumeSentBye(final Buffer buffer) throws SipParseException, IOException { final int index = buffer.getReaderIndex(); int count = 0; boolean done = false; boolean firstColon = false; boolean consumePort = false; while (!done && buffer.hasReadableBytes()) { final byte b = buffer.readByte(); ++count; if (firstColon && isDigit(b)) { // get port consumePort = true; done = true; count -= 2; continue; } else if (firstColon) { // consume firstColon = false; continue; } if (b == SipParser.COLON) { firstColon = true; } else if (b == SipParser.SEMI) { --count; done = true; } } if (count == 0) { return null; } buffer.setReaderIndex(index); final Buffer host = buffer.readBytes(count); Buffer port = null; if (consumePort) { buffer.readByte(); // consume ':' port = consumePort(buffer); } return new Buffer[]{ host, port}; } /** * Consume a port, which according to RFC 3261 is: * * port = 1*DIGIT * * @param buffer * @return the buffer containing only digits or null if there was none fun. * @throws IOException */ public static Buffer consumePort(final Buffer buffer) throws IOException { int count = 0; final int index = buffer.getReaderIndex(); boolean done = false; while (!done && buffer.hasReadableBytes()) { if (isDigit(buffer.readByte())) { ++count; } else { done = true; } } buffer.setReaderIndex(index); if (count != 0) { return buffer.readBytes(count); } return null; } public static Buffer consumeHostname(final Buffer buffer) throws IOException { return null; } /** * Consume a m-type, which according to RFC3261 section 25.1 Basic Rules is: * *
     * m-type           =  discrete-type / composite-type
     * discrete-type    =  "text" / "image" / "audio" / "video" / "io.sipstack.application.application" / extension-token
     * composite-type   =  "message" / "multipart" / extension-token
     * extension-token  =  ietf-token / x-token
     * ietf-token       =  token
     * x-token          =  "x-" token
     * 
* * And if you really read through all of that stuff it all boils down to * "token" so that is all we end up doing in this method. Hence, this method * is really only to put some context to consuming a media-type * * @return * @throws IndexOutOfBoundsException * @throws IOException */ public static Buffer consumeMType(final Buffer buffer) throws SipParseException { try { return consumeToken(buffer); } catch (final IndexOutOfBoundsException e) { throw new SipParseException(buffer.getReaderIndex(), "Tried to consume m-type but buffer ended abruptly", e); } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), "Tried to consume m-type but problem reading from underlying stream", e); } } /** * Consume a m-subtype, which according to RFC3261 section 25.1 Basic Rules * is: * *
     * m-subtype        =  extension-token / iana-token
     * extension-token  =  ietf-token / x-token
     * ietf-token       =  token
     * x-token          =  "x-" token
     * iana-token       =  token
     * 
* * And if you really read through all of that stuff it all boils down to * "token" so that is all we end up doing in this method. Hence, this method * is really only to put some context to consuming a media-type * * @return * @throws IndexOutOfBoundsException * @throws IOException */ public static Buffer consumeMSubtype(final Buffer buffer) throws SipParseException { try { return consumeToken(buffer); } catch (final IndexOutOfBoundsException e) { throw new SipParseException(buffer.getReaderIndex(), "Tried to consume m-type but buffer ended abruptly", e); } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), "Tried to consume m-type but problem reading from underlying stream", e); } } /** * Helper method that counts the number of bytes that are considered part of * the next token in the {@link Buffer}. * * @param buffer * @return a count of the number of bytes the next token contains or zero if * no token is to be found within the buffer. * @throws IOException * @throws IndexOutOfBoundsException */ public static int getTokenCount(final Buffer buffer) throws IndexOutOfBoundsException, IOException { boolean done = false; int count = 0; buffer.markReaderIndex(); while (buffer.hasReadableBytes() && !done) { final byte b = buffer.readByte(); final boolean ok = isAlphaNum(b) || b == DASH || b == PERIOD || b == EXCLAMATIONPOINT || b == PERCENT || b == STAR || b == UNDERSCORE || b == PLUS || b == BACKTICK || b == TICK || b == TILDE; if (ok) { ++count; } else { done = true; } } buffer.resetReaderIndex(); return count; } /** * Helper method that counts the number of bytes that are considered part of * the next alphanum block. * * @param buffer * @return a count of the number of bytes the next alphaum contains or zero * if none is found. * @throws IndexOutOfBoundsException * @throws IOException */ public static int getAlphaNumCount(final Buffer buffer) throws IndexOutOfBoundsException, IOException { boolean done = false; int count = 0; final int index = buffer.getReaderIndex(); while (buffer.hasReadableBytes() && !done) { final byte b = buffer.readByte(); if (isAlphaNum(b)) { ++count; } else { done = true; } } buffer.setReaderIndex(index); return count; } /** * Check whether next byte is a alpha numeric one. * * @param buffer * @return true if the next byte is a alpha numeric character, otherwise * false * @throws IOException * @throws IndexOutOfBoundsException */ public static boolean isNextAlphaNum(final Buffer buffer) throws IndexOutOfBoundsException, IOException { if (buffer.hasReadableBytes()) { final byte b = buffer.peekByte(); return isAlphaNum(b); } return false; } /** * Checks whether the character could be part of the host portion of a SIP URI. * * This does not perform an entirely robust validation as it does not operate * with the full context of the hostname. * * @param ch * @return true if the character is valid in hostnames, false otherwise */ public static boolean isHostPortCharacter(final char ch) { return isAlphaNum(ch) || ch == DASH || ch == PERIOD || ch == COLON; } public static boolean isHostPortCharacter(final byte b) { return isHostPortCharacter((char) b); } /** * Helper method for checking whether the supplied byte is a alphanumeric * character or not. * * @param b * @return true if the byte is indeed a alphanumeric character, false * otherwise */ public static boolean isAlphaNum(final char ch) { return ch >= 97 && ch <= 122 || ch >= 48 && ch <= 57 || ch >= 65 && ch <= 90; } public static boolean isAlphaNum(final byte b) { return isAlphaNum((char) 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 static boolean isAlpha(final char ch) { return ch >= 97 && ch <= 122 || ch >= 65 && ch <= 90; } public static boolean isAlpha(final byte b) { return isAlpha((char) b); } /** * Consume linear whitespace (LWS), which according to RFC3261 section 25.1 * Basic Rules is: * * LWS = [*WSP CRLF] 1*WSP ; linear whitespace * * @param buffer * @return the number of bytes consumed */ public static int consumeLWS(final Buffer buffer) throws SipParseException { final int i = buffer.getReaderIndex(); consumeWS(buffer); // if we consume a CRLF we expect at least ONE WS to be present next if (consumeCRLF(buffer) > 0) { expectWS(buffer); } consumeWS(buffer); if (buffer.getReaderIndex() == i) { throw new SipParseException(i, "Expected at least 1 WSP"); } return buffer.getReaderIndex() - i; } /** * Consume CR + LF * * @param buffer * @return the number of bytes we consumed, which should be two * if we indeed consumed CRLF or zero otherwise. */ public static int consumeCRLF(final Buffer buffer) throws SipParseException { try { buffer.markReaderIndex(); final byte cr = buffer.readByte(); final byte lf = buffer.readByte(); if (cr == CR && lf == LF) { return 2; } } catch (final IndexOutOfBoundsException e) { // fall through } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), UNABLE_TO_READ_FROM_STREAM, e); } buffer.resetReaderIndex(); return 0; } /** * Check so that the next byte in the passed in buffer is the expected one. * * @param buffer * the buffer that we will check. * @param expected * the buffer that contains the expected byte (note, it is 1 * byte, not bytes) * @throws ParseException * in case the expected byte is not the next byte in the buffer */ public static void expect(final Buffer buffer, final byte expected) throws SipParseException, IOException { final byte actual = buffer.readByte(); if (actual != expected) { final String actualStr = new String(new byte[]{actual}, Charset.forName("UTF-8")); final String expectedStr = new String(new byte[]{expected}); throw new SipParseException(buffer.getReaderIndex(), "Expected '" + expected + "' (" + expectedStr + ") got '" + actual + "' (" + actualStr + ")"); } } /** * Consume all the whitespace we find (WS) * * @param buffer * @return true if we did consume something, false otherwise */ public static int consumeWS(final Buffer buffer) throws SipParseException { try { boolean done = false; int count = 0; while (buffer.hasReadableBytes() && !done) { if (isNext(buffer, SP) || isNext(buffer, HTAB)) { buffer.readByte(); ++count; } else { done = true; } } return count; } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), UNABLE_TO_READ_FROM_STREAM, e); } } /** * Check so that the next byte in the passed in buffer is the expected one. * * @param buffer * the buffer that we will check. * @param expected * the expected char * @throws SipParseException * in case the expected char is not the next char in the buffer * or if there is an error reading from the underlying stream */ public static void expect(final Buffer buffer, final char ch) throws SipParseException { try { final int i = buffer.readUnsignedByte(); if (i != ch) { throw new SipParseException(buffer.getReaderIndex(), "Expected '" + ch + "' got '" + i + "'"); } } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), UNABLE_TO_READ_FROM_STREAM, e); } } /** * Convenience method for slicing out the next header name but NOT checking * if HCOLON shows up afterwards. This is useful when dealing with a stream * of bytes coming in over the network, as in TCP, so you may actually find * SP or HTAB but the COLON, and as such, the HCOLON, hasn't shown up yet * so the method {@link #nextHeaderName(Buffer)} would blow up on no * HCOLON, which is not what we want. Hence, use this method and then * check HCOLON yourself. * * @param buffer * @return */ public static Buffer nextHeaderNameDontCheckHColon(final Buffer buffer) { try { final int startIndex = buffer.getReaderIndex(); int nameIndex = 0; while (buffer.hasReadableBytes() && nameIndex == 0) { if (isNext(buffer, SP) || isNext(buffer, HTAB) || isNext(buffer, COLON)) { nameIndex = buffer.getReaderIndex(); } else { buffer.readByte(); } } // Bad header! No HCOLON found! (or beginning thereof anyway) if (nameIndex == 0) { // probably ran out of bytes to read so lets just return null buffer.setReaderIndex(startIndex); return null; } return buffer.slice(startIndex, nameIndex); } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), UNABLE_TO_READ_FROM_STREAM, e); } } public static Buffer nextHeaderName(final Buffer buffer) throws SipParseException { final Buffer name = nextHeaderNameDontCheckHColon(buffer); if (name != null) { expectHCOLON(buffer); } return name; } public static List nextHeaders(final Buffer buffer) throws SipParseException { try { final int startIndex = buffer.getReaderIndex(); int nameIndex = 0; while (buffer.hasReadableBytes() && nameIndex == 0) { if (isNext(buffer, SP) || isNext(buffer, HTAB) || isNext(buffer, COLON)) { nameIndex = buffer.getReaderIndex(); } else { buffer.readByte(); } } // Bad header! No HCOLON found! (or beginning thereof anyway) if (nameIndex == 0) { // probably ran out of bytes to read so lets just return null return null; } final Buffer name = buffer.slice(startIndex, nameIndex); expectHCOLON(buffer); final List values = readHeaderValues(name, buffer).values; final List headers = new ArrayList(values.size()); for (final Buffer value : values) { headers.add(new SipHeaderImpl(name, value)); } return headers; } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), UNABLE_TO_READ_FROM_STREAM, e); } } /** * Not all headers allow for multiple values on a single line. This is a * basic check for validating whether or not that the header allows it or * not. Note, for headers such as Contact, it depends! * * @param headerName * @return */ private static boolean isHeaderAllowingMultipleValues(final Buffer headerName) { final int size = headerName.getReadableBytes(); if (size == 7) { return !isSubjectHeader(headerName); } else if (size == 5) { return !isAllowHeader(headerName); } else if (size == 4) { return !isDateHeader(headerName); } else if (size == 1) { return !isAllowEventsHeaderShort(headerName); } else if (size == 12) { return !isAllowEventsHeader(headerName); } return true; } /** * The date header also allows for comma within the value of the header. * * @param name * @return */ private static boolean isDateHeader(final Buffer name) { try { return name.getByte(0) == 'D' && name.getByte(1) == 'a' && name.getByte(2) == 't' && name.getByte(3) == 'e'; } catch (final IOException e) { return false; } } private static boolean isAllowHeader(final Buffer name) { try { return name.getByte(0) == 'A' && name.getByte(1) == 'l' && name.getByte(2) == 'l' && name.getByte(3) == 'o' && name.getByte(4) == 'w'; } catch (final IOException e) { return false; } } private static boolean isAllowEventsHeaderShort(final Buffer name) { try { return name.getByte(0) == 'u'; } catch (final IOException e) { return false; } } private static boolean isAllowEventsHeader(final Buffer name) { try { return name.getByte(0) == 'A' && name.getByte(1) == 'l' && name.getByte(2) == 'l' && name.getByte(3) == 'o' && name.getByte(4) == 'w' && name.getByte(5) == '-' && name.getByte(6) == 'E' && name.getByte(7) == 'v' && name.getByte(8) == 'e' && name.getByte(9) == 'n' && name.getByte(10) == 't' && name.getByte(11) == 's'; } catch (final IOException e) { return false; } } private static boolean isSubjectHeader(final Buffer name) { try { return name.getByte(0) == 'S' && name.getByte(1) == 'u' && name.getByte(2) == 'b' && name.getByte(3) == 'j' && name.getByte(4) == 'e' && name.getByte(5) == 'c' && name.getByte(6) == 't'; } catch (final IOException e) { return false; } } /** * Because parsing streaming data where not everything has shown up yet we must remember * the state of where we are in the "parse out header values". */ public static final class HeaderValueState { public List values = new ArrayList<>(2); public int start; public int stop = -1; public boolean foundCR = false; public boolean foundLF = false; public boolean foundCRLF = false; public boolean foundComma = false; public boolean insideQuotedString = false; public boolean done = false; public boolean foldedLine = false; public HeaderValueState(final int readerIndex) { start = readerIndex; } public void reset(final int readerIndex) { start = readerIndex; values = new ArrayList<>(2); stop = -1; foundCR = false; foundLF = false; foundCRLF = false; foundComma = false; insideQuotedString = false; done = false; foldedLine = false; } } public static HeaderValueState readHeaderValues(final HeaderValueState state, final Buffer headerName, final Buffer buffer) throws IOException { while (buffer.hasReadableBytes() && !state.done) { final byte b = buffer.readByte(); switch (b) { case DOUBLE_QOUTE: state.insideQuotedString = !state.insideQuotedString; break; case LF: state.foundLF = true; state.foundCRLF = state.foundCR; final int separatorLen = state.foundCRLF ? 2 : 1; state.stop = buffer.getReaderIndex() - separatorLen; break; case CR: state.foundCR = true; // state.stop = buffer.getReaderIndex() - 1; break; case COMMA: // if we find a comma then we may have found the end of this // header value depending whether or not the header we are // framing actually allows multiple values on a single line if (!state.insideQuotedString && isHeaderAllowingMultipleValues(headerName)) { state.stop = buffer.getReaderIndex() - 1; buffer.setReaderIndex(state.stop); consumeCOMMA(buffer); state.foundComma = true; state.foundCRLF = true; } break; default: break; } if (state.foundCRLF || state.foundLF) { state.values.add(buffer.slice(state.start, state.stop)); // if (isNext(buffer, LF)) { // buffer.readByte(); // } // Until we have implemented the CompositeBuffer I'm just going to cheat like this // and yes, it is stupid but folded lines aren't that common so... if (state.foldedLine) { final Buffer line2 = state.values.remove(state.values.size() - 1); final Buffer line1 = state.values.remove(state.values.size() - 1); final Buffer folded = Buffers.wrap(line1 + " " + line2); state.values.add(folded); state.foldedLine = false; } if (isNext(buffer, SP) || isNext(buffer, HTAB)) { consumeWS(buffer); state.foldedLine = !state.foundComma; } else if (!state.foundComma) { state.done = true; } state.foundCR = false; state.foundLF = false; state.foundCRLF = false; state.foundComma = false; state.start = buffer.getReaderIndex(); } } return state; } public static HeaderValueState readHeaderValues(final Buffer headerName, final Buffer buffer) throws IOException { final HeaderValueState state = new HeaderValueState(buffer.getReaderIndex()); return readHeaderValues(state, headerName, buffer); } /** * Get the next header, which may actually be returning multiple if there * are multiple headers on the same line. * * @param buffer * @return an array where the first element is the name of the buffer and * the second element is the value of the buffer * @throws SipParseException */ public static SipHeader nextHeader(final Buffer buffer) throws SipParseException { try { final int startIndex = buffer.getReaderIndex(); int nameIndex = 0; while (buffer.hasReadableBytes() && nameIndex == 0) { if (isNext(buffer, SP) || isNext(buffer, HTAB) || isNext(buffer, COLON)) { nameIndex = buffer.getReaderIndex(); } else { buffer.readByte(); } } // Bad header! No HCOLON found! (or beginning thereof anyway) if (nameIndex == 0) { // probably ran out of bytes to read so lets just return null return null; } final Buffer name = buffer.slice(startIndex, nameIndex); expectHCOLON(buffer); // Note, we are framing headers from a sip request or response which // is why we safely can assume that there will ALWAYS be a CRLF // after each line Buffer valueBuffer = buffer.readLine(); // Folded lines are rare so try and avoid the bulk of // the work if possible. if (isNext(buffer, SP) || isNext(buffer, HTAB)) { List foldedLines = null; boolean done = false; while (!done) { if (isNext(buffer, SP) || isNext(buffer, HTAB)) { consumeWS(buffer); if (foldedLines == null) { foldedLines = new ArrayList(2); } foldedLines.add(buffer.readLine()); } else { done = true; } } if (foldedLines != null) { // even though stupid, folded lines are not that common // so optimize if this ever becomes a problem. String stupid = valueBuffer.toString(); for (final Buffer line : foldedLines) { stupid += " " + line.toString(); } valueBuffer = Buffers.wrap(stupid.getBytes(Charset.forName("UTF-8"))); consumeWS(valueBuffer); } } return new SipHeaderImpl(name, valueBuffer); } catch (final IOException e) { throw new SipParseException(buffer.getReaderIndex(), UNABLE_TO_READ_FROM_STREAM, e); } } /** * Frame the supplied buffer into a {@link SipMessage}. No deep analysis of the message will be * performed by this framer so there is no guarantee that this {@link SipMessage} is actually a * well formed message. * * @param buffer * @return the framed {@link SipMessage} */ public static SipMessage frame(final Buffer buffer) throws IOException { if (true) return frame2(buffer); if (!couldBeSipMessage(buffer)) { throw new SipParseException(0, "Cannot be a SIP message because is doesnt start with \"SIP\" " + "(for responses) or a method (for requests)"); } // we just assume that the initial line // indeed is a correct sip line final Buffer rawInitialLine = buffer.readLine(); // which means that the headers are about // to start now. final int startHeaders = buffer.getReaderIndex(); Buffer currentLine = null; while ((currentLine = buffer.readLine()) != null && currentLine.hasReadableBytes()) { // just moving along, we don't really care why // we stop, we have found what we want anyway, which // is the boundary between headers and the potential // payload (or end of message) } final Buffer headers = buffer.slice(startHeaders, buffer.getReaderIndex()); Buffer payload = null; if (buffer.hasReadableBytes()) { payload = buffer.slice(); } if (SipInitialLine.isResponseLine(rawInitialLine)) { throw new RuntimeException("No longer using the old mutable sip messages"); // return new SipResponseImpl(rawInitialLine, headers, payload); } else { throw new RuntimeException("No longer using the old mutable sip messages"); // return new SipRequestImpl(rawInitialLine, headers, payload); } } /** * * @param buffer * @return * @throws IOException */ public static SipMessage frame2(final Buffer buffer) throws IOException { if (!couldBeSipMessage(buffer)) { throw new SipParseException(0, "Cannot be a SIP message because is doesnt start with \"SIP\" " + "(for responses) or a method (for requests)"); } final int startIndex = buffer.getReaderIndex(); final SipInitialLine initialLine = SipInitialLine.parse(buffer.readLine()); // which means that the headers are about // to start now. final int startHeaders = buffer.getReaderIndex(); // Move along as long as we actually can consume an header and Buffer headerName = null; final List headers = new ArrayList<>(); short count = 0; int contentLength = 0; short indexOfTo = -1; short indexOfFrom = -1; short indexOfCSeq = -1; short indexOfCallId = -1; short indexOfMaxForwards = -1; short indexOfVia = -1; short indexOfRoute = -1; short indexOfRecordRoute = -1; short indexOfContact = -1; while (consumeCRLF(buffer) != 2 && (headerName = SipParser.nextHeaderName(buffer)) != null) { final List values = readHeaderValues(headerName, buffer).values; for (final Buffer value : values) { SipHeader header = new SipHeaderImpl(headerName, value); // The headers that are most commonly used will be fully // parsed just because no stack can really function without // looking into these headers. if (header.isContentLengthHeader()) { final ContentLengthHeader l = header.ensure().toContentLengthHeader(); contentLength = l.getContentLength(); header = l; } else if (header.isContactHeader() && indexOfContact == -1) { header = header.ensure(); indexOfContact = count; } else if (header.isCSeqHeader() && indexOfCSeq == -1) { header = header.ensure(); indexOfCSeq = count; } else if (header.isMaxForwardsHeader() && indexOfMaxForwards == -1) { header = header.ensure(); indexOfMaxForwards = count; } else if (header.isFromHeader() && indexOfFrom == -1) { header = header.ensure(); indexOfFrom = count; } else if (header.isToHeader() && indexOfTo == -1) { header = header.ensure(); indexOfTo = count; } else if (header.isViaHeader() && indexOfVia == -1) { header = header.ensure(); indexOfVia = count; } else if (header.isCallIdHeader() && indexOfCallId == -1) { header = header.ensure(); indexOfCallId = count; } else if (header.isRouteHeader() && indexOfRoute == -1) { header = header.ensure(); indexOfRoute = count; } else if (header.isRecordRouteHeader() && indexOfRecordRoute == -1) { header = header.ensure(); indexOfRecordRoute = count; } headers.add(header); ++count; } } // final Buffer headers = buffer.slice(startHeaders, buffer.getReaderIndex()); Buffer payload = null; if (contentLength > 0 && buffer.hasReadableBytes()) { payload = buffer.readBytes(Math.min(contentLength, buffer.getReadableBytes())); } else { payload = Buffers.EMPTY_BUFFER; } // slice out the entire message from this buffer since // everything is immutable when this message potentially // is written to socket again, there is really nothing to do // other than just write these bytes out. final Buffer msg = buffer.slice(startIndex, buffer.getReaderIndex()); if (initialLine.isRequestLine()) { return new ImmutableSipRequest(msg, initialLine.toRequestLine(), headers, indexOfTo, indexOfFrom, indexOfCSeq, indexOfCallId, indexOfMaxForwards, indexOfVia, indexOfRoute, indexOfRecordRoute, indexOfContact, payload); } else { return new ImmutableSipResponse(msg, initialLine.toResponseLine(), headers, indexOfTo, indexOfFrom, indexOfCSeq, indexOfCallId, indexOfMaxForwards, indexOfVia, indexOfRoute, indexOfRecordRoute, indexOfContact, payload); } } /** * Helper function that checks whether or not the data could be a SIP message. It is a very * basic check but if it doesn't go through it definitely is not a SIP message. * * @param data * @return */ public static boolean couldBeSipMessage(final Buffer data) throws IOException { final byte a = data.getByte(0); final byte b = data.getByte(1); final byte c = data.getByte(2); return couldBeSipMessage(a, b, c); } public static boolean couldBeSipMessage(final byte a, final byte b, final byte c) throws IOException { return a == 'S' && b == 'I' && c == 'P' || // response a == 'I' && b == 'N' && c == 'V' || // INVITE a == 'A' && b == 'C' && c == 'K' || // ACK a == 'B' && b == 'Y' && c == 'E' || // BYE a == 'O' && b == 'P' && c == 'T' || // OPTIONS a == 'C' && b == 'A' && c == 'N' || // CANCEL a == 'M' && b == 'E' && c == 'S' || // MESSAGE a == 'R' && b == 'E' && c == 'G' || // REGISTER a == 'I' && b == 'N' && c == 'F' || // INFO a == 'P' && b == 'R' && c == 'A' || // PRACK a == 'S' && b == 'U' && c == 'B' || // SUBSCRIBE a == 'N' && b == 'O' && c == 'T' || // NOTIFY a == 'U' && b == 'P' && c == 'D' || // UPDATE a == 'R' && b == 'E' && c == 'F' || // REFER a == 'P' && b == 'U' && c == 'B'; // PUBLISH } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy