From ee046f56a58a3009f8fd588a51ea169ac5d669a3 Mon Sep 17 00:00:00 2001 From: Teerapap Changwichukarn Date: Thu, 17 Aug 2023 10:56:04 +0800 Subject: [PATCH] Fix IPv6 handling in Via header for IPv6 with zero compression Changes in detail * Implement `consumeHost()` method in `SipParser` loosely based on RFC 3261 * Fix typo in method name `consumeSentBye()` -> `consumeSentBy()` * Refactor `consumeVia()` method to make use of `consumeSentBy()` * Refactor `consumeSentBy()` method to make use of `consumeHost()` * Improve `consumeGenericParam()` method to loosely handle host in addition to token * Add unit tests for IPv6 address with zero compression for Via header --- .../io/pkts/packet/sip/impl/SipParser.java | 224 ++++++++++-------- .../pkts/packet/sip/impl/SipParserTest.java | 26 +- 2 files changed, 139 insertions(+), 111 deletions(-) diff --git a/pkts-sip/src/main/java/io/pkts/packet/sip/impl/SipParser.java b/pkts-sip/src/main/java/io/pkts/packet/sip/impl/SipParser.java index ad5b2903..8c9e1231 100644 --- a/pkts-sip/src/main/java/io/pkts/packet/sip/impl/SipParser.java +++ b/pkts-sip/src/main/java/io/pkts/packet/sip/impl/SipParser.java @@ -459,8 +459,7 @@ public static boolean isSips(final Buffer buffer) throws SipParseException, Inde 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 + while (consumeSEMI(buffer) > 0) { params.add(consumeGenericParam(buffer)); } return params; @@ -503,11 +502,14 @@ public static Buffer[] consumeGenericParam(final Buffer buffer) throws IndexOutO 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); + // token or host whichever is longer + final int count = Math.max(getTokenCount(buffer), getHostCount(buffer)); + if (count > 0) { + value = buffer.readBytes(count); + } } } return new Buffer[]{ @@ -1327,15 +1329,6 @@ public static Buffer consumeAlphaNum(final Buffer buffer) 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); @@ -1343,59 +1336,14 @@ public static Object[] consumeVia(final Buffer buffer) throws SipParseException, throw new SipParseException(buffer.getReaderIndex(), "Expected at least 1 WSP"); } - 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"); - } + Buffer[] sentBy = consumeSentBy(buffer); + result[1] = sentBy[0]; + result[2] = sentBy[1]; final List params = consumeGenericParams(buffer); + if (params.size() == 0) { + throw new SipParseException(buffer.getReaderIndex(), "Expected at least 1 parameter because Via without a branch parameter is illegal"); + } result[3] = params; return result; @@ -1427,55 +1375,25 @@ public static Object[] consumeVia(final Buffer buffer) throws SipParseException, * 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. + * '[' and ']' will signify IPv6 address for the host part. * * @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; - } + public static Buffer[] consumeSentBy(final Buffer buffer) throws SipParseException, IOException { + Buffer host = consumeHost(buffer); - 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) { + if (isNext(buffer, SipParser.COLON)) { buffer.readByte(); // consume ':' port = consumePort(buffer); + if (port == null) { + throw new SipParseException(buffer.getReaderIndex() + 1, "Expected port after colon"); + } } return new Buffer[]{ - host, port}; + host, port}; } /** @@ -1507,8 +1425,81 @@ public static Buffer consumePort(final Buffer buffer) throws IOException { return null; } - public static Buffer consumeHostname(final Buffer buffer) throws IOException { - return null; + /** + * Consume a host, which according to RFC3261 is: + * + *
+     * 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
+     * 
+ * + * 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 ':'. + * + * '[' and ']' will signify IPv6 address for the host part. + * + * @param buffer + * @return + * @throws IOException + */ + public static Buffer consumeHost(final Buffer buffer) throws IOException { + final int index = buffer.getReaderIndex(); + int count = 0; + boolean endBracket = false; + boolean isIPv6 = false; + + // TODO: Reuse SipUserHostInfo.Parser to properly parse host (domain/ipv4/ipv6) + while (buffer.hasReadableBytes() && ++count < MAX_LOOK_AHEAD) { + final byte b = buffer.readByte(); + + if (count == 1 && b == SipParser.LSBRACKET) { + isIPv6 = true; + --count; + } else if (isIPv6 && b == SipParser.RSBRACKET) { + endBracket = true; + --count; + break; + } else if ((!isIPv6 && b == SipParser.COLON) || b == SipParser.SEMI || b == SipParser.SP) { + --count; + break; + } + } + + if (count == 0) { + return null; + } + + if (count == MAX_LOOK_AHEAD) { + throw new SipParseException(buffer.getReaderIndex(), + "Have not been able to find the entire host after " + count + " bytes, giving up"); + } + + if (isIPv6 && !endBracket) { + throw new SipParseException(index + 1 + count, "IPv6 address reference does not end with ']'"); + } + + final Buffer host; + if (isIPv6) { + buffer.setReaderIndex(index + 1); // consume '[' + host = buffer.readBytes(count); + buffer.readByte(); // consume ']' + } else { + buffer.setReaderIndex(index); + host = buffer.readBytes(count); + } + return host; } /** @@ -1628,6 +1619,31 @@ public static int getAlphaNumCount(final Buffer buffer) throws IndexOutOfBoundsE return count; } + /** + * Helper method that counts the number of bytes that are considered part of + * the next host in the {@link Buffer}. + * + * @param buffer + * @return a count of the number of bytes the next host contains or zero if + * no host is to be found within the buffer. + * @throws IOException + * @throws IndexOutOfBoundsException + */ + public static int getHostCount(final Buffer buffer) throws IndexOutOfBoundsException, IOException { + int count = 0; + buffer.markReaderIndex(); + while (buffer.hasReadableBytes()) { + final byte b = buffer.readByte(); + final boolean ok = isAlphaNum(b) || b == DASH || b == PERIOD || b == LSBRACKET || b == RSBRACKET || b == COLON; + if (!ok) { + break; + } + ++count; + } + buffer.resetReaderIndex(); + return count; + } + /** * Check whether next byte is a alpha numeric one. * diff --git a/pkts-sip/src/test/java/io/pkts/packet/sip/impl/SipParserTest.java b/pkts-sip/src/test/java/io/pkts/packet/sip/impl/SipParserTest.java index 017ef78b..079b15ec 100644 --- a/pkts-sip/src/test/java/io/pkts/packet/sip/impl/SipParserTest.java +++ b/pkts-sip/src/test/java/io/pkts/packet/sip/impl/SipParserTest.java @@ -945,11 +945,22 @@ public void testConsumeVia() throws Exception { assertVia("SIP/2.0/TLS test.aboutsip.com;ttl=45;branch=asdf;apa=monkey", "TLS", "test.aboutsip.com", null, null, "ttl", "45", "branch", "asdf", "apa", "monkey"); - final String ipv6 = "2001:0db8:85a3:0042:1000:8a2e:0370:7334"; - assertVia("SIP/2.0/UDP " + ipv6 + ";branch=asdf", "UDP", ipv6, null, null, "branch", "asdf"); - assertVia("SIP/2.0/UDP " + ipv6 + ":9090;branch=asdf", "UDP", ipv6, "9090", null, "branch", "asdf"); - assertVia("SIP/2.0/TLS " + ipv6 + ":9090;rport;branch=asdf", "TLS", ipv6, "9090", null, "rport", null, - "branch", "asdf"); + final String[] ipv6Array = new String[]{ + "2001:0db8:85a3:0042:1000:8a2e:0370:7334" /* full form */, + "ef82::1a12:1234:1b12", /* Rule 1 - zero compression rule */ + "1234:fd2:5621:1:89:0:0:4500", /* Rule 2 - leading zero compression rule */ + "2001:1234::1b12:0:0:1a13", /* Rule 3 - zero is compressed at only one junction */ + }; + for (String ipv6 : ipv6Array) { + assertVia("SIP/2.0/UDP [" + ipv6 + "];branch=asdf", "UDP", ipv6, null, null, "branch", "asdf"); + assertVia("SIP/2.0/UDP [" + ipv6 + "]:9090;branch=asdf", "UDP", ipv6, "9090", null, "branch", "asdf"); + assertVia("SIP/2.0/TLS [" + ipv6 + "]:9090;rport;branch=asdf", "TLS", ipv6, "9090", null, "rport", null, + "branch", "asdf"); + assertVia("SIP/2.0/TLS [" + ipv6 + "]:9090;received=" + ipv6 + ";rport=9090;branch=asdf", "TLS", ipv6, "9090", null, + "received", ipv6, + "rport", "9090", + "branch", "asdf"); + } } /** @@ -963,7 +974,8 @@ public void testConsumeBadVia() throws Exception { assertBadVia("SIP/1.0UDP 127.0.0.1:5088;branch=asdf", 5, "wrong protocol version"); assertBadVia("SIP/2.0UDP 127.0.0.1:5088;branch=asdf", 8, "expected to freak out on a missing slash"); assertBadVia("SIP/2.0/UDP sip.com", 19, "no branch parameter. Should not have accepted this"); - assertBadVia("SIP/2.0/UDP :::", 15, "Strange number of colons. Cant parse a valid host out of it."); + assertBadVia("SIP/2.0/UDP 2001:0db8:85a3:0042:1000:8a2e:0370:7334;branch=asdf", 18, "IPv6 address without reference bracket"); + assertBadVia("SIP/2.0/UDP [2001:0db8:85a3:0042:1000:8a2e:0370:7334;branch=asdf", 52, "No end bracket for IPv6"); assertBadVia("SIP/2.0/UDP 127.0.0.1:;branch=asdf", 23, "No port specified after the colon"); } @@ -1072,7 +1084,7 @@ private void assertConsumeSentBy(final String toParse, final String expectedHost final String leftOver) throws Exception { final Buffer buffer = Buffers.wrap(toParse); - final Buffer[] result = SipParser.consumeSentBye(buffer); + final Buffer[] result = SipParser.consumeSentBy(buffer); if (expectedHost == null) { assertThat(result[0], is((Buffer) null)); } else {