-----Changes -----
[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         switch (cmd) {
127         case TEST_INIT:
128                 info->name = "sip_uri_parse_test";
129                 info->category = "channels/chan_sip/";
130                 info->summary = "tests sip uri parsing";
131                 info->description =
132                                                         " Tests parsing of various URIs"
133                                                         " Verifies output matches expected behavior.";
134                 return AST_TEST_NOT_RUN;
135         case TEST_EXECUTE:
136                 break;
137         }
138
139         /* Test 1, simple URI */
140         name = pass = domain = port = transport = NULL;
141         if (parse_uri(uri1, "sip:,sips:", &name, &pass, &domain, &port, &transport) ||
142                 strcmp(name, "name")        ||
143                 !ast_strlen_zero(pass)      ||
144                 strcmp(domain, "host")      ||
145                 !ast_strlen_zero(port)      ||
146                 !ast_strlen_zero(transport)) {
147
148                 ast_str_append(&args->ast_test_error_str, 0, "Test 1: simple uri failed. \n");
149                 res = AST_TEST_FAIL;
150         }
151
152         /* Test 2, add tcp transport */
153         name = pass = domain = port = transport = NULL;
154         if (parse_uri(uri2, "sip:,sips:", &name, &pass, &domain, &port, &transport) ||
155                 strcmp(name, "name")        ||
156                 !ast_strlen_zero(pass)      ||
157                 strcmp(domain, "host")    ||
158                 !ast_strlen_zero(port)      ||
159                 strcmp(transport, "tcp")) {
160
161                 ast_str_append(&args->ast_test_error_str, 0, "Test 2: uri with addtion of tcp transport failed. \n");
162                 res = AST_TEST_FAIL;
163         }
164
165         /* Test 3, add secret */
166         name = pass = domain = port = transport = NULL;
167         if (parse_uri(uri3, "sip:,sips:", &name, &pass, &domain, &port, &transport) ||
168             strcmp(name, "name")        ||
169                 strcmp(pass, "secret")      ||
170                 strcmp(domain, "host")    ||
171                 !ast_strlen_zero(port)      ||
172                 strcmp(transport, "tcp")) {
173
174                 ast_str_append(&args->ast_test_error_str, 0, "Test 3: uri with addition of secret failed.\n");
175                 res = AST_TEST_FAIL;
176         }
177
178         /* Test 4, add port and unparsed header field*/
179         name = pass = domain = port = transport = NULL;
180         if (parse_uri(uri4, "sip:,sips:", &name, &pass, &domain, &port, &transport) ||
181             strcmp(name, "name")        ||
182                 strcmp(pass, "secret")      ||
183                 strcmp(domain, "host")    ||
184                 strcmp(port, "port")      ||
185                 strcmp(transport, "tcp")) {
186
187                 ast_str_append(&args->ast_test_error_str, 0, "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_str_append(&args->ast_test_error_str, 0, "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(uri4, "sip:,sips:", NULL, NULL, NULL, NULL, NULL)) {
201                 ast_str_append(&args->ast_test_error_str, 0, "Test 6: passing NULL output parameters failed.\n");
202                 res = AST_TEST_FAIL;
203         }
204
205         return res;
206 }
207
208 /*! \brief  Get caller id name from SIP headers, copy into output buffer
209  *
210  *  \retval input string pointer placed after display-name field if possible
211  */
212 const char *get_calleridname(const char *input, char *output, size_t outputsize)
213 {
214         /* From RFC3261:
215          * 
216          * From           =  ( "From" / "f" ) HCOLON from-spec
217          * from-spec      =  ( name-addr / addr-spec ) *( SEMI from-param )
218          * name-addr      =  [ display-name ] LAQUOT addr-spec RAQUOT
219          * display-name   =  *(token LWS)/ quoted-string
220          * token          =  1*(alphanum / "-" / "." / "!" / "%" / "*"
221          *                     / "_" / "+" / "`" / "'" / "~" )
222          * quoted-string  =  SWS DQUOTE *(qdtext / quoted-pair ) DQUOTE
223          * qdtext         =  LWS / %x21 / %x23-5B / %x5D-7E
224          *                     / UTF8-NONASCII
225          * quoted-pair    =  "\" (%x00-09 / %x0B-0C / %x0E-7F)
226          *
227          * HCOLON         = *WSP ":" SWS
228          * SWS            = [LWS]
229          * LWS            = *[*WSP CRLF] 1*WSP
230          * WSP            = (SP / HTAB)
231          *
232          * Deviations from it:
233          * - following CRLF's in LWS is not done (here at least)
234          * - ascii NUL is never legal as it terminates the C-string
235          * - utf8-nonascii is not checked for validity
236          */
237         char *orig_output = output;
238         const char *orig_input = input;
239
240         /* clear any empty characters in the beginning */
241         input = ast_skip_blanks(input);
242
243         /* no data at all or no storage room? */
244         if (!input || *input == '<' || !outputsize || !output) {
245                 return orig_input;
246         }
247
248         /* make sure the output buffer is initilized */
249         *orig_output = '\0';
250
251         /* make room for '\0' at the end of the output buffer */
252         outputsize--;
253
254         /* quoted-string rules */
255         if (input[0] == '"') {
256                 input++; /* skip the first " */
257
258                 for (;((outputsize > 0) && *input); input++) {
259                         if (*input == '"') {  /* end of quoted-string */
260                                 break;
261                         } else if (*input == 0x5c) { /* quoted-pair = "\" (%x00-09 / %x0B-0C / %x0E-7F) */
262                                 input++;
263                                 if (!*input || (unsigned char)*input > 0x7f || *input == 0xa || *input == 0xd) {
264                                         continue;  /* not a valid quoted-pair, so skip it */
265                                 }
266                         } else if (((*input != 0x9) && ((unsigned char) *input < 0x20)) ||
267                                     (*input == 0x7f)) {
268                                 continue; /* skip this invalid character. */
269                         }
270
271                         *output++ = *input;
272                         outputsize--;
273                 }
274
275                 /* if this is successful, input should be at the ending quote */
276                 if (!input || *input != '"') {
277                         ast_log(LOG_WARNING, "No ending quote for display-name was found\n");
278                         *orig_output = '\0';
279                         return orig_input;
280                 }
281
282                 /* make sure input is past the last quote */
283                 input++;
284
285                 /* terminate outbuf */
286                 *output = '\0';
287         } else {  /* either an addr-spec or tokenLWS-combo */
288                 for (;((outputsize > 0) && *input); input++) {
289                         /* token or WSP (without LWS) */
290                         if ((*input >= '0' && *input <= '9') || (*input >= 'A' && *input <= 'Z')
291                                 || (*input >= 'a' && *input <= 'z') || *input == '-' || *input == '.'
292                                 || *input == '!' || *input == '%' || *input == '*' || *input == '_'
293                                 || *input == '+' || *input == '`' || *input == '\'' || *input == '~'
294                                 || *input == 0x9 || *input == ' ') {
295                                 *output++ = *input;
296                                 outputsize -= 1;
297                         } else if (*input == '<') {   /* end of tokenLWS-combo */
298                                 /* we could assert that the previous char is LWS, but we don't care */
299                                 break;
300                         } else if (*input == ':') {
301                                 /* This invalid character which indicates this is addr-spec rather than display-name. */
302                                 *orig_output = '\0';
303                                 return orig_input;
304                         } else {         /* else, invalid character we can skip. */
305                                 continue;    /* skip this character */
306                         }
307                 }
308
309                 /* set NULL while trimming trailing whitespace */
310                 do {
311                         *output-- = '\0';
312                 } while (*output == 0x9 || *output == ' '); /* we won't go past orig_output as first was a non-space */
313         }
314
315         return input;
316 }
317
318 AST_TEST_DEFINE(get_calleridname_test)
319 {
320         int res = AST_TEST_PASS;
321         const char *in1 = "\" quoted-text internal \\\" quote \"<stuff>";
322         const char *in2 = " token text with no quotes <stuff>";
323         const char *overflow1 = " \"quoted-text overflow 1234567890123456789012345678901234567890\" <stuff>";
324         const char *noendquote = " \"quoted-text no end <stuff>";
325         const char *addrspec = " \"sip:blah@blah <stuff>";
326         const char *after_dname;
327         char dname[40];
328
329         switch (cmd) {
330         case TEST_INIT:
331                 info->name = "sip_get_calleridname_test";
332                 info->category = "channels/chan_sip/";
333                 info->summary = "decodes callerid name from sip header";
334                 info->description = "Decodes display-name field of sip header.  Checks for valid output and expected failure cases.";
335                 return AST_TEST_NOT_RUN;
336         case TEST_EXECUTE:
337                 break;
338         }
339
340         /* quoted-text with backslash escaped quote */
341         after_dname = get_calleridname(in1, dname, sizeof(dname));
342         ast_test_status_update(&args->status_update, "display-name1: %s\nafter: %s\n", dname, after_dname);
343         if (strcmp(dname, " quoted-text internal \" quote ")) {
344                 ast_test_status_update(&args->status_update, "display-name1 test failed\n");
345                 ast_str_append(&args->ast_test_error_str, 0, "quoted-text with internal backslash decode failed. \n");
346                 res = AST_TEST_FAIL;
347         }
348
349         /* token text */
350         after_dname = get_calleridname(in2, dname, sizeof(dname));
351         ast_test_status_update(&args->status_update, "display-name2: %s\nafter: %s\n", dname, after_dname);
352         if (strcmp(dname, "token text with no quotes")) {
353                 ast_test_status_update(&args->status_update, "display-name2 test failed\n");
354                 ast_str_append(&args->ast_test_error_str, 0, "token text with decode failed. \n");
355                 res = AST_TEST_FAIL;
356         }
357
358         /* quoted-text buffer overflow */
359         after_dname = get_calleridname(overflow1, dname, sizeof(dname));
360         ast_test_status_update(&args->status_update, "overflow display-name1: %s\nafter: %s\n", dname, after_dname);
361         if (*dname != '\0' && after_dname != overflow1) {
362                 ast_test_status_update(&args->status_update, "overflow display-name1 test failed\n");
363                 ast_str_append(&args->ast_test_error_str, 0, "quoted-text buffer overflow check failed. \n");
364                 res = AST_TEST_FAIL;
365         }
366
367         /* quoted-text buffer with no terminating end quote */
368         after_dname = get_calleridname(noendquote, dname, sizeof(dname));
369         ast_test_status_update(&args->status_update, "noendquote display-name1: %s\nafter: %s\n", dname, after_dname);
370         if (*dname != '\0' && after_dname != noendquote) {
371                 ast_test_status_update(&args->status_update, "no end quote for quoted-text display-name failed\n");
372                 ast_str_append(&args->ast_test_error_str, 0, "quoted-text buffer check no terminating end quote failed. \n");
373                 res = AST_TEST_FAIL;
374         }
375
376         /* addr-spec rather than display-name. */
377         after_dname = get_calleridname(addrspec, dname, sizeof(dname));
378         ast_test_status_update(&args->status_update, "noendquote display-name1: %s\nafter: %s\n", dname, after_dname);
379         if (*dname != '\0' && after_dname != addrspec) {
380                 ast_test_status_update(&args->status_update, "detection of addr-spec failed\n");
381                 ast_str_append(&args->ast_test_error_str, 0, "detection of addr-spec failed. \n");
382                 res = AST_TEST_FAIL;
383         }
384
385         return res;
386 }
387
388
389 void sip_request_parser_register_tests(void)
390 {
391         AST_TEST_REGISTER(get_calleridname_test);
392         AST_TEST_REGISTER(sip_parse_uri_test);
393 }
394 void sip_request_parser_unregister_tests(void)
395 {
396         AST_TEST_UNREGISTER(sip_parse_uri_test);
397         AST_TEST_UNREGISTER(get_calleridname_test);
398 }