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