Merge "rtp_engine/res_rtp_asterisk: Fix RTP struct reentrancy crashes."
[asterisk/asterisk.git] / res / res_pjsip_header_funcs.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2013, Fairview 5 Engineering, LLC
5  *
6  * George Joseph <george.joseph@fairview5.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*** MODULEINFO
20         <depend>pjproject</depend>
21         <depend>res_pjsip</depend>
22         <depend>res_pjsip_session</depend>
23         <support_level>core</support_level>
24  ***/
25
26 #include "asterisk.h"
27
28 #include <pjsip.h>
29 #include <pjsip_ua.h>
30
31 #include "asterisk/res_pjsip.h"
32 #include "asterisk/res_pjsip_session.h"
33 #include "asterisk/channel.h"
34 #include "asterisk/pbx.h"
35 #include "asterisk/app.h"
36 #include "asterisk/module.h"
37 #include "asterisk/utils.h"
38
39 /*** DOCUMENTATION
40         <function name="PJSIP_HEADER" language="en_US">
41                 <synopsis>
42                         Gets headers from an inbound PJSIP channel. Adds, updates or removes the
43                         specified SIP header from an outbound PJSIP channel.
44                 </synopsis>
45                 <syntax>
46                         <parameter name="action" required="true">
47                                 <enumlist>
48                                         <enum name="read"><para>Returns instance <replaceable>number</replaceable>
49                                         of header <replaceable>name</replaceable>.</para></enum>
50
51                                         <enum name="add"><para>Adds a new header <replaceable>name</replaceable>
52                                         to this session.</para></enum>
53
54                                         <enum name="update"><para>Updates instance <replaceable>number</replaceable>
55                                         of header <replaceable>name</replaceable> to a new value.
56                                         The header must already exist.</para></enum>
57
58                                         <enum name="remove"><para>Removes all instances of previously added headers
59                                         whose names match <replaceable>name</replaceable>. A <literal>*</literal>
60                                         may be appended to <replaceable>name</replaceable> to remove all headers
61                                         <emphasis>beginning with</emphasis> <replaceable>name</replaceable>.
62                                         <replaceable>name</replaceable> may be set to a single <literal>*</literal>
63                                         to clear <emphasis>all</emphasis> previously added headers. In all cases,
64                                         the number of headers actually removed is returned.</para></enum>
65                                 </enumlist>
66                         </parameter>
67
68                         <parameter name="name" required="true"><para>The name of the header.</para></parameter>
69
70                         <parameter name="number" required="false">
71                                 <para>If there's more than 1 header with the same name, this specifies which header
72                                 to read or update.  If not specified, defaults to <literal>1</literal> meaning
73                                 the first matching header.  Not valid for <literal>add</literal> or
74                                 <literal>remove</literal>.</para>
75                         </parameter>
76
77                 </syntax>
78                 <description>
79                         <para>PJSIP_HEADER allows you to read specific SIP headers from the inbound
80                         PJSIP channel as well as write(add, update, remove) headers on the outbound
81                         channel. One exception is that you can read headers that you have already
82                         added on the outbound channel.</para>
83                         <para>Examples:</para>
84                         <para>;</para>
85                         <para>; Set 'somevar' to the value of the 'From' header.</para>
86                         <para>exten => 1,1,Set(somevar=${PJSIP_HEADER(read,From)})</para>
87                         <para>;</para>
88                         <para>; Set 'via2' to the value of the 2nd 'Via' header.</para>
89                         <para>exten => 1,1,Set(via2=${PJSIP_HEADER(read,Via,2)})</para>
90                         <para>;</para>
91                         <para>; Add an 'X-Myheader' header with the value of 'myvalue'.</para>
92                         <para>exten => 1,1,Set(PJSIP_HEADER(add,X-MyHeader)=myvalue)</para>
93                         <para>;</para>
94                         <para>; Add an 'X-Myheader' header with an empty value.</para>
95                         <para>exten => 1,1,Set(PJSIP_HEADER(add,X-MyHeader)=)</para>
96                         <para>;</para>
97                         <para>; Update the value of the header named 'X-Myheader' to 'newvalue'.</para>
98                         <para>; 'X-Myheader' must already exist or the call will fail.</para>
99                         <para>exten => 1,1,Set(PJSIP_HEADER(update,X-MyHeader)=newvalue)</para>
100                         <para>;</para>
101                         <para>; Remove all headers whose names exactly match 'X-MyHeader'.</para>
102                         <para>exten => 1,1,Set(PJSIP_HEADER(remove,X-MyHeader)=)</para>
103                         <para>;</para>
104                         <para>; Remove all headers that begin with 'X-My'.</para>
105                         <para>exten => 1,1,Set(PJSIP_HEADER(remove,X-My*)=)</para>
106                         <para>;</para>
107                         <para>; Remove all previously added headers.</para>
108                         <para>exten => 1,1,Set(PJSIP_HEADER(remove,*)=)</para>
109                         <para>;</para>
110
111                         <note><para>The <literal>remove</literal> action can be called by reading
112                         <emphasis>or</emphasis> writing PJSIP_HEADER.</para>
113                         <para>;</para>
114                         <para>; Display the number of headers removed</para>
115                         <para>exten => 1,1,Verbose( Removed ${PJSIP_HEADER(remove,X-MyHeader)} headers)</para>
116                         <para>;</para>
117                         <para>; Set a variable to the number of headers removed</para>
118                         <para>exten => 1,1,Set(count=${PJSIP_HEADER(remove,X-MyHeader)})</para>
119                         <para>;</para>
120                         <para>; Just remove them ignoring any count</para>
121                         <para>exten => 1,1,Set(=${PJSIP_HEADER(remove,X-MyHeader)})</para>
122                         <para>exten => 1,1,Set(PJSIP_HEADER(remove,X-MyHeader)=)</para>
123                         <para>;</para>
124                         </note>
125
126                         <note><para>If you call PJSIP_HEADER in a normal dialplan context you'll be
127                         operating on the <emphasis>caller's (incoming)</emphasis> channel which
128                         may not be what you want. To operate on the <emphasis>callee's (outgoing)</emphasis>
129                         channel call PJSIP_HEADER in a pre-dial handler. </para>
130                         <para>Example:</para>
131                         <para>;</para>
132                         <para>[handler]</para>
133                         <para>exten => addheader,1,Set(PJSIP_HEADER(add,X-MyHeader)=myvalue)</para>
134                         <para>exten => addheader,2,Set(PJSIP_HEADER(add,X-MyHeader2)=myvalue2)</para>
135                         <para>;</para>
136                         <para>[somecontext]</para>
137                         <para>exten => 1,1,Dial(PJSIP/${EXTEN},,b(handler^addheader^1))</para>
138                         <para>;</para>
139                         </note>
140                 </description>
141         </function>
142  ***/
143
144 /*! \brief Linked list for accumulating headers */
145 struct hdr_list_entry {
146         pjsip_hdr *hdr;
147         AST_LIST_ENTRY(hdr_list_entry) nextptr;
148 };
149 AST_LIST_HEAD(hdr_list, hdr_list_entry);
150
151 /*! \brief Destructor for hdr_list */
152 static void hdr_list_destroy(void *obj)
153 {
154         AST_LIST_HEAD_DESTROY((struct hdr_list *) obj);
155 }
156
157 /*! \brief Datastore for saving headers */
158 static const struct ast_datastore_info header_datastore = {
159         .type = "header_datastore",
160         .destroy = hdr_list_destroy,
161 };
162
163 /*! \brief Data structure used for ast_sip_push_task_synchronous  */
164 struct header_data {
165         struct ast_sip_channel_pvt *channel;
166         char *header_name;
167         const char *header_value;
168         char *buf;
169         int header_number;
170         size_t len;
171 };
172
173 /*!
174  * \internal
175  * \brief Insert the header pointers into the linked list.
176  *
177  * For each header in the message, allocate a list entry,
178  * clone the header, then insert the entry.
179  */
180 static int insert_headers(pj_pool_t * pool, struct hdr_list *list, pjsip_msg * msg)
181 {
182         pjsip_hdr *hdr = msg->hdr.next;
183         struct hdr_list_entry *le;
184
185         while (hdr && hdr != &msg->hdr) {
186                 le = pj_pool_zalloc(pool, sizeof(struct hdr_list_entry));
187                 le->hdr = pjsip_hdr_clone(pool, hdr);
188                 AST_LIST_INSERT_TAIL(list, le, nextptr);
189                 hdr = hdr->next;
190         }
191
192         return 0;
193 }
194
195 /*!
196  * \internal
197  * \brief Session supplement callback on an incoming INVITE request
198  *
199  * Retrieve the header_datastore from the session or create one if it doesn't exist.
200  * Create and initialize the list if needed.
201  * Insert the headers.
202  */
203 static int incoming_request(struct ast_sip_session *session, pjsip_rx_data * rdata)
204 {
205         pj_pool_t *pool = session->inv_session->dlg->pool;
206         RAII_VAR(struct ast_datastore *, datastore,
207                          ast_sip_session_get_datastore(session, header_datastore.type), ao2_cleanup);
208
209         if (!datastore) {
210                 if (!(datastore =
211                           ast_sip_session_alloc_datastore(&header_datastore, header_datastore.type))
212                         ||
213                         !(datastore->data = pj_pool_alloc(pool, sizeof(struct hdr_list))) ||
214                         ast_sip_session_add_datastore(session, datastore)) {
215                         ast_log(AST_LOG_ERROR, "Unable to create datastore for header functions.\n");
216                         return 0;
217                 }
218                 AST_LIST_HEAD_INIT((struct hdr_list *) datastore->data);
219         }
220         insert_headers(pool, (struct hdr_list *) datastore->data, rdata->msg_info.msg);
221
222         return 0;
223 }
224
225 /*!
226  * \internal
227  * \brief Search list for nth occurrence of specific header.
228  */
229 static pjsip_hdr *find_header(struct hdr_list *list, const char *header_name,
230                                                           int header_number)
231 {
232         struct hdr_list_entry *le;
233         pjsip_hdr *hdr = NULL;
234         int i = 1;
235
236         if (!list || ast_strlen_zero(header_name) || header_number < 1) {
237                 return NULL;
238         }
239
240         AST_LIST_TRAVERSE(list, le, nextptr) {
241                 if (pj_stricmp2(&le->hdr->name, header_name) == 0 && i++ == header_number) {
242                         hdr = le->hdr;
243                         break;
244                 }
245         }
246
247         return hdr;
248 }
249
250
251 /*!
252  * \internal
253  * \brief Implements PJSIP_HEADER 'read' by searching the for the requested header.
254  *
255  * Retrieve the header_datastore.
256  * Search for the nth matching header.
257  * Validate the pjsip_hdr found.
258  * Parse pjsip_hdr into a name and value.
259  * Return the value.
260  */
261 static int read_header(void *obj)
262 {
263         struct header_data *data = obj;
264         pjsip_hdr *hdr = NULL;
265         char *pj_hdr_string;
266         size_t pj_hdr_string_len;
267         char *p;
268         size_t plen;
269         RAII_VAR(struct ast_datastore *, datastore,
270                          ast_sip_session_get_datastore(data->channel->session, header_datastore.type),
271                          ao2_cleanup);
272
273         if (!datastore || !datastore->data) {
274                 ast_debug(1, "There was no datastore from which to read headers.\n");
275                 return -1;
276         }
277
278         hdr = find_header((struct hdr_list *) datastore->data, data->header_name,
279                                           data->header_number);
280
281         if (!hdr) {
282                 ast_debug(1, "There was no header named %s.\n", data->header_name);
283                 return -1;
284         }
285
286         pj_hdr_string = ast_alloca(data->len);
287         pj_hdr_string_len = pjsip_hdr_print_on(hdr, pj_hdr_string, data->len);
288         pj_hdr_string[pj_hdr_string_len] = '\0';
289
290         p = strchr(pj_hdr_string, ':');
291         if (!p) {
292                 ast_log(AST_LOG_ERROR,
293                                 "A malformed header was returned from pjsip_hdr_print_on.\n");
294                 return -1;
295         }
296
297         ++p;
298         p = ast_strip(p);
299         plen = strlen(p);
300         if (plen + 1 > data->len) {
301                 ast_log(AST_LOG_ERROR,
302                                 "Buffer isn't big enough to hold header value.  %zu > %zu\n", plen + 1,
303                                 data->len);
304                 return -1;
305         }
306
307         ast_copy_string(data->buf, p, data->len);
308
309         return 0;
310 }
311
312 /*!
313  * \internal
314  * \brief Implements PJSIP_HEADER 'add' by inserting the specified header into thge list.
315  *
316  * Retrieve the header_datastore from the session or create one if it doesn't exist.
317  * Create and initialize the list if needed.
318  * Create the pj_strs for name and value.
319  * Create pjsip_msg and hdr_list_entry.
320  * Add the entry to the list.
321  */
322 static int add_header(void *obj)
323 {
324         struct header_data *data = obj;
325         struct ast_sip_session *session = data->channel->session;
326         pj_pool_t *pool = session->inv_session->dlg->pool;
327         pj_str_t pj_header_name;
328         pj_str_t pj_header_value;
329         struct hdr_list_entry *le;
330         struct hdr_list *list;
331
332         RAII_VAR(struct ast_datastore *, datastore,
333                          ast_sip_session_get_datastore(session, header_datastore.type), ao2_cleanup);
334
335         if (!datastore) {
336                 if (!(datastore = ast_sip_session_alloc_datastore(&header_datastore,
337                                                                                                                   header_datastore.type))
338                         || !(datastore->data = pj_pool_alloc(pool, sizeof(struct hdr_list)))
339                         || ast_sip_session_add_datastore(session, datastore)) {
340                         ast_log(AST_LOG_ERROR, "Unable to create datastore for header functions.\n");
341                         return -1;
342                 }
343                 AST_LIST_HEAD_INIT((struct hdr_list *) datastore->data);
344         }
345
346         ast_debug(1, "Adding header %s with value %s\n", data->header_name,
347                           data->header_value);
348
349         pj_cstr(&pj_header_name, data->header_name);
350         pj_cstr(&pj_header_value, data->header_value);
351         le = pj_pool_zalloc(pool, sizeof(struct hdr_list_entry));
352         le->hdr = (pjsip_hdr *) pjsip_generic_string_hdr_create(pool, &pj_header_name,
353                                                                                                                         &pj_header_value);
354         list = datastore->data;
355
356         AST_LIST_INSERT_TAIL(list, le, nextptr);
357
358         return 0;
359 }
360
361 /*!
362  * \internal
363  * \brief Implements PJSIP_HEADER 'update' by finding the specified header and updating it.
364  *
365  * Retrieve the header_datastore from the session or create one if it doesn't exist.
366  * Create and initialize the list if needed.
367  * Create the pj_strs for name and value.
368  * Create pjsip_msg and hdr_list_entry.
369  * Add the entry to the list.
370  */
371 static int update_header(void *obj)
372 {
373         struct header_data *data = obj;
374         pjsip_hdr *hdr = NULL;
375         RAII_VAR(struct ast_datastore *, datastore,
376                          ast_sip_session_get_datastore(data->channel->session, header_datastore.type),
377                          ao2_cleanup);
378
379         if (!datastore || !datastore->data) {
380                 ast_log(AST_LOG_ERROR, "No headers had been previously added to this session.\n");
381                 return -1;
382         }
383
384         hdr = find_header((struct hdr_list *) datastore->data, data->header_name,
385                                           data->header_number);
386
387         if (!hdr) {
388                 ast_log(AST_LOG_ERROR, "There was no header named %s.\n", data->header_name);
389                 return -1;
390         }
391
392         pj_strcpy2(&((pjsip_generic_string_hdr *) hdr)->hvalue, data->header_value);
393
394         return 0;
395 }
396
397 /*!
398  * \internal
399  * \brief Implements PJSIP_HEADER 'remove' by finding the specified header and removing it.
400  *
401  * Retrieve the header_datastore from the session.  Fail if it doesn't exist.
402  * If the header_name is exactly '*', the entire list is simply destroyed.
403  * Otherwise search the list for the matching header name which may be a partial name.
404  */
405 static int remove_header(void *obj)
406 {
407         struct header_data *data = obj;
408         size_t len = strlen(data->header_name);
409         struct hdr_list *list;
410         struct hdr_list_entry *le;
411         int removed_count = 0;
412         RAII_VAR(struct ast_datastore *, datastore,
413                          ast_sip_session_get_datastore(data->channel->session, header_datastore.type),
414                          ao2_cleanup);
415
416         if (!datastore || !datastore->data) {
417                 ast_log(AST_LOG_ERROR, "No headers had been previously added to this session.\n");
418                 return -1;
419         }
420
421         list = datastore->data;
422         AST_LIST_TRAVERSE_SAFE_BEGIN(list, le, nextptr) {
423                 if (data->header_name[len - 1] == '*') {
424                         if (pj_strnicmp2(&le->hdr->name, data->header_name, len - 1) == 0) {
425                                 AST_LIST_REMOVE_CURRENT(nextptr);
426                                 removed_count++;
427                         }
428                 } else {
429                         if (pj_stricmp2(&le->hdr->name, data->header_name) == 0) {
430                                 AST_LIST_REMOVE_CURRENT(nextptr);
431                                 removed_count++;
432                         }
433                 }
434         }
435         AST_LIST_TRAVERSE_SAFE_END;
436
437         if (data->buf && data->len) {
438                 snprintf(data->buf, data->len, "%d", removed_count);
439         }
440
441         return 0;
442 }
443
444 /*!
445  * \brief Implements function 'read' callback.
446  *
447  * Valid actions are 'read' and 'remove'.
448  */
449 static int func_read_header(struct ast_channel *chan, const char *function, char *data,
450                                                         char *buf, size_t len)
451 {
452         struct ast_sip_channel_pvt *channel = chan ? ast_channel_tech_pvt(chan) : NULL;
453         struct header_data header_data;
454         int number;
455         AST_DECLARE_APP_ARGS(args,
456                                                  AST_APP_ARG(action);
457                                                  AST_APP_ARG(header_name); AST_APP_ARG(header_number););
458         AST_STANDARD_APP_ARGS(args, data);
459
460         if (!channel || strncmp(ast_channel_name(chan), "PJSIP/", 6)) {
461                 ast_log(LOG_ERROR, "This function requires a PJSIP channel.\n");
462                 return -1;
463         }
464
465         if (ast_strlen_zero(args.action)) {
466                 ast_log(AST_LOG_ERROR, "This function requires an action.\n");
467                 return -1;
468         }
469         if (ast_strlen_zero(args.header_name)) {
470                 ast_log(AST_LOG_ERROR, "This function requires a header name.\n");
471                 return -1;
472         }
473         if (!args.header_number) {
474                 number = 1;
475         } else {
476                 sscanf(args.header_number, "%30d", &number);
477                 if (number < 1) {
478                         number = 1;
479                 }
480         }
481
482         header_data.channel = channel;
483         header_data.header_name = args.header_name;
484         header_data.header_number = number;
485         header_data.header_value = NULL;
486         header_data.buf = buf;
487         header_data.len = len;
488
489         if (stricmp(args.action, "read") == 0) {
490                 return ast_sip_push_task_synchronous(channel->session->serializer, read_header,
491                                                                                          &header_data);
492         } else if (stricmp(args.action, "remove") == 0) {
493                 return ast_sip_push_task_synchronous(channel->session->serializer, remove_header,
494                                                                                          &header_data);
495         } else {
496                 ast_log(AST_LOG_ERROR,
497                                 "Unknown action \'%s\' is not valid,  Must be \'read\' or \'remove\'.\n",
498                                 args.action);
499                 return -1;
500         }
501 }
502
503 /*!
504  * \brief Implements function 'write' callback.
505  *
506  * Valid actions are 'add', 'update' and 'remove'.
507  */
508 static int func_write_header(struct ast_channel *chan, const char *cmd, char *data,
509                                                          const char *value)
510 {
511         struct ast_sip_channel_pvt *channel = chan ? ast_channel_tech_pvt(chan) : NULL;
512         struct header_data header_data;
513         int header_number;
514         AST_DECLARE_APP_ARGS(args,
515                                                  AST_APP_ARG(action);
516                                                  AST_APP_ARG(header_name); AST_APP_ARG(header_number););
517         AST_STANDARD_APP_ARGS(args, data);
518
519         if (!channel || strncmp(ast_channel_name(chan), "PJSIP/", 6)) {
520                 ast_log(LOG_ERROR, "This function requires a PJSIP channel.\n");
521                 return -1;
522         }
523
524         if (ast_strlen_zero(args.action)) {
525                 ast_log(AST_LOG_ERROR, "This function requires an action.\n");
526                 return -1;
527         }
528         if (ast_strlen_zero(args.header_name)) {
529                 ast_log(AST_LOG_ERROR, "This function requires a header name.\n");
530                 return -1;
531         }
532         if (!args.header_number) {
533                 header_number = 1;
534         } else {
535                 sscanf(args.header_number, "%30d", &header_number);
536                 if (header_number < 1) {
537                         header_number = 1;
538                 }
539         }
540
541         header_data.channel = channel;
542         header_data.header_name = args.header_name;
543         header_data.header_number = header_number;
544         header_data.header_value = value;
545         header_data.buf = NULL;
546         header_data.len = 0;
547
548         if (stricmp(args.action, "add") == 0) {
549                 return ast_sip_push_task_synchronous(channel->session->serializer, add_header,
550                                                                                          &header_data);
551         } else if (stricmp(args.action, "update") == 0) {
552                 return ast_sip_push_task_synchronous(channel->session->serializer, update_header,
553                                                                                          &header_data);
554         } else if (stricmp(args.action, "remove") == 0) {
555                 return ast_sip_push_task_synchronous(channel->session->serializer, remove_header,
556                                                                                          &header_data);
557         } else {
558                 ast_log(AST_LOG_ERROR,
559                                 "Unknown action \'%s\' is not valid,  Must be \'add\', \'update\', or \'remove\'.\n",
560                                 args.action);
561                 return -1;
562         }
563 }
564
565 static struct ast_custom_function pjsip_header_function = {
566         .name = "PJSIP_HEADER",
567         .read = func_read_header,
568         .write = func_write_header,
569 };
570
571 /*!
572  * \internal
573  * \brief Session supplement callback for outgoing INVITE requests
574  *
575  * Retrieve the header_datastore from the session.
576  * Add each header in the list to the outgoing message.
577  *
578  * These pjsip_hdr structures will have been created by add_header.
579  * Because outgoing_request may be called more than once with the same header
580  * list (as in the case of an authentication exchange), each pjsip_hdr structure
581  * MUST be newly cloned for each outgoing message.
582  */
583 static void outgoing_request(struct ast_sip_session *session, pjsip_tx_data * tdata)
584 {
585         pj_pool_t *pool = session->inv_session->dlg->pool;
586         struct hdr_list *list;
587         struct hdr_list_entry *le;
588         RAII_VAR(struct ast_datastore *, datastore,
589                          ast_sip_session_get_datastore(session, header_datastore.type), ao2_cleanup);
590
591         if (!datastore || !datastore->data ||
592                 (session->inv_session->state >= PJSIP_INV_STATE_CONFIRMED)) {
593                 return;
594         }
595
596         list = datastore->data;
597         AST_LIST_TRAVERSE(list, le, nextptr) {
598                 pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *) pjsip_hdr_clone(pool, le->hdr));
599         }
600         ast_sip_session_remove_datastore(session, datastore->uid);
601 }
602
603 static struct ast_sip_session_supplement header_funcs_supplement = {
604         .method = "INVITE",
605         .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL - 1000,
606         .incoming_request = incoming_request,
607         .outgoing_request = outgoing_request,
608 };
609
610 static int load_module(void)
611 {
612         CHECK_PJSIP_SESSION_MODULE_LOADED();
613
614         ast_sip_session_register_supplement(&header_funcs_supplement);
615         ast_custom_function_register(&pjsip_header_function);
616
617         return AST_MODULE_LOAD_SUCCESS;
618 }
619
620 static int unload_module(void)
621 {
622         ast_custom_function_unregister(&pjsip_header_function);
623         ast_sip_session_unregister_supplement(&header_funcs_supplement);
624         return 0;
625 }
626
627 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "PJSIP Header Functions",
628         .support_level = AST_MODULE_SUPPORT_CORE,
629         .load = load_module,
630         .unload = unload_module,
631         .load_pri = AST_MODPRI_APP_DEPEND,
632 );