Merge branch 'collectd-5.8'
[collectd.git] / src / curl.c
1 /**
2  * collectd - src/curl.c
3  * Copyright (C) 2006-2009  Florian octo Forster
4  * Copyright (C) 2009       Aman Gupta
5  *
6  * This program is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by the
8  * Free Software Foundation; only version 2 of the License is applicable.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
18  *
19  * Authors:
20  *   Florian octo Forster <octo at collectd.org>
21  *   Aman Gupta <aman at tmm1.net>
22  **/
23
24 #include "collectd.h"
25
26 #include "common.h"
27 #include "plugin.h"
28 #include "utils_curl_stats.h"
29 #include "utils_match.h"
30 #include "utils_time.h"
31
32 #include <curl/curl.h>
33
34 /*
35  * Data types
36  */
37 struct web_match_s;
38 typedef struct web_match_s web_match_t;
39 struct web_match_s /* {{{ */
40 {
41   char *regex;
42   char *exclude_regex;
43   int dstype;
44   char *type;
45   char *instance;
46
47   cu_match_t *match;
48
49   web_match_t *next;
50 }; /* }}} */
51
52 struct web_page_s;
53 typedef struct web_page_s web_page_t;
54 struct web_page_s /* {{{ */
55 {
56   char *plugin_name;
57   char *instance;
58
59   char *url;
60   char *user;
61   char *pass;
62   char *credentials;
63   bool digest;
64   bool verify_peer;
65   bool verify_host;
66   char *cacert;
67   struct curl_slist *headers;
68   char *post_body;
69   bool response_time;
70   bool response_code;
71   int timeout;
72   curl_stats_t *stats;
73
74   CURL *curl;
75   char curl_errbuf[CURL_ERROR_SIZE];
76   char *buffer;
77   size_t buffer_size;
78   size_t buffer_fill;
79
80   web_match_t *matches;
81
82   web_page_t *next;
83 }; /* }}} */
84
85 /*
86  * Global variables;
87  */
88 static web_page_t *pages_g;
89
90 /*
91  * Private functions
92  */
93 static size_t cc_curl_callback(void *buf, /* {{{ */
94                                size_t size, size_t nmemb, void *user_data) {
95   web_page_t *wp;
96   size_t len;
97
98   len = size * nmemb;
99   if (len == 0)
100     return len;
101
102   wp = user_data;
103   if (wp == NULL)
104     return 0;
105
106   if ((wp->buffer_fill + len) >= wp->buffer_size) {
107     char *temp;
108     size_t temp_size;
109
110     temp_size = wp->buffer_fill + len + 1;
111     temp = realloc(wp->buffer, temp_size);
112     if (temp == NULL) {
113       ERROR("curl plugin: realloc failed.");
114       return 0;
115     }
116     wp->buffer = temp;
117     wp->buffer_size = temp_size;
118   }
119
120   memcpy(wp->buffer + wp->buffer_fill, (char *)buf, len);
121   wp->buffer_fill += len;
122   wp->buffer[wp->buffer_fill] = 0;
123
124   return len;
125 } /* }}} size_t cc_curl_callback */
126
127 static void cc_web_match_free(web_match_t *wm) /* {{{ */
128 {
129   if (wm == NULL)
130     return;
131
132   sfree(wm->regex);
133   sfree(wm->type);
134   sfree(wm->instance);
135   match_destroy(wm->match);
136   cc_web_match_free(wm->next);
137   sfree(wm);
138 } /* }}} void cc_web_match_free */
139
140 static void cc_web_page_free(web_page_t *wp) /* {{{ */
141 {
142   if (wp == NULL)
143     return;
144
145   if (wp->curl != NULL)
146     curl_easy_cleanup(wp->curl);
147   wp->curl = NULL;
148
149   sfree(wp->plugin_name);
150   sfree(wp->instance);
151
152   sfree(wp->url);
153   sfree(wp->user);
154   sfree(wp->pass);
155   sfree(wp->credentials);
156   sfree(wp->cacert);
157   sfree(wp->post_body);
158   curl_slist_free_all(wp->headers);
159   curl_stats_destroy(wp->stats);
160
161   sfree(wp->buffer);
162
163   cc_web_match_free(wp->matches);
164   cc_web_page_free(wp->next);
165   sfree(wp);
166 } /* }}} void cc_web_page_free */
167
168 static int cc_config_append_string(const char *name,
169                                    struct curl_slist **dest, /* {{{ */
170                                    oconfig_item_t *ci) {
171   struct curl_slist *temp = NULL;
172   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
173     WARNING("curl plugin: `%s' needs exactly one string argument.", name);
174     return -1;
175   }
176
177   temp = curl_slist_append(*dest, ci->values[0].value.string);
178   if (temp == NULL)
179     return -1;
180
181   *dest = temp;
182
183   return 0;
184 } /* }}} int cc_config_append_string */
185
186 static int cc_config_add_match_dstype(int *dstype_ret, /* {{{ */
187                                       oconfig_item_t *ci) {
188   int dstype;
189
190   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
191     WARNING("curl plugin: `DSType' needs exactly one string argument.");
192     return -1;
193   }
194
195   if (strncasecmp("Gauge", ci->values[0].value.string, strlen("Gauge")) == 0) {
196     dstype = UTILS_MATCH_DS_TYPE_GAUGE;
197     if (strcasecmp("GaugeAverage", ci->values[0].value.string) == 0)
198       dstype |= UTILS_MATCH_CF_GAUGE_AVERAGE;
199     else if (strcasecmp("GaugeMin", ci->values[0].value.string) == 0)
200       dstype |= UTILS_MATCH_CF_GAUGE_MIN;
201     else if (strcasecmp("GaugeMax", ci->values[0].value.string) == 0)
202       dstype |= UTILS_MATCH_CF_GAUGE_MAX;
203     else if (strcasecmp("GaugeLast", ci->values[0].value.string) == 0)
204       dstype |= UTILS_MATCH_CF_GAUGE_LAST;
205     else
206       dstype = 0;
207   } else if (strncasecmp("Counter", ci->values[0].value.string,
208                          strlen("Counter")) == 0) {
209     dstype = UTILS_MATCH_DS_TYPE_COUNTER;
210     if (strcasecmp("CounterSet", ci->values[0].value.string) == 0)
211       dstype |= UTILS_MATCH_CF_COUNTER_SET;
212     else if (strcasecmp("CounterAdd", ci->values[0].value.string) == 0)
213       dstype |= UTILS_MATCH_CF_COUNTER_ADD;
214     else if (strcasecmp("CounterInc", ci->values[0].value.string) == 0)
215       dstype |= UTILS_MATCH_CF_COUNTER_INC;
216     else
217       dstype = 0;
218   } else if (strncasecmp("Derive", ci->values[0].value.string,
219                          strlen("Derive")) == 0) {
220     dstype = UTILS_MATCH_DS_TYPE_DERIVE;
221     if (strcasecmp("DeriveSet", ci->values[0].value.string) == 0)
222       dstype |= UTILS_MATCH_CF_DERIVE_SET;
223     else if (strcasecmp("DeriveAdd", ci->values[0].value.string) == 0)
224       dstype |= UTILS_MATCH_CF_DERIVE_ADD;
225     else if (strcasecmp("DeriveInc", ci->values[0].value.string) == 0)
226       dstype |= UTILS_MATCH_CF_DERIVE_INC;
227     else
228       dstype = 0;
229   } else if (strncasecmp("Absolute", ci->values[0].value.string,
230                          strlen("Absolute")) == 0) {
231     dstype = UTILS_MATCH_DS_TYPE_ABSOLUTE;
232     if (strcasecmp("AbsoluteSet", ci->values[0].value.string) ==
233         0) /* Absolute DS is reset-on-read so no sense doin anything else but
234               set */
235       dstype |= UTILS_MATCH_CF_ABSOLUTE_SET;
236     else
237       dstype = 0;
238   }
239
240   else {
241     dstype = 0;
242   }
243
244   if (dstype == 0) {
245     WARNING("curl plugin: `%s' is not a valid argument to `DSType'.",
246             ci->values[0].value.string);
247     return -1;
248   }
249
250   *dstype_ret = dstype;
251   return 0;
252 } /* }}} int cc_config_add_match_dstype */
253
254 static int cc_config_add_match(web_page_t *page, /* {{{ */
255                                oconfig_item_t *ci) {
256   web_match_t *match;
257   int status;
258
259   if (ci->values_num != 0) {
260     WARNING("curl plugin: Ignoring arguments for the `Match' block.");
261   }
262
263   match = calloc(1, sizeof(*match));
264   if (match == NULL) {
265     ERROR("curl plugin: calloc failed.");
266     return -1;
267   }
268
269   status = 0;
270   for (int i = 0; i < ci->children_num; i++) {
271     oconfig_item_t *child = ci->children + i;
272
273     if (strcasecmp("Regex", child->key) == 0)
274       status = cf_util_get_string(child, &match->regex);
275     else if (strcasecmp("ExcludeRegex", child->key) == 0)
276       status = cf_util_get_string(child, &match->exclude_regex);
277     else if (strcasecmp("DSType", child->key) == 0)
278       status = cc_config_add_match_dstype(&match->dstype, child);
279     else if (strcasecmp("Type", child->key) == 0)
280       status = cf_util_get_string(child, &match->type);
281     else if (strcasecmp("Instance", child->key) == 0)
282       status = cf_util_get_string(child, &match->instance);
283     else {
284       WARNING("curl plugin: Option `%s' not allowed here.", child->key);
285       status = -1;
286     }
287
288     if (status != 0)
289       break;
290   } /* for (i = 0; i < ci->children_num; i++) */
291
292   while (status == 0) {
293     if (match->regex == NULL) {
294       WARNING("curl plugin: `Regex' missing in `Match' block.");
295       status = -1;
296     }
297
298     if (match->type == NULL) {
299       WARNING("curl plugin: `Type' missing in `Match' block.");
300       status = -1;
301     }
302
303     if (match->dstype == 0) {
304       WARNING("curl plugin: `DSType' missing in `Match' block.");
305       status = -1;
306     }
307
308     break;
309   } /* while (status == 0) */
310
311   if (status != 0) {
312     cc_web_match_free(match);
313     return status;
314   }
315
316   match->match =
317       match_create_simple(match->regex, match->exclude_regex, match->dstype);
318   if (match->match == NULL) {
319     ERROR("curl plugin: match_create_simple failed.");
320     cc_web_match_free(match);
321     return -1;
322   } else {
323     web_match_t *prev;
324
325     prev = page->matches;
326     while ((prev != NULL) && (prev->next != NULL))
327       prev = prev->next;
328
329     if (prev == NULL)
330       page->matches = match;
331     else
332       prev->next = match;
333   }
334
335   return 0;
336 } /* }}} int cc_config_add_match */
337
338 static int cc_page_init_curl(web_page_t *wp) /* {{{ */
339 {
340   wp->curl = curl_easy_init();
341   if (wp->curl == NULL) {
342     ERROR("curl plugin: curl_easy_init failed.");
343     return -1;
344   }
345
346   curl_easy_setopt(wp->curl, CURLOPT_NOSIGNAL, 1L);
347   curl_easy_setopt(wp->curl, CURLOPT_WRITEFUNCTION, cc_curl_callback);
348   curl_easy_setopt(wp->curl, CURLOPT_WRITEDATA, wp);
349   curl_easy_setopt(wp->curl, CURLOPT_USERAGENT, COLLECTD_USERAGENT);
350   curl_easy_setopt(wp->curl, CURLOPT_ERRORBUFFER, wp->curl_errbuf);
351   curl_easy_setopt(wp->curl, CURLOPT_FOLLOWLOCATION, 1L);
352   curl_easy_setopt(wp->curl, CURLOPT_MAXREDIRS, 50L);
353
354   if (wp->user != NULL) {
355 #ifdef HAVE_CURLOPT_USERNAME
356     curl_easy_setopt(wp->curl, CURLOPT_USERNAME, wp->user);
357     curl_easy_setopt(wp->curl, CURLOPT_PASSWORD,
358                      (wp->pass == NULL) ? "" : wp->pass);
359 #else
360     size_t credentials_size;
361
362     credentials_size = strlen(wp->user) + 2;
363     if (wp->pass != NULL)
364       credentials_size += strlen(wp->pass);
365
366     wp->credentials = malloc(credentials_size);
367     if (wp->credentials == NULL) {
368       ERROR("curl plugin: malloc failed.");
369       return -1;
370     }
371
372     snprintf(wp->credentials, credentials_size, "%s:%s", wp->user,
373              (wp->pass == NULL) ? "" : wp->pass);
374     curl_easy_setopt(wp->curl, CURLOPT_USERPWD, wp->credentials);
375 #endif
376
377     if (wp->digest)
378       curl_easy_setopt(wp->curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
379   }
380
381   curl_easy_setopt(wp->curl, CURLOPT_SSL_VERIFYPEER, (long)wp->verify_peer);
382   curl_easy_setopt(wp->curl, CURLOPT_SSL_VERIFYHOST, wp->verify_host ? 2L : 0L);
383   if (wp->cacert != NULL)
384     curl_easy_setopt(wp->curl, CURLOPT_CAINFO, wp->cacert);
385   if (wp->headers != NULL)
386     curl_easy_setopt(wp->curl, CURLOPT_HTTPHEADER, wp->headers);
387   if (wp->post_body != NULL)
388     curl_easy_setopt(wp->curl, CURLOPT_POSTFIELDS, wp->post_body);
389
390 #ifdef HAVE_CURLOPT_TIMEOUT_MS
391   if (wp->timeout >= 0)
392     curl_easy_setopt(wp->curl, CURLOPT_TIMEOUT_MS, (long)wp->timeout);
393   else
394     curl_easy_setopt(wp->curl, CURLOPT_TIMEOUT_MS,
395                      (long)CDTIME_T_TO_MS(plugin_get_interval()));
396 #endif
397
398   return 0;
399 } /* }}} int cc_page_init_curl */
400
401 static int cc_config_add_page(oconfig_item_t *ci) /* {{{ */
402 {
403   web_page_t *page;
404   int status;
405
406   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
407     WARNING("curl plugin: `Page' blocks need exactly one string argument.");
408     return -1;
409   }
410
411   page = calloc(1, sizeof(*page));
412   if (page == NULL) {
413     ERROR("curl plugin: calloc failed.");
414     return -1;
415   }
416   page->plugin_name = NULL;
417   page->url = NULL;
418   page->user = NULL;
419   page->pass = NULL;
420   page->digest = false;
421   page->verify_peer = true;
422   page->verify_host = true;
423   page->response_time = false;
424   page->response_code = false;
425   page->timeout = -1;
426   page->stats = NULL;
427
428   page->instance = strdup(ci->values[0].value.string);
429   if (page->instance == NULL) {
430     ERROR("curl plugin: strdup failed.");
431     sfree(page);
432     return -1;
433   }
434
435   /* Process all children */
436   status = 0;
437   for (int i = 0; i < ci->children_num; i++) {
438     oconfig_item_t *child = ci->children + i;
439
440     if (strcasecmp("Plugin", child->key) == 0)
441       status = cf_util_get_string(child, &page->plugin_name);
442     else if (strcasecmp("URL", child->key) == 0)
443       status = cf_util_get_string(child, &page->url);
444     else if (strcasecmp("User", child->key) == 0)
445       status = cf_util_get_string(child, &page->user);
446     else if (strcasecmp("Password", child->key) == 0)
447       status = cf_util_get_string(child, &page->pass);
448     else if (strcasecmp("Digest", child->key) == 0)
449       status = cf_util_get_boolean(child, &page->digest);
450     else if (strcasecmp("VerifyPeer", child->key) == 0)
451       status = cf_util_get_boolean(child, &page->verify_peer);
452     else if (strcasecmp("VerifyHost", child->key) == 0)
453       status = cf_util_get_boolean(child, &page->verify_host);
454     else if (strcasecmp("MeasureResponseTime", child->key) == 0)
455       status = cf_util_get_boolean(child, &page->response_time);
456     else if (strcasecmp("MeasureResponseCode", child->key) == 0)
457       status = cf_util_get_boolean(child, &page->response_code);
458     else if (strcasecmp("CACert", child->key) == 0)
459       status = cf_util_get_string(child, &page->cacert);
460     else if (strcasecmp("Match", child->key) == 0)
461       /* Be liberal with failing matches => don't set `status'. */
462       cc_config_add_match(page, child);
463     else if (strcasecmp("Header", child->key) == 0)
464       status = cc_config_append_string("Header", &page->headers, child);
465     else if (strcasecmp("Post", child->key) == 0)
466       status = cf_util_get_string(child, &page->post_body);
467     else if (strcasecmp("Timeout", child->key) == 0)
468       status = cf_util_get_int(child, &page->timeout);
469     else if (strcasecmp("Statistics", child->key) == 0) {
470       page->stats = curl_stats_from_config(child);
471       if (page->stats == NULL)
472         status = -1;
473     } else {
474       WARNING("curl plugin: Option `%s' not allowed here.", child->key);
475       status = -1;
476     }
477
478     if (status != 0)
479       break;
480   } /* for (i = 0; i < ci->children_num; i++) */
481
482   /* Additionial sanity checks and libCURL initialization. */
483   while (status == 0) {
484     if (page->url == NULL) {
485       WARNING("curl plugin: `URL' missing in `Page' block.");
486       status = -1;
487     }
488
489     if (page->matches == NULL && page->stats == NULL && !page->response_time &&
490         !page->response_code) {
491       assert(page->instance != NULL);
492       WARNING("curl plugin: No (valid) `Match' block "
493               "or Statistics or MeasureResponseTime or MeasureResponseCode "
494               "within `Page' block `%s'.",
495               page->instance);
496       status = -1;
497     }
498
499     if (status == 0)
500       status = cc_page_init_curl(page);
501
502     break;
503   } /* while (status == 0) */
504
505   if (status != 0) {
506     cc_web_page_free(page);
507     return status;
508   }
509
510   /* Add the new page to the linked list */
511   if (pages_g == NULL)
512     pages_g = page;
513   else {
514     web_page_t *prev;
515
516     prev = pages_g;
517     while (prev->next != NULL)
518       prev = prev->next;
519     prev->next = page;
520   }
521
522   return 0;
523 } /* }}} int cc_config_add_page */
524
525 static int cc_config(oconfig_item_t *ci) /* {{{ */
526 {
527   int success;
528   int errors;
529   int status;
530
531   success = 0;
532   errors = 0;
533
534   for (int i = 0; i < ci->children_num; i++) {
535     oconfig_item_t *child = ci->children + i;
536
537     if (strcasecmp("Page", child->key) == 0) {
538       status = cc_config_add_page(child);
539       if (status == 0)
540         success++;
541       else
542         errors++;
543     } else {
544       WARNING("curl plugin: Option `%s' not allowed here.", child->key);
545       errors++;
546     }
547   }
548
549   if ((success == 0) && (errors > 0)) {
550     ERROR("curl plugin: All statements failed.");
551     return -1;
552   }
553
554   return 0;
555 } /* }}} int cc_config */
556
557 static int cc_init(void) /* {{{ */
558 {
559   if (pages_g == NULL) {
560     INFO("curl plugin: No pages have been defined.");
561     return -1;
562   }
563   curl_global_init(CURL_GLOBAL_SSL);
564   return 0;
565 } /* }}} int cc_init */
566
567 static void cc_submit(const web_page_t *wp, const web_match_t *wm, /* {{{ */
568                       value_t value) {
569   value_list_t vl = VALUE_LIST_INIT;
570
571   vl.values = &value;
572   vl.values_len = 1;
573   sstrncpy(vl.plugin, (wp->plugin_name != NULL) ? wp->plugin_name : "curl",
574            sizeof(vl.plugin));
575   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
576   sstrncpy(vl.type, wm->type, sizeof(vl.type));
577   if (wm->instance != NULL)
578     sstrncpy(vl.type_instance, wm->instance, sizeof(vl.type_instance));
579
580   plugin_dispatch_values(&vl);
581 } /* }}} void cc_submit */
582
583 static void cc_submit_response_code(const web_page_t *wp, long code) /* {{{ */
584 {
585   value_list_t vl = VALUE_LIST_INIT;
586
587   vl.values = &(value_t){.gauge = (gauge_t)code};
588   vl.values_len = 1;
589   sstrncpy(vl.plugin, (wp->plugin_name != NULL) ? wp->plugin_name : "curl",
590            sizeof(vl.plugin));
591   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
592   sstrncpy(vl.type, "response_code", sizeof(vl.type));
593
594   plugin_dispatch_values(&vl);
595 } /* }}} void cc_submit_response_code */
596
597 static void cc_submit_response_time(const web_page_t *wp, /* {{{ */
598                                     gauge_t response_time) {
599   value_list_t vl = VALUE_LIST_INIT;
600
601   vl.values = &(value_t){.gauge = response_time};
602   vl.values_len = 1;
603   sstrncpy(vl.plugin, (wp->plugin_name != NULL) ? wp->plugin_name : "curl",
604            sizeof(vl.plugin));
605   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
606   sstrncpy(vl.type, "response_time", sizeof(vl.type));
607
608   plugin_dispatch_values(&vl);
609 } /* }}} void cc_submit_response_time */
610
611 static int cc_read_page(web_page_t *wp) /* {{{ */
612 {
613   int status;
614   cdtime_t start = 0;
615
616   if (wp->response_time)
617     start = cdtime();
618
619   wp->buffer_fill = 0;
620
621   curl_easy_setopt(wp->curl, CURLOPT_URL, wp->url);
622
623   status = curl_easy_perform(wp->curl);
624   if (status != CURLE_OK) {
625     ERROR("curl plugin: curl_easy_perform failed with status %i: %s", status,
626           wp->curl_errbuf);
627     return -1;
628   }
629
630   if (wp->response_time)
631     cc_submit_response_time(wp, CDTIME_T_TO_DOUBLE(cdtime() - start));
632   if (wp->stats != NULL)
633     curl_stats_dispatch(wp->stats, wp->curl, NULL, "curl", wp->instance);
634
635   if (wp->response_code) {
636     long response_code = 0;
637     status =
638         curl_easy_getinfo(wp->curl, CURLINFO_RESPONSE_CODE, &response_code);
639     if (status != CURLE_OK) {
640       ERROR("curl plugin: Fetching response code failed with status %i: %s",
641             status, wp->curl_errbuf);
642     } else {
643       cc_submit_response_code(wp, response_code);
644     }
645   }
646
647   for (web_match_t *wm = wp->matches; wm != NULL; wm = wm->next) {
648     cu_match_value_t *mv;
649
650     status = match_apply(wm->match, wp->buffer);
651     if (status != 0) {
652       WARNING("curl plugin: match_apply failed.");
653       continue;
654     }
655
656     mv = match_get_user_data(wm->match);
657     if (mv == NULL) {
658       WARNING("curl plugin: match_get_user_data returned NULL.");
659       continue;
660     }
661
662     cc_submit(wp, wm, mv->value);
663     match_value_reset(mv);
664   } /* for (wm = wp->matches; wm != NULL; wm = wm->next) */
665
666   return 0;
667 } /* }}} int cc_read_page */
668
669 static int cc_read(void) /* {{{ */
670 {
671   for (web_page_t *wp = pages_g; wp != NULL; wp = wp->next)
672     cc_read_page(wp);
673
674   return 0;
675 } /* }}} int cc_read */
676
677 static int cc_shutdown(void) /* {{{ */
678 {
679   cc_web_page_free(pages_g);
680   pages_g = NULL;
681
682   return 0;
683 } /* }}} int cc_shutdown */
684
685 void module_register(void) {
686   plugin_register_complex_config("curl", cc_config);
687   plugin_register_init("curl", cc_init);
688   plugin_register_read("curl", cc_read);
689   plugin_register_shutdown("curl", cc_shutdown);
690 } /* void module_register */