rfc compliant sip option parsing + new unit test
authorDavid Vossel <dvossel@digium.com>
Mon, 28 Jun 2010 18:38:47 +0000 (18:38 +0000)
committerDavid Vossel <dvossel@digium.com>
Mon, 28 Jun 2010 18:38:47 +0000 (18:38 +0000)
RFC 3261 section 8.2.2.3 states that if any unsupported options
are found in the Require header field, a "420 (Bad Extension)"
response should be sent with an Unsupported header field containing
only the unsupported options.

This is not currently being done correctly.  Right now, if Asterisk
detects any unsupported sip options in a Require header the entire
list of options are returned in the Unsupported header even if some
of those options are in fact supported.  This patch fixes that by
building an unsupported options character buffer when parsing the
options that can be sent with the 420 response.  A unit test verifying
this functionality has been created.  Some code refactoring was required.

Review: https://reviewboard.asterisk.org/r/680/

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@272880 65c4cc65-6c06-0410-ace0-fbb531ad65f3

channels/chan_sip.c
channels/sip/include/reqresp_parser.h
channels/sip/include/sip.h
channels/sip/reqresp_parser.c

index 1a2c78e..18d40cc 100644 (file)
@@ -635,59 +635,6 @@ static const struct  cfsip_methods {
        { SIP_PING,      NO_RTP, "PING",     CAN_CREATE_DIALOG_UNSUPPORTED_METHOD }
 };
 
-/*! \brief List of well-known SIP options. If we get this in a require,
-   we should check the list and answer accordingly. */
-static const struct cfsip_options {
-       int id;             /*!< Bitmap ID */
-       int supported;      /*!< Supported by Asterisk ? */
-       char * const text;  /*!< Text id, as in standard */
-} sip_options[] = {    /* XXX used in 3 places */
-       /* RFC3262: PRACK 100% reliability */
-       { SIP_OPT_100REL,       NOT_SUPPORTED,  "100rel" },
-       /* RFC3959: SIP Early session support */
-       { SIP_OPT_EARLY_SESSION, NOT_SUPPORTED, "early-session" },
-       /* SIMPLE events:  RFC4662 */
-       { SIP_OPT_EVENTLIST,    NOT_SUPPORTED,  "eventlist" },
-       /* RFC 4916- Connected line ID updates */
-       { SIP_OPT_FROMCHANGE,   NOT_SUPPORTED,  "from-change" },
-       /* GRUU: Globally Routable User Agent URI's */
-       { SIP_OPT_GRUU,         NOT_SUPPORTED,  "gruu" },
-       /* RFC4244 History info */
-       { SIP_OPT_HISTINFO,     NOT_SUPPORTED,  "histinfo" },
-       /* RFC3911: SIP Join header support */
-       { SIP_OPT_JOIN,         NOT_SUPPORTED,  "join" },
-       /* Disable the REFER subscription, RFC 4488 */
-       { SIP_OPT_NOREFERSUB,   NOT_SUPPORTED,  "norefersub" },
-       /* SIP outbound - the final NAT battle - draft-sip-outbound */
-       { SIP_OPT_OUTBOUND,     NOT_SUPPORTED,  "outbound" },
-       /* RFC3327: Path support */
-       { SIP_OPT_PATH,         NOT_SUPPORTED,  "path" },
-       /* RFC3840: Callee preferences */
-       { SIP_OPT_PREF,         NOT_SUPPORTED,  "pref" },
-       /* RFC3312: Precondition support */
-       { SIP_OPT_PRECONDITION, NOT_SUPPORTED,  "precondition" },
-       /* RFC3323: Privacy with proxies*/
-       { SIP_OPT_PRIVACY,      NOT_SUPPORTED,  "privacy" },
-       /* RFC-ietf-sip-uri-list-conferencing-02.txt conference invite lists */
-       { SIP_OPT_RECLISTINV,   NOT_SUPPORTED,  "recipient-list-invite" },
-       /* RFC-ietf-sip-uri-list-subscribe-02.txt - subscription lists */
-       { SIP_OPT_RECLISTSUB,   NOT_SUPPORTED,  "recipient-list-subscribe" },
-       /* RFC3891: Replaces: header for transfer */
-       { SIP_OPT_REPLACES,     SUPPORTED,      "replaces" },
-       /* One version of Polycom firmware has the wrong label */
-       { SIP_OPT_REPLACES,     SUPPORTED,      "replace" },
-       /* RFC4412 Resource priorities */
-       { SIP_OPT_RESPRIORITY,  NOT_SUPPORTED,  "resource-priority" },
-       /* RFC3329: Security agreement mechanism */
-       { SIP_OPT_SEC_AGREE,    NOT_SUPPORTED,  "sec_agree" },
-       /* RFC4092: Usage of the SDP ANAT Semantics in the SIP */
-       { SIP_OPT_SDP_ANAT,     NOT_SUPPORTED,  "sdp-anat" },
-       /* RFC4028: SIP Session-Timers */
-       { SIP_OPT_TIMER,        SUPPORTED,      "timer" },
-       /* RFC4538: Target-dialog */
-       { SIP_OPT_TARGET_DIALOG,NOT_SUPPORTED,  "tdialog" },
-};
-
 /*! \brief Diversion header reasons
  *
  * The core defines a bunch of constants used to define
@@ -1480,7 +1427,6 @@ static int determine_firstline_parts(struct sip_request *req);
 static const struct cfsubscription_types *find_subscription_type(enum subscriptiontype subtype);
 static const char *gettag(const struct sip_request *req, const char *header, char *tagbuf, int tagbufsize);
 static int find_sip_method(const char *msg);
-static unsigned int parse_sip_options(struct sip_pvt *pvt, const char *supported);
 static unsigned int parse_allowed_methods(struct sip_request *req);
 static unsigned int set_pvt_allowed_methods(struct sip_pvt *pvt, struct sip_request *req);
 static int parse_request(struct sip_request *req);
@@ -2941,58 +2887,6 @@ static int find_sip_method(const char *msg)
        return res;
 }
 
-/*! \brief Parse supported header in incoming packet */
-static unsigned int parse_sip_options(struct sip_pvt *pvt, const char *supported)
-{
-       char *next, *sep;
-       char *temp;
-       unsigned int profile = 0;
-       int i, found;
-
-       if (ast_strlen_zero(supported) )
-               return 0;
-       temp = ast_strdupa(supported);
-
-       if (sipdebug)
-               ast_debug(3, "Begin: parsing SIP \"Supported: %s\"\n", supported);
-
-       for (next = temp; next; next = sep) {
-               found = FALSE;
-               if ( (sep = strchr(next, ',')) != NULL)
-                       *sep++ = '\0';
-               next = ast_skip_blanks(next);
-               if (sipdebug)
-                       ast_debug(3, "Found SIP option: -%s-\n", next);
-               for (i = 0; i < ARRAY_LEN(sip_options); i++) {
-                       if (!strcasecmp(next, sip_options[i].text)) {
-                               profile |= sip_options[i].id;
-                               found = TRUE;
-                               if (sipdebug)
-                                       ast_debug(3, "Matched SIP option: %s\n", next);
-                               break;
-                       }
-               }
-
-               /* This function is used to parse both Suported: and Require: headers.
-               Let the caller of this function know that an unknown option tag was
-               encountered, so that if the UAC requires it then the request can be
-               rejected with a 420 response. */
-               if (!found)
-                       profile |= SIP_OPT_UNKNOWN;
-
-               if (!found && sipdebug) {
-                       if (!strncasecmp(next, "x-", 2))
-                               ast_debug(3, "Found private SIP option, not supported: %s\n", next);
-                       else
-                               ast_debug(3, "Found no match for SIP option: %s (Please file bug report!)\n", next);
-               }
-       }
-
-       if (pvt)
-               pvt->sipoptions = profile;
-       return profile;
-}
-
 /*! \brief See if we pass debug IP filter */
 static inline int sip_debug_test_addr(const struct sockaddr_in *addr)
 {
@@ -20497,18 +20391,22 @@ static int handle_request_invite(struct sip_pvt *p, struct sip_request *req, int
        /* Find out what they support */
        if (!p->sipoptions) {
                const char *supported = get_header(req, "Supported");
-               if (!ast_strlen_zero(supported))
-                       parse_sip_options(p, supported);
+               if (!ast_strlen_zero(supported)) {
+                       p->sipoptions = parse_sip_options(supported, NULL, 0);
+               }
        }
 
        /* Find out what they require */
        required = get_header(req, "Require");
        if (!ast_strlen_zero(required)) {
-               required_profile = parse_sip_options(NULL, required);
-               if (required_profile && !(required_profile & (SIP_OPT_REPLACES | SIP_OPT_TIMER))) {
-                       /* At this point we only support REPLACES and Session-Timer */
-                       transmit_response_with_unsupported(p, "420 Bad extension (unsupported)", req, required);
-                       ast_log(LOG_WARNING, "Received SIP INVITE with unsupported required extension: %s\n", required);
+               char unsupported[256] = { 0, };
+               required_profile = parse_sip_options(required, unsupported, ARRAY_LEN(unsupported));
+
+               /* If there are any options required that we do not support,
+                * then send a 420 with only those unsupported options listed */
+               if (!ast_strlen_zero(unsupported)) {
+                       transmit_response_with_unsupported(p, "420 Bad extension (unsupported)", req, unsupported);
+                       ast_log(LOG_WARNING, "Received SIP INVITE with unsupported required extension: required:%s unsupported:%s\n", required, unsupported);
                        p->invitestate = INV_COMPLETED;
                        if (!p->lastinvite)
                                sip_scheddestroy(p, DEFAULT_TRANS_TIMEOUT);
index 60fc646..58784a6 100644 (file)
@@ -122,4 +122,18 @@ void sip_request_parser_register_tests(void);
  */
 void sip_request_parser_unregister_tests(void);
 
+/*!
+ * \brief Parse supported header in incoming packet
+ *
+ * \details This function parses through the options parameters and
+ * builds a bit field representing all the SIP options in that field. When an
+ * item is found that is not supported, it is copied to the unsupported
+ * out buffer.
+ *
+ * \param option list
+ * \param unsupported out buffer (optional)
+ * \param unsupported out buffer length (optional)
+ */
+unsigned int parse_sip_options(const char *options, char *unsupported, size_t unsupported_len);
+
 #endif
index ca8a897..01cbb2c 100644 (file)
@@ -1698,5 +1698,57 @@ struct contact {
 
 AST_LIST_HEAD_NOLOCK(contactliststruct, contact);
 
+/*! \brief List of well-known SIP options. If we get this in a require,
+   we should check the list and answer accordingly. */
+static const struct cfsip_options {
+       int id;             /*!< Bitmap ID */
+       int supported;      /*!< Supported by Asterisk ? */
+       char * const text;  /*!< Text id, as in standard */
+} sip_options[] = {    /* XXX used in 3 places */
+       /* RFC3262: PRACK 100% reliability */
+       { SIP_OPT_100REL,       NOT_SUPPORTED,  "100rel" },
+       /* RFC3959: SIP Early session support */
+       { SIP_OPT_EARLY_SESSION, NOT_SUPPORTED, "early-session" },
+       /* SIMPLE events:  RFC4662 */
+       { SIP_OPT_EVENTLIST,    NOT_SUPPORTED,  "eventlist" },
+       /* RFC 4916- Connected line ID updates */
+       { SIP_OPT_FROMCHANGE,   NOT_SUPPORTED,  "from-change" },
+       /* GRUU: Globally Routable User Agent URI's */
+       { SIP_OPT_GRUU,         NOT_SUPPORTED,  "gruu" },
+       /* RFC4244 History info */
+       { SIP_OPT_HISTINFO,     NOT_SUPPORTED,  "histinfo" },
+       /* RFC3911: SIP Join header support */
+       { SIP_OPT_JOIN,         NOT_SUPPORTED,  "join" },
+       /* Disable the REFER subscription, RFC 4488 */
+       { SIP_OPT_NOREFERSUB,   NOT_SUPPORTED,  "norefersub" },
+       /* SIP outbound - the final NAT battle - draft-sip-outbound */
+       { SIP_OPT_OUTBOUND,     NOT_SUPPORTED,  "outbound" },
+       /* RFC3327: Path support */
+       { SIP_OPT_PATH,         NOT_SUPPORTED,  "path" },
+       /* RFC3840: Callee preferences */
+       { SIP_OPT_PREF,         NOT_SUPPORTED,  "pref" },
+       /* RFC3312: Precondition support */
+       { SIP_OPT_PRECONDITION, NOT_SUPPORTED,  "precondition" },
+       /* RFC3323: Privacy with proxies*/
+       { SIP_OPT_PRIVACY,      NOT_SUPPORTED,  "privacy" },
+       /* RFC-ietf-sip-uri-list-conferencing-02.txt conference invite lists */
+       { SIP_OPT_RECLISTINV,   NOT_SUPPORTED,  "recipient-list-invite" },
+       /* RFC-ietf-sip-uri-list-subscribe-02.txt - subscription lists */
+       { SIP_OPT_RECLISTSUB,   NOT_SUPPORTED,  "recipient-list-subscribe" },
+       /* RFC3891: Replaces: header for transfer */
+       { SIP_OPT_REPLACES,     SUPPORTED,      "replaces" },
+       /* One version of Polycom firmware has the wrong label */
+       { SIP_OPT_REPLACES,     SUPPORTED,      "replace" },
+       /* RFC4412 Resource priorities */
+       { SIP_OPT_RESPRIORITY,  NOT_SUPPORTED,  "resource-priority" },
+       /* RFC3329: Security agreement mechanism */
+       { SIP_OPT_SEC_AGREE,    NOT_SUPPORTED,  "sec_agree" },
+       /* RFC4092: Usage of the SDP ANAT Semantics in the SIP */
+       { SIP_OPT_SDP_ANAT,     NOT_SUPPORTED,  "sdp-anat" },
+       /* RFC4028: SIP Session-Timers */
+       { SIP_OPT_TIMER,        SUPPORTED,      "timer" },
+       /* RFC4538: Target-dialog */
+       { SIP_OPT_TARGET_DIALOG,NOT_SUPPORTED,  "tdialog" },
+};
 
 #endif
index 446e963..bf35bc2 100644 (file)
@@ -1575,6 +1575,235 @@ AST_TEST_DEFINE(parse_contact_header_test)
        return res;
 }
 
+/*!
+ * \brief Parse supported header in incoming packet
+ *
+ * \details This function parses through the options parameters and
+ * builds a bit field representing all the SIP options in that field. When an
+ * item is found that is not supported, it is copied to the unsupported
+ * out buffer.
+ *
+ * \param option list
+ * \param unsupported out buffer (optional)
+ * \param unsupported out buffer length (optional)
+ */
+unsigned int parse_sip_options(const char *options, char *unsupported, size_t unsupported_len)
+{
+       char *next, *sep;
+       char *temp;
+       int i, found, supported;
+       unsigned int profile = 0;
+
+       char *out = unsupported;
+       size_t outlen = unsupported_len;
+       char *cur_out = out;
+
+       if (out && (outlen > 0)) {
+               memset(out, 0, outlen);
+       }
+
+       if (ast_strlen_zero(options) )
+               return 0;
+
+       temp = ast_strdupa(options);
+
+       ast_debug(3, "Begin: parsing SIP \"Supported: %s\"\n", options);
+
+       for (next = temp; next; next = sep) {
+               found = FALSE;
+               supported = FALSE;
+               if ((sep = strchr(next, ',')) != NULL) {
+                       *sep++ = '\0';
+               }
+
+               /* trim leading and trailing whitespace */
+               next = ast_strip(next);
+
+               if (ast_strlen_zero(next)) {
+                       continue; /* if there is a blank argument in there just skip it */
+               }
+
+               ast_debug(3, "Found SIP option: -%s-\n", next);
+               for (i = 0; i < ARRAY_LEN(sip_options); i++) {
+                       if (!strcasecmp(next, sip_options[i].text)) {
+                               profile |= sip_options[i].id;
+                               if (sip_options[i].supported == SUPPORTED) {
+                                       supported = TRUE;
+                               }
+                               found = TRUE;
+                               ast_debug(3, "Matched SIP option: %s\n", next);
+                               break;
+                       }
+               }
+
+               /* If option is not supported, add to unsupported out buffer */
+               if (!supported && out && outlen) {
+                       size_t copylen = strlen(next);
+                       size_t cur_outlen = strlen(out);
+                       /* Check to see if there is enough room to store this option.
+                        * Copy length is string length plus 2 for the ',' and '\0' */
+                       if ((cur_outlen + copylen + 2) < outlen) {
+                               /* if this isn't the first item, add the ',' */
+                               if (cur_outlen) {
+                                       *cur_out = ',';
+                                       cur_out++;
+                                       cur_outlen++;
+                               }
+                               ast_copy_string(cur_out, next, (outlen - cur_outlen));
+                               cur_out += copylen;
+                       }
+               }
+
+               if (!found) {
+                       profile |= SIP_OPT_UNKNOWN;
+                       if (!strncasecmp(next, "x-", 2))
+                               ast_debug(3, "Found private SIP option, not supported: %s\n", next);
+                       else
+                               ast_debug(3, "Found no match for SIP option: %s (Please file bug report!)\n", next);
+               }
+       }
+
+       return profile;
+}
+
+AST_TEST_DEFINE(sip_parse_options_test)
+{
+       int res = AST_TEST_PASS;
+       char unsupported[64];
+       unsigned int option_profile = 0;
+       struct testdata {
+               char *name;
+               char *input_options;
+               char *expected_unsupported;
+               unsigned int expected_profile;
+               AST_LIST_ENTRY(testdata) list;
+       };
+
+       struct testdata *testdataptr;
+       static AST_LIST_HEAD_NOLOCK(testdataliststruct, testdata) testdatalist;
+
+       struct testdata test1 = {
+               .name = "test_all_unsupported",
+               .input_options = "unsupported1,,, ,unsupported2,unsupported3,unsupported4",
+               .expected_unsupported = "unsupported1,unsupported2,unsupported3,unsupported4",
+               .expected_profile = SIP_OPT_UNKNOWN,
+       };
+       struct testdata test2 = {
+               .name = "test_all_unsupported_one_supported",
+               .input_options = "  unsupported1, replaces,   unsupported3  , , , ,unsupported4",
+               .expected_unsupported = "unsupported1,unsupported3,unsupported4",
+               .expected_profile = SIP_OPT_UNKNOWN | SIP_OPT_REPLACES
+       };
+       struct testdata test3 = {
+               .name = "test_two_supported_two_unsupported",
+               .input_options = ",,  timer  ,replaces     ,unsupported3,unsupported4",
+               .expected_unsupported = "unsupported3,unsupported4",
+               .expected_profile = SIP_OPT_UNKNOWN | SIP_OPT_REPLACES | SIP_OPT_TIMER,
+       };
+
+       struct testdata test4 = {
+               .name = "test_all_supported",
+               .input_options = "timer,replaces",
+               .expected_unsupported = "",
+               .expected_profile = SIP_OPT_REPLACES | SIP_OPT_TIMER,
+       };
+
+       struct testdata test5 = {
+               .name = "test_all_supported_redundant",
+               .input_options = "timer,replaces,timer,replace,timer,replaces",
+               .expected_unsupported = "",
+               .expected_profile = SIP_OPT_REPLACES | SIP_OPT_TIMER,
+       };
+       struct testdata test6 = {
+               .name = "test_buffer_overflow",
+               .input_options = "unsupported1,replaces,timer,unsupported4,unsupported_huge____"
+               "____________________________________,__________________________________________"
+               "________________________________________________",
+               .expected_unsupported = "unsupported1,unsupported4",
+               .expected_profile = SIP_OPT_UNKNOWN | SIP_OPT_REPLACES | SIP_OPT_TIMER,
+       };
+       struct testdata test7 = {
+               .name = "test_null_input",
+               .input_options = NULL,
+               .expected_unsupported = "",
+               .expected_profile = 0,
+       };
+       struct testdata test8 = {
+               .name = "test_whitespace_input",
+               .input_options = "         ",
+               .expected_unsupported = "",
+               .expected_profile = 0,
+       };
+       struct testdata test9 = {
+               .name = "test_whitespace_plus_option_input",
+               .input_options = " , , ,timer , ,  , ,        ,    ",
+               .expected_unsupported = "",
+               .expected_profile = SIP_OPT_TIMER,
+       };
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "sip_parse_options_test";
+               info->category = "channels/chan_sip/";
+               info->summary = "Tests parsing of sip options";
+               info->description =
+                                                       "Tests parsing of SIP options from supported and required "
+                                                       "header fields.  Verifies when unsupported options are encountered "
+                                                       "that they are appended to the unsupported out buffer and that the "
+                                                       "correct bit field representnig the option profile is returned.";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       AST_LIST_HEAD_SET_NOLOCK(&testdatalist, &test1);
+       AST_LIST_INSERT_TAIL(&testdatalist, &test2, list);
+       AST_LIST_INSERT_TAIL(&testdatalist, &test3, list);
+       AST_LIST_INSERT_TAIL(&testdatalist, &test4, list);
+       AST_LIST_INSERT_TAIL(&testdatalist, &test5, list);
+       AST_LIST_INSERT_TAIL(&testdatalist, &test6, list);
+       AST_LIST_INSERT_TAIL(&testdatalist, &test7, list);
+       AST_LIST_INSERT_TAIL(&testdatalist, &test8, list);
+       AST_LIST_INSERT_TAIL(&testdatalist, &test9, list);
+
+       /* Test with unsupported char buffer */
+       AST_LIST_TRAVERSE(&testdatalist, testdataptr, list) {
+               option_profile = parse_sip_options(testdataptr->input_options, unsupported, ARRAY_LEN(unsupported));
+               if (option_profile != testdataptr->expected_profile ||
+                       strcmp(unsupported, testdataptr->expected_unsupported)) {
+                       ast_test_status_update(test, "Test with output buffer \"%s\", expected unsupported: %s actual unsupported:"
+                               "%s expected bit profile: %x actual bit profile: %x\n",
+                               testdataptr->name,
+                               testdataptr->expected_unsupported,
+                               unsupported,
+                               testdataptr->expected_profile,
+                               option_profile);
+                       res = AST_TEST_FAIL;
+               } else {
+                       ast_test_status_update(test, "\"%s\" passed got expected unsupported: %s and bit profile: %x\n",
+                               testdataptr->name,
+                               unsupported,
+                               option_profile);
+               }
+
+               option_profile = parse_sip_options(testdataptr->input_options, NULL, 0);
+               if (option_profile != testdataptr->expected_profile) {
+                       ast_test_status_update(test, "NULL output test \"%s\", expected bit profile: %x actual bit profile: %x\n",
+                               testdataptr->name,
+                               testdataptr->expected_profile,
+                               option_profile);
+                       res = AST_TEST_FAIL;
+               } else {
+                       ast_test_status_update(test, "\"%s\" with NULL output buf passed, bit profile: %x\n",
+                               testdataptr->name,
+                               option_profile);
+               }
+       }
+
+       return res;
+}
+
+
 void sip_request_parser_register_tests(void)
 {
        AST_TEST_REGISTER(get_calleridname_test);
@@ -1584,6 +1813,7 @@ void sip_request_parser_register_tests(void)
        AST_TEST_REGISTER(sip_parse_uri_fully_test);
        AST_TEST_REGISTER(parse_name_andor_addr_test);
        AST_TEST_REGISTER(parse_contact_header_test);
+       AST_TEST_REGISTER(sip_parse_options_test);
 }
 void sip_request_parser_unregister_tests(void)
 {
@@ -1594,4 +1824,5 @@ void sip_request_parser_unregister_tests(void)
        AST_TEST_UNREGISTER(sip_parse_uri_fully_test);
        AST_TEST_UNREGISTER(parse_name_andor_addr_test);
        AST_TEST_UNREGISTER(parse_contact_header_test);
+       AST_TEST_UNREGISTER(sip_parse_options_test);
 }