additional parse_uri test and documentation
[asterisk/asterisk.git] / channels / sip / reqresp_parser.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2010, Digium, Inc.
5  *
6  * See http://www.asterisk.org for more information about
7  * the Asterisk project. Please do not directly contact
8  * any of the maintainers of this project for assistance;
9  * the project provides a web site, mailing lists and IRC
10  * channels for your use.
11  *
12  * This program is free software, distributed under the terms of
13  * the GNU General Public License Version 2. See the LICENSE file
14  * at the top of the source tree.
15  */
16
17 /*!
18  * \file
19  * \brief sip request parsing functions and unit tests
20  */
21
22 #include "asterisk.h"
23
24 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
25
26 #include "include/sip.h"
27 #include "include/reqresp_parser.h"
28
29 /*! \brief * parses a URI in its components.*/
30 int parse_uri(char *uri, const char *scheme, char **ret_name, char **pass, char **domain, char **port, char **transport)
31 {
32         char *name = NULL;
33         char *tmp; /* used as temporary place holder */
34         int error = 0;
35
36         /* check for valid input */
37         if (ast_strlen_zero(uri)) {
38                 return -1;
39         }
40
41         /* strip [?headers] from end of uri */
42         if ((tmp = strrchr(uri, '?'))) {
43                 *tmp = '\0';
44         }
45
46         /* init field as required */
47         if (pass)
48                 *pass = "";
49         if (port)
50                 *port = "";
51         if (scheme) {
52                 int l;
53                 char *scheme2 = ast_strdupa(scheme);
54                 char *cur = strsep(&scheme2, ",");
55                 for (; !ast_strlen_zero(cur); cur = strsep(&scheme2, ",")) {
56                         l = strlen(cur);
57                         if (!strncasecmp(uri, cur, l)) {
58                                 uri += l;
59                                 break;
60                         }
61                 }
62                 if (ast_strlen_zero(cur)) {
63                         ast_debug(1, "No supported scheme found in '%s' using the scheme[s] %s\n", uri, scheme);
64                         error = -1;
65                 }
66         }
67         if (transport) {
68                 char *t, *type = "";
69                 *transport = "";
70                 if ((t = strstr(uri, "transport="))) {
71                         strsep(&t, "=");
72                         if ((type = strsep(&t, ";"))) {
73                                 *transport = type;
74                         }
75                 }
76         }
77
78         if (!domain) {
79                 /* if we don't want to split around domain, keep everything as a name,
80                  * so we need to do nothing here, except remember why.
81                  */
82         } else {
83                 /* store the result in a temp. variable to avoid it being
84                  * overwritten if arguments point to the same place.
85                  */
86                 char *c, *dom = "";
87
88                 if ((c = strchr(uri, '@')) == NULL) {
89                         /* domain-only URI, according to the SIP RFC. */
90                         dom = uri;
91                         name = "";
92                 } else {
93                         *c++ = '\0';
94                         dom = c;
95                         name = uri;
96                 }
97
98                 /* Remove parameters in domain and name */
99                 dom = strsep(&dom, ";");
100                 name = strsep(&name, ";");
101
102                 if (port && (c = strchr(dom, ':'))) { /* Remove :port */
103                         *c++ = '\0';
104                         *port = c;
105                 }
106                 if (pass && (c = strchr(name, ':'))) {  /* user:password */
107                         *c++ = '\0';
108                         *pass = c;
109                 }
110                 *domain = dom;
111         }
112         if (ret_name)   /* same as for domain, store the result only at the end */
113                 *ret_name = name;
114
115         return error;
116 }
117
118 AST_TEST_DEFINE(sip_parse_uri_test)
119 {
120         int res = AST_TEST_PASS;
121         char *name, *pass, *domain, *port, *transport;
122         char uri1[] = "sip:name@host";
123         char uri2[] = "sip:name@host;transport=tcp";
124         char uri3[] = "sip:name:secret@host;transport=tcp";
125         char uri4[] = "sip:name:secret@host:port;transport=tcp?headers=%40%40testblah&headers2=blah%20blah";
126         /* test 5 is for NULL input */
127         char uri6[] = "sip:name:secret@host:port;transport=tcp?headers=%40%40testblah&headers2=blah%20blah";
128         char uri7[] = "sip:name:secret@host:port;transport=tcp?headers=%40%40testblah&headers2=blah%20blah";
129
130         switch (cmd) {
131         case TEST_INIT:
132                 info->name = "sip_uri_parse_test";
133                 info->category = "channels/chan_sip/";
134                 info->summary = "tests sip uri parsing";
135                 info->description =
136                                                         " Tests parsing of various URIs"
137                                                         " Verifies output matches expected behavior.";
138                 return AST_TEST_NOT_RUN;
139         case TEST_EXECUTE:
140                 break;
141         }
142
143         /* Test 1, simple URI */
144         name = pass = domain = port = transport = NULL;
145         if (parse_uri(uri1, "sip:,sips:", &name, &pass, &domain, &port, &transport) ||
146                         strcmp(name, "name")        ||
147                         !ast_strlen_zero(pass)      ||
148                         strcmp(domain, "host")      ||
149                         !ast_strlen_zero(port)      ||
150                         !ast_strlen_zero(transport)) {
151                 ast_test_status_update(test, "Test 1: simple uri failed. \n");
152                 res = AST_TEST_FAIL;
153         }
154
155         /* Test 2, add tcp transport */
156         name = pass = domain = port = transport = NULL;
157         if (parse_uri(uri2, "sip:,sips:", &name, &pass, &domain, &port, &transport) ||
158                         strcmp(name, "name")        ||
159                         !ast_strlen_zero(pass)      ||
160                         strcmp(domain, "host")    ||
161                         !ast_strlen_zero(port)      ||
162                         strcmp(transport, "tcp")) {
163                 ast_test_status_update(test, "Test 2: uri with addtion of tcp transport failed. \n");
164                 res = AST_TEST_FAIL;
165         }
166
167         /* Test 3, add secret */
168         name = pass = domain = port = transport = NULL;
169         if (parse_uri(uri3, "sip:,sips:", &name, &pass, &domain, &port, &transport) ||
170                         strcmp(name, "name")        ||
171                         strcmp(pass, "secret")      ||
172                         strcmp(domain, "host")    ||
173                         !ast_strlen_zero(port)      ||
174                         strcmp(transport, "tcp")) {
175                 ast_test_status_update(test, "Test 3: uri with addition of secret failed.\n");
176                 res = AST_TEST_FAIL;
177         }
178
179         /* Test 4, add port and unparsed header field*/
180         name = pass = domain = port = transport = NULL;
181         if (parse_uri(uri4, "sip:,sips:", &name, &pass, &domain, &port, &transport) ||
182                         strcmp(name, "name")        ||
183                         strcmp(pass, "secret")      ||
184                         strcmp(domain, "host")    ||
185                         strcmp(port, "port")      ||
186                         strcmp(transport, "tcp")) {
187                 ast_test_status_update(test, "Test 4: add port and unparsed header field failed.\n");
188                 res = AST_TEST_FAIL;
189         }
190
191         /* Test 5, verify parse_uri does not crash when given a NULL uri */
192         name = pass = domain = port = transport = NULL;
193         if (!parse_uri(NULL, "sip:,sips:", &name, &pass, &domain, &port, &transport)) {
194                 ast_test_status_update(test, "Test 5: passing a NULL uri failed.\n");
195                 res = AST_TEST_FAIL;
196         }
197
198         /* Test 6, verify parse_uri does not crash when given a NULL output parameters */
199         name = pass = domain = port = transport = NULL;
200         if (parse_uri(uri6, "sip:,sips:", NULL, NULL, NULL, NULL, NULL)) {
201                 ast_test_status_update(test, "Test 6: passing NULL output parameters failed.\n");
202                 res = AST_TEST_FAIL;
203         }
204
205         /* Test 7, verify parse_uri returns user:secret and domain:port when no port or secret output parameters are supplied. */
206         name = pass = domain = port = transport = NULL;
207         if (parse_uri(uri7, "sip:,sips:", &name, NULL, &domain, NULL, NULL) ||
208                         strcmp(name, "name:secret")        ||
209                         strcmp(domain, "host:port")) {
210
211                 ast_test_status_update(test, "Test 7: providing no port and secret output parameters failed.\n");
212                 res = AST_TEST_FAIL;
213         }
214         return res;
215 }
216
217 /*! \brief  Get caller id name from SIP headers, copy into output buffer
218  *
219  *  \retval input string pointer placed after display-name field if possible
220  */
221 const char *get_calleridname(const char *input, char *output, size_t outputsize)
222 {
223         /* From RFC3261:
224          * 
225          * From           =  ( "From" / "f" ) HCOLON from-spec
226          * from-spec      =  ( name-addr / addr-spec ) *( SEMI from-param )
227          * name-addr      =  [ display-name ] LAQUOT addr-spec RAQUOT
228          * display-name   =  *(token LWS)/ quoted-string
229          * token          =  1*(alphanum / "-" / "." / "!" / "%" / "*"
230          *                     / "_" / "+" / "`" / "'" / "~" )
231          * quoted-string  =  SWS DQUOTE *(qdtext / quoted-pair ) DQUOTE
232          * qdtext         =  LWS / %x21 / %x23-5B / %x5D-7E
233          *                     / UTF8-NONASCII
234          * quoted-pair    =  "\" (%x00-09 / %x0B-0C / %x0E-7F)
235          *
236          * HCOLON         = *WSP ":" SWS
237          * SWS            = [LWS]
238          * LWS            = *[*WSP CRLF] 1*WSP
239          * WSP            = (SP / HTAB)
240          *
241          * Deviations from it:
242          * - following CRLF's in LWS is not done (here at least)
243          * - ascii NUL is never legal as it terminates the C-string
244          * - utf8-nonascii is not checked for validity
245          */
246         char *orig_output = output;
247         const char *orig_input = input;
248
249         /* clear any empty characters in the beginning */
250         input = ast_skip_blanks(input);
251
252         /* no data at all or no storage room? */
253         if (!input || *input == '<' || !outputsize || !output) {
254                 return orig_input;
255         }
256
257         /* make sure the output buffer is initilized */
258         *orig_output = '\0';
259
260         /* make room for '\0' at the end of the output buffer */
261         outputsize--;
262
263         /* quoted-string rules */
264         if (input[0] == '"') {
265                 input++; /* skip the first " */
266
267                 for (;((outputsize > 0) && *input); input++) {
268                         if (*input == '"') {  /* end of quoted-string */
269                                 break;
270                         } else if (*input == 0x5c) { /* quoted-pair = "\" (%x00-09 / %x0B-0C / %x0E-7F) */
271                                 input++;
272                                 if (!*input || (unsigned char)*input > 0x7f || *input == 0xa || *input == 0xd) {
273                                         continue;  /* not a valid quoted-pair, so skip it */
274                                 }
275                         } else if (((*input != 0x9) && ((unsigned char) *input < 0x20)) ||
276                                     (*input == 0x7f)) {
277                                 continue; /* skip this invalid character. */
278                         }
279
280                         *output++ = *input;
281                         outputsize--;
282                 }
283
284                 /* if this is successful, input should be at the ending quote */
285                 if (!input || *input != '"') {
286                         ast_log(LOG_WARNING, "No ending quote for display-name was found\n");
287                         *orig_output = '\0';
288                         return orig_input;
289                 }
290
291                 /* make sure input is past the last quote */
292                 input++;
293
294                 /* terminate outbuf */
295                 *output = '\0';
296         } else {  /* either an addr-spec or tokenLWS-combo */
297                 for (;((outputsize > 0) && *input); input++) {
298                         /* token or WSP (without LWS) */
299                         if ((*input >= '0' && *input <= '9') || (*input >= 'A' && *input <= 'Z')
300                                 || (*input >= 'a' && *input <= 'z') || *input == '-' || *input == '.'
301                                 || *input == '!' || *input == '%' || *input == '*' || *input == '_'
302                                 || *input == '+' || *input == '`' || *input == '\'' || *input == '~'
303                                 || *input == 0x9 || *input == ' ') {
304                                 *output++ = *input;
305                                 outputsize -= 1;
306                         } else if (*input == '<') {   /* end of tokenLWS-combo */
307                                 /* we could assert that the previous char is LWS, but we don't care */
308                                 break;
309                         } else if (*input == ':') {
310                                 /* This invalid character which indicates this is addr-spec rather than display-name. */
311                                 *orig_output = '\0';
312                                 return orig_input;
313                         } else {         /* else, invalid character we can skip. */
314                                 continue;    /* skip this character */
315                         }
316                 }
317
318                 /* set NULL while trimming trailing whitespace */
319                 do {
320                         *output-- = '\0';
321                 } while (*output == 0x9 || *output == ' '); /* we won't go past orig_output as first was a non-space */
322         }
323
324         return input;
325 }
326
327 AST_TEST_DEFINE(get_calleridname_test)
328 {
329         int res = AST_TEST_PASS;
330         const char *in1 = "\" quoted-text internal \\\" quote \"<stuff>";
331         const char *in2 = " token text with no quotes <stuff>";
332         const char *overflow1 = " \"quoted-text overflow 1234567890123456789012345678901234567890\" <stuff>";
333         const char *noendquote = " \"quoted-text no end <stuff>";
334         const char *addrspec = " \"sip:blah@blah <stuff>";
335         const char *after_dname;
336         char dname[40];
337
338         switch (cmd) {
339         case TEST_INIT:
340                 info->name = "sip_get_calleridname_test";
341                 info->category = "channels/chan_sip/";
342                 info->summary = "decodes callerid name from sip header";
343                 info->description = "Decodes display-name field of sip header.  Checks for valid output and expected failure cases.";
344                 return AST_TEST_NOT_RUN;
345         case TEST_EXECUTE:
346                 break;
347         }
348
349         /* quoted-text with backslash escaped quote */
350         after_dname = get_calleridname(in1, dname, sizeof(dname));
351         ast_test_status_update(test, "display-name1: %s\nafter: %s\n", dname, after_dname);
352         if (strcmp(dname, " quoted-text internal \" quote ")) {
353                 ast_test_status_update(test, "display-name1 test failed\n");
354                 res = AST_TEST_FAIL;
355         }
356
357         /* token text */
358         after_dname = get_calleridname(in2, dname, sizeof(dname));
359         ast_test_status_update(test, "display-name2: %s\nafter: %s\n", dname, after_dname);
360         if (strcmp(dname, "token text with no quotes")) {
361                 ast_test_status_update(test, "display-name2 test failed\n");
362                 res = AST_TEST_FAIL;
363         }
364
365         /* quoted-text buffer overflow */
366         after_dname = get_calleridname(overflow1, dname, sizeof(dname));
367         ast_test_status_update(test, "overflow display-name1: %s\nafter: %s\n", dname, after_dname);
368         if (*dname != '\0' && after_dname != overflow1) {
369                 ast_test_status_update(test, "overflow display-name1 test failed\n");
370                 res = AST_TEST_FAIL;
371         }
372
373         /* quoted-text buffer with no terminating end quote */
374         after_dname = get_calleridname(noendquote, dname, sizeof(dname));
375         ast_test_status_update(test, "noendquote display-name1: %s\nafter: %s\n", dname, after_dname);
376         if (*dname != '\0' && after_dname != noendquote) {
377                 ast_test_status_update(test, "no end quote for quoted-text display-name failed\n");
378                 res = AST_TEST_FAIL;
379         }
380
381         /* addr-spec rather than display-name. */
382         after_dname = get_calleridname(addrspec, dname, sizeof(dname));
383         ast_test_status_update(test, "noendquote display-name1: %s\nafter: %s\n", dname, after_dname);
384         if (*dname != '\0' && after_dname != addrspec) {
385                 ast_test_status_update(test, "detection of addr-spec failed\n");
386                 res = AST_TEST_FAIL;
387         }
388
389         return res;
390 }
391
392
393 void sip_request_parser_register_tests(void)
394 {
395         AST_TEST_REGISTER(get_calleridname_test);
396         AST_TEST_REGISTER(sip_parse_uri_test);
397 }
398 void sip_request_parser_unregister_tests(void)
399 {
400         AST_TEST_UNREGISTER(sip_parse_uri_test);
401         AST_TEST_UNREGISTER(get_calleridname_test);
402 }