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