ami_testhooks.c automatically registers hook
[asterisk/asterisk.git] / funcs / func_curl.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C)  2004 - 2006, Tilghman Lesher
5  *
6  * Tilghman Lesher <curl-20050919@the-tilghman.com>
7  * and Brian Wilkins <bwilkins@cfl.rr.com> (Added POST option)
8  *
9  * app_curl.c is distributed with no restrictions on usage or
10  * redistribution.
11  *
12  * See http://www.asterisk.org for more information about
13  * the Asterisk project. Please do not directly contact
14  * any of the maintainers of this project for assistance;
15  * the project provides a web site, mailing lists and IRC
16  * channels for your use.
17  *
18  */
19
20 /*! \file
21  * 
22  * \brief Curl - Load a URL
23  *
24  * \author Tilghman Lesher <curl-20050919@the-tilghman.com>
25  *
26  * \note Brian Wilkins <bwilkins@cfl.rr.com> (Added POST option) 
27  *
28  * \extref Depends on the CURL library  - http://curl.haxx.se/
29  * 
30  * \ingroup functions
31  */
32  
33 /*** MODULEINFO
34         <depend>curl</depend>
35  ***/
36
37 #include "asterisk.h"
38
39 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
40
41 #include <curl/curl.h>
42
43 #include "asterisk/lock.h"
44 #include "asterisk/file.h"
45 #include "asterisk/channel.h"
46 #include "asterisk/pbx.h"
47 #include "asterisk/cli.h"
48 #include "asterisk/module.h"
49 #include "asterisk/app.h"
50 #include "asterisk/utils.h"
51 #include "asterisk/threadstorage.h"
52
53 #define CURLVERSION_ATLEAST(a,b,c) \
54         ((LIBCURL_VERSION_MAJOR > (a)) || ((LIBCURL_VERSION_MAJOR == (a)) && (LIBCURL_VERSION_MINOR > (b))) || ((LIBCURL_VERSION_MAJOR == (a)) && (LIBCURL_VERSION_MINOR == (b)) && (LIBCURL_VERSION_PATCH >= (c))))
55
56 #define CURLOPT_SPECIAL_HASHCOMPAT -500
57
58 static void curlds_free(void *data);
59
60 static struct ast_datastore_info curl_info = {
61         .type = "CURL",
62         .destroy = curlds_free,
63 };
64
65 struct curl_settings {
66         AST_LIST_ENTRY(curl_settings) list;
67         CURLoption key;
68         void *value;
69 };
70
71 AST_LIST_HEAD_STATIC(global_curl_info, curl_settings);
72
73 static void curlds_free(void *data)
74 {
75         AST_LIST_HEAD(global_curl_info, curl_settings) *list = data;
76         struct curl_settings *setting;
77         if (!list) {
78                 return;
79         }
80         while ((setting = AST_LIST_REMOVE_HEAD(list, list))) {
81                 free(setting);
82         }
83         AST_LIST_HEAD_DESTROY(list);
84 }
85
86 enum optiontype {
87         OT_BOOLEAN,
88         OT_INTEGER,
89         OT_INTEGER_MS,
90         OT_STRING,
91         OT_ENUM,
92 };
93
94 static int parse_curlopt_key(const char *name, CURLoption *key, enum optiontype *ot)
95 {
96         if (!strcasecmp(name, "header")) {
97                 *key = CURLOPT_HEADER;
98                 *ot = OT_BOOLEAN;
99         } else if (!strcasecmp(name, "proxy")) {
100                 *key = CURLOPT_PROXY;
101                 *ot = OT_STRING;
102         } else if (!strcasecmp(name, "proxyport")) {
103                 *key = CURLOPT_PROXYPORT;
104                 *ot = OT_INTEGER;
105         } else if (!strcasecmp(name, "proxytype")) {
106                 *key = CURLOPT_PROXYTYPE;
107                 *ot = OT_ENUM;
108         } else if (!strcasecmp(name, "dnstimeout")) {
109                 *key = CURLOPT_DNS_CACHE_TIMEOUT;
110                 *ot = OT_INTEGER;
111         } else if (!strcasecmp(name, "userpwd")) {
112                 *key = CURLOPT_USERPWD;
113                 *ot = OT_STRING;
114         } else if (!strcasecmp(name, "proxyuserpwd")) {
115                 *key = CURLOPT_PROXYUSERPWD;
116                 *ot = OT_STRING;
117         } else if (!strcasecmp(name, "maxredirs")) {
118                 *key = CURLOPT_MAXREDIRS;
119                 *ot = OT_INTEGER;
120         } else if (!strcasecmp(name, "referer")) {
121                 *key = CURLOPT_REFERER;
122                 *ot = OT_STRING;
123         } else if (!strcasecmp(name, "useragent")) {
124                 *key = CURLOPT_USERAGENT;
125                 *ot = OT_STRING;
126         } else if (!strcasecmp(name, "cookie")) {
127                 *key = CURLOPT_COOKIE;
128                 *ot = OT_STRING;
129         } else if (!strcasecmp(name, "ftptimeout")) {
130                 *key = CURLOPT_FTP_RESPONSE_TIMEOUT;
131                 *ot = OT_INTEGER;
132         } else if (!strcasecmp(name, "httptimeout")) {
133 #if CURLVERSION_ATLEAST(7,16,2)
134                 *key = CURLOPT_TIMEOUT_MS;
135                 *ot = OT_INTEGER_MS;
136 #else
137                 *key = CURLOPT_TIMEOUT;
138                 *ot = OT_INTEGER;
139 #endif
140         } else if (!strcasecmp(name, "conntimeout")) {
141 #if CURLVERSION_ATLEAST(7,16,2)
142                 *key = CURLOPT_CONNECTTIMEOUT_MS;
143                 *ot = OT_INTEGER_MS;
144 #else
145                 *key = CURLOPT_CONNECTTIMEOUT;
146                 *ot = OT_INTEGER;
147 #endif
148         } else if (!strcasecmp(name, "ftptext")) {
149                 *key = CURLOPT_TRANSFERTEXT;
150                 *ot = OT_BOOLEAN;
151         } else if (!strcasecmp(name, "ssl_verifypeer")) {
152                 *key = CURLOPT_SSL_VERIFYPEER;
153                 *ot = OT_BOOLEAN;
154         } else if (!strcasecmp(name, "hashcompat")) {
155                 *key = CURLOPT_SPECIAL_HASHCOMPAT;
156                 *ot = OT_BOOLEAN;
157         } else {
158                 return -1;
159         }
160         return 0;
161 }
162
163 static int acf_curlopt_write(struct ast_channel *chan, const char *cmd, char *name, const char *value)
164 {
165         struct ast_datastore *store;
166         struct global_curl_info *list;
167         struct curl_settings *cur, *new = NULL;
168         CURLoption key;
169         enum optiontype ot;
170
171         if (chan) {
172                 if (!(store = ast_channel_datastore_find(chan, &curl_info, NULL))) {
173                         /* Create a new datastore */
174                         if (!(store = ast_datastore_alloc(&curl_info, NULL))) {
175                                 ast_log(LOG_ERROR, "Unable to allocate new datastore.  Cannot set any CURL options\n");
176                                 return -1;
177                         }
178
179                         if (!(list = ast_calloc(1, sizeof(*list)))) {
180                                 ast_log(LOG_ERROR, "Unable to allocate list head.  Cannot set any CURL options\n");
181                                 ast_datastore_free(store);
182                         }
183
184                         store->data = list;
185                         AST_LIST_HEAD_INIT(list);
186                         ast_channel_datastore_add(chan, store);
187                 } else {
188                         list = store->data;
189                 }
190         } else {
191                 /* Populate the global structure */
192                 list = &global_curl_info;
193         }
194
195         if (!parse_curlopt_key(name, &key, &ot)) {
196                 if (ot == OT_BOOLEAN) {
197                         if ((new = ast_calloc(1, sizeof(*new)))) {
198                                 new->value = (void *)((long) ast_true(value));
199                         }
200                 } else if (ot == OT_INTEGER) {
201                         long tmp = atol(value);
202                         if ((new = ast_calloc(1, sizeof(*new)))) {
203                                 new->value = (void *)tmp;
204                         }
205                 } else if (ot == OT_INTEGER_MS) {
206                         long tmp = atof(value) * 1000.0;
207                         if ((new = ast_calloc(1, sizeof(*new)))) {
208                                 new->value = (void *)tmp;
209                         }
210                 } else if (ot == OT_STRING) {
211                         if ((new = ast_calloc(1, sizeof(*new) + strlen(value) + 1))) {
212                                 new->value = (char *)new + sizeof(*new);
213                                 strcpy(new->value, value);
214                         }
215                 } else if (ot == OT_ENUM) {
216                         if (key == CURLOPT_PROXYTYPE) {
217                                 long ptype =
218 #if CURLVERSION_ATLEAST(7,10,0)
219                                         CURLPROXY_HTTP;
220 #else
221                                         CURLPROXY_SOCKS5;
222 #endif
223                                 if (0) {
224 #if CURLVERSION_ATLEAST(7,15,2)
225                                 } else if (!strcasecmp(value, "socks4")) {
226                                         ptype = CURLPROXY_SOCKS4;
227 #endif
228 #if CURLVERSION_ATLEAST(7,18,0)
229                                 } else if (!strcasecmp(value, "socks4a")) {
230                                         ptype = CURLPROXY_SOCKS4A;
231 #endif
232 #if CURLVERSION_ATLEAST(7,18,0)
233                                 } else if (!strcasecmp(value, "socks5")) {
234                                         ptype = CURLPROXY_SOCKS5;
235 #endif
236 #if CURLVERSION_ATLEAST(7,18,0)
237                                 } else if (!strncasecmp(value, "socks5", 6)) {
238                                         ptype = CURLPROXY_SOCKS5_HOSTNAME;
239 #endif
240                                 }
241
242                                 if ((new = ast_calloc(1, sizeof(*new)))) {
243                                         new->value = (void *)ptype;
244                                 }
245                         } else {
246                                 /* Highly unlikely */
247                                 goto yuck;
248                         }
249                 }
250
251                 /* Memory allocation error */
252                 if (!new) {
253                         return -1;
254                 }
255
256                 new->key = key;
257         } else {
258 yuck:
259                 ast_log(LOG_ERROR, "Unrecognized option: %s\n", name);
260                 return -1;
261         }
262
263         /* Remove any existing entry */
264         AST_LIST_LOCK(list);
265         AST_LIST_TRAVERSE_SAFE_BEGIN(list, cur, list) {
266                 if (cur->key == new->key) {
267                         AST_LIST_REMOVE_CURRENT(list);
268                         free(cur);
269                         break;
270                 }
271         }
272         AST_LIST_TRAVERSE_SAFE_END
273
274         /* Insert new entry */
275         ast_debug(1, "Inserting entry %p with key %d and value %p\n", new, new->key, new->value);
276         AST_LIST_INSERT_TAIL(list, new, list);
277         AST_LIST_UNLOCK(list);
278
279         return 0;
280 }
281
282 static int acf_curlopt_helper(struct ast_channel *chan, const char *cmd, char *data, char *buf, struct ast_str **bufstr, ssize_t len)
283 {
284         struct ast_datastore *store;
285         struct global_curl_info *list[2] = { &global_curl_info, NULL };
286         struct curl_settings *cur = NULL;
287         CURLoption key;
288         enum optiontype ot;
289         int i;
290
291         if (parse_curlopt_key(data, &key, &ot)) {
292                 ast_log(LOG_ERROR, "Unrecognized option: '%s'\n", data);
293                 return -1;
294         }
295
296         if (chan && (store = ast_channel_datastore_find(chan, &curl_info, NULL))) {
297                 list[0] = store->data;
298                 list[1] = &global_curl_info;
299         }
300
301         for (i = 0; i < 2; i++) {
302                 if (!list[i]) {
303                         break;
304                 }
305                 AST_LIST_LOCK(list[i]);
306                 AST_LIST_TRAVERSE(list[i], cur, list) {
307                         if (cur->key == key) {
308                                 if (ot == OT_BOOLEAN || ot == OT_INTEGER) {
309                                         if (buf) {
310                                                 snprintf(buf, len, "%ld", (long) cur->value);
311                                         } else {
312                                                 ast_str_set(bufstr, len, "%ld", (long) cur->value);
313                                         }
314                                 } else if (ot == OT_INTEGER_MS) {
315                                         if ((long) cur->value % 1000 == 0) {
316                                                 if (buf) {
317                                                         snprintf(buf, len, "%ld", (long)cur->value / 1000);
318                                                 } else {
319                                                         ast_str_set(bufstr, len, "%ld", (long) cur->value / 1000);
320                                                 }
321                                         } else {
322                                                 if (buf) {
323                                                         snprintf(buf, len, "%.3f", (double) ((long) cur->value) / 1000.0);
324                                                 } else {
325                                                         ast_str_set(bufstr, len, "%.3f", (double) ((long) cur->value) / 1000.0);
326                                                 }
327                                         }
328                                 } else if (ot == OT_STRING) {
329                                         ast_debug(1, "Found entry %p, with key %d and value %p\n", cur, cur->key, cur->value);
330                                         if (buf) {
331                                                 ast_copy_string(buf, cur->value, len);
332                                         } else {
333                                                 ast_str_set(bufstr, 0, "%s", (char *) cur->value);
334                                         }
335                                 } else if (key == CURLOPT_PROXYTYPE) {
336                                         if (0) {
337 #if CURLVERSION_ATLEAST(7,15,2)
338                                         } else if ((long)cur->value == CURLPROXY_SOCKS4) {
339                                                 if (buf) {
340                                                         ast_copy_string(buf, "socks4", len);
341                                                 } else {
342                                                         ast_str_set(bufstr, 0, "socks4");
343                                                 }
344 #endif
345 #if CURLVERSION_ATLEAST(7,18,0)
346                                         } else if ((long)cur->value == CURLPROXY_SOCKS4A) {
347                                                 if (buf) {
348                                                         ast_copy_string(buf, "socks4a", len);
349                                                 } else {
350                                                         ast_str_set(bufstr, 0, "socks4a");
351                                                 }
352 #endif
353                                         } else if ((long)cur->value == CURLPROXY_SOCKS5) {
354                                                 if (buf) {
355                                                         ast_copy_string(buf, "socks5", len);
356                                                 } else {
357                                                         ast_str_set(bufstr, 0, "socks5");
358                                                 }
359 #if CURLVERSION_ATLEAST(7,18,0)
360                                         } else if ((long)cur->value == CURLPROXY_SOCKS5_HOSTNAME) {
361                                                 if (buf) {
362                                                         ast_copy_string(buf, "socks5hostname", len);
363                                                 } else {
364                                                         ast_str_set(bufstr, 0, "socks5hostname");
365                                                 }
366 #endif
367 #if CURLVERSION_ATLEAST(7,10,0)
368                                         } else if ((long)cur->value == CURLPROXY_HTTP) {
369                                                 if (buf) {
370                                                         ast_copy_string(buf, "http", len);
371                                                 } else {
372                                                         ast_str_set(bufstr, 0, "http");
373                                                 }
374 #endif
375                                         } else {
376                                                 if (buf) {
377                                                         ast_copy_string(buf, "unknown", len);
378                                                 } else {
379                                                         ast_str_set(bufstr, 0, "unknown");
380                                                 }
381                                         }
382                                 }
383                                 break;
384                         }
385                 }
386                 AST_LIST_UNLOCK(list[i]);
387                 if (cur) {
388                         break;
389                 }
390         }
391
392         return cur ? 0 : -1;
393 }
394
395 static int acf_curlopt_read(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len)
396 {
397         return acf_curlopt_helper(chan, cmd, data, buf, NULL, len);
398 }
399
400 static int acf_curlopt_read2(struct ast_channel *chan, const char *cmd, char *data, struct ast_str **buf, ssize_t len)
401 {
402         return acf_curlopt_helper(chan, cmd, data, NULL, buf, len);
403 }
404
405 static size_t WriteMemoryCallback(void *ptr, size_t size, size_t nmemb, void *data)
406 {
407         register int realsize = size * nmemb;
408         struct ast_str **pstr = (struct ast_str **)data;
409
410         ast_debug(3, "Called with data=%p, str=%p, realsize=%d, len=%zu, used=%zu\n", data, *pstr, realsize, ast_str_size(*pstr), ast_str_strlen(*pstr));
411
412         ast_str_append_substr(pstr, 0, ptr, realsize);
413
414         ast_debug(3, "Now, len=%zu, used=%zu\n", ast_str_size(*pstr), ast_str_strlen(*pstr));
415
416         return realsize;
417 }
418
419 static const char * const global_useragent = "asterisk-libcurl-agent/1.0";
420
421 static int curl_instance_init(void *data)
422 {
423         CURL **curl = data;
424
425         if (!(*curl = curl_easy_init()))
426                 return -1;
427
428         curl_easy_setopt(*curl, CURLOPT_NOSIGNAL, 1);
429         curl_easy_setopt(*curl, CURLOPT_TIMEOUT, 180);
430         curl_easy_setopt(*curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
431         curl_easy_setopt(*curl, CURLOPT_USERAGENT, global_useragent);
432
433         return 0;
434 }
435
436 static void curl_instance_cleanup(void *data)
437 {
438         CURL **curl = data;
439
440         curl_easy_cleanup(*curl);
441
442         ast_free(data);
443 }
444
445 AST_THREADSTORAGE_CUSTOM(curl_instance, curl_instance_init, curl_instance_cleanup);
446
447 static int acf_curl_helper(struct ast_channel *chan, const char *cmd, char *info, char *buf, struct ast_str **input_str, ssize_t len)
448 {
449         struct ast_str *str = ast_str_create(16);
450         int ret = -1;
451         AST_DECLARE_APP_ARGS(args,
452                 AST_APP_ARG(url);
453                 AST_APP_ARG(postdata);
454         );
455         CURL **curl;
456         struct curl_settings *cur;
457         struct ast_datastore *store = NULL;
458         int hashcompat = 0;
459         AST_LIST_HEAD(global_curl_info, curl_settings) *list = NULL;
460
461         if (buf) {
462                 *buf = '\0';
463         }
464
465         if (ast_strlen_zero(info)) {
466                 ast_log(LOG_WARNING, "CURL requires an argument (URL)\n");
467                 ast_free(str);
468                 return -1;
469         }
470
471         AST_STANDARD_APP_ARGS(args, info);
472
473         if (chan) {
474                 ast_autoservice_start(chan);
475         }
476
477         if (!(curl = ast_threadstorage_get(&curl_instance, sizeof(*curl)))) {
478                 ast_log(LOG_ERROR, "Cannot allocate curl structure\n");
479                 return -1;
480         }
481
482         AST_LIST_LOCK(&global_curl_info);
483         AST_LIST_TRAVERSE(&global_curl_info, cur, list) {
484                 if (cur->key == CURLOPT_SPECIAL_HASHCOMPAT) {
485                         hashcompat = (cur->value != NULL) ? 1 : 0;
486                 } else {
487                         curl_easy_setopt(*curl, cur->key, cur->value);
488                 }
489         }
490
491         if (chan && (store = ast_channel_datastore_find(chan, &curl_info, NULL))) {
492                 list = store->data;
493                 AST_LIST_LOCK(list);
494                 AST_LIST_TRAVERSE(list, cur, list) {
495                         if (cur->key == CURLOPT_SPECIAL_HASHCOMPAT) {
496                                 hashcompat = (cur->value != NULL) ? 1 : 0;
497                         } else {
498                                 curl_easy_setopt(*curl, cur->key, cur->value);
499                         }
500                 }
501         }
502
503         curl_easy_setopt(*curl, CURLOPT_URL, args.url);
504         curl_easy_setopt(*curl, CURLOPT_FILE, (void *) &str);
505
506         if (args.postdata) {
507                 curl_easy_setopt(*curl, CURLOPT_POST, 1);
508                 curl_easy_setopt(*curl, CURLOPT_POSTFIELDS, args.postdata);
509         }
510
511         curl_easy_perform(*curl);
512
513         if (store) {
514                 AST_LIST_UNLOCK(list);
515         }
516         AST_LIST_UNLOCK(&global_curl_info);
517
518         if (args.postdata) {
519                 curl_easy_setopt(*curl, CURLOPT_POST, 0);
520         }
521
522         if (ast_str_strlen(str)) {
523                 ast_str_trim_blanks(str);
524
525                 ast_debug(3, "str='%s'\n", ast_str_buffer(str));
526                 if (hashcompat) {
527                         char *remainder = ast_str_buffer(str);
528                         char *piece;
529                         struct ast_str *fields = ast_str_create(ast_str_strlen(str) / 2);
530                         struct ast_str *values = ast_str_create(ast_str_strlen(str) / 2);
531                         int rowcount = 0;
532                         while ((piece = strsep(&remainder, "&"))) {
533                                 char *name = strsep(&piece, "=");
534                                 ast_uri_decode(piece);
535                                 ast_uri_decode(name);
536                                 ast_str_append(&fields, 0, "%s%s", rowcount ? "," : "", name);
537                                 ast_str_append(&values, 0, "%s%s", rowcount ? "," : "", piece);
538                                 rowcount++;
539                         }
540                         pbx_builtin_setvar_helper(chan, "~ODBCFIELDS~", ast_str_buffer(fields));
541                         if (buf) {
542                                 ast_copy_string(buf, ast_str_buffer(values), len);
543                         } else {
544                                 ast_str_set(input_str, len, "%s", ast_str_buffer(values));
545                         }
546                         ast_free(fields);
547                         ast_free(values);
548                 } else {
549                         if (buf) {
550                                 ast_copy_string(buf, ast_str_buffer(str), len);
551                         } else {
552                                 ast_str_set(input_str, len, "%s", ast_str_buffer(str));
553                         }
554                 }
555                 ret = 0;
556         }
557         ast_free(str);
558
559         if (chan)
560                 ast_autoservice_stop(chan);
561
562         return ret;
563 }
564
565 static int acf_curl_exec(struct ast_channel *chan, const char *cmd, char *info, char *buf, size_t len)
566 {
567         return acf_curl_helper(chan, cmd, info, buf, NULL, len);
568 }
569
570 static int acf_curl2_exec(struct ast_channel *chan, const char *cmd, char *info, struct ast_str **buf, ssize_t len)
571 {
572         return acf_curl_helper(chan, cmd, info, NULL, buf, len);
573 }
574
575 static struct ast_custom_function acf_curl = {
576         .name = "CURL",
577         .synopsis = "Retrieves the contents of a URL",
578         .syntax = "CURL(url[,post-data])",
579         .desc =
580         "  url       - URL to retrieve\n"
581         "  post-data - Optional data to send as a POST (GET is default action)\n",
582         .read = acf_curl_exec,
583         .read2 = acf_curl2_exec,
584 };
585
586 static struct ast_custom_function acf_curlopt = {
587         .name = "CURLOPT",
588         .synopsis = "Set options for use with the CURL() function",
589         .syntax = "CURLOPT(<option>)",
590         .desc =
591 "  cookie         - Send cookie with request [none]\n"
592 "  conntimeout    - Number of seconds to wait for connection\n"
593 "  dnstimeout     - Number of seconds to wait for DNS response\n"
594 "  ftptext        - For FTP, force a text transfer (boolean)\n"
595 "  ftptimeout     - For FTP, the server response timeout\n"
596 "  header         - Retrieve header information (boolean)\n"
597 "  httptimeout    - Number of seconds to wait for HTTP response\n"
598 "  maxredirs      - Maximum number of redirects to follow\n"
599 "  proxy          - Hostname or IP to use as a proxy\n"
600 "  proxytype      - http, socks4, or socks5\n"
601 "  proxyport      - port number of the proxy\n"
602 "  proxyuserpwd   - A <user>:<pass> to use for authentication\n"
603 "  referer        - Referer URL to use for the request\n"
604 "  useragent      - UserAgent string to use\n"
605 "  userpwd        - A <user>:<pass> to use for authentication\n"
606 "  ssl_verifypeer - Whether to verify the peer certificate (boolean)\n"
607 "  hashcompat     - Result data will be compatible for use with HASH()\n"
608 "",
609         .read = acf_curlopt_read,
610         .read2 = acf_curlopt_read2,
611         .write = acf_curlopt_write,
612 };
613
614 static int unload_module(void)
615 {
616         int res;
617
618         res = ast_custom_function_unregister(&acf_curl);
619         res |= ast_custom_function_unregister(&acf_curlopt);
620
621         return res;
622 }
623
624 static int load_module(void)
625 {
626         int res;
627
628         if (!ast_module_check("res_curl.so")) {
629                 if (ast_load_resource("res_curl.so") != AST_MODULE_LOAD_SUCCESS) {
630                         ast_log(LOG_ERROR, "Cannot load res_curl, so func_curl cannot be loaded\n");
631                         return AST_MODULE_LOAD_DECLINE;
632                 }
633         }
634
635         res = ast_custom_function_register(&acf_curl);
636         res |= ast_custom_function_register(&acf_curlopt);
637
638         return res;
639 }
640
641 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Load external URL");
642