curl_json plugin: Expand unit tests.
[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_URL, wp->url);
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     ssnprintf(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->url = NULL;
417   page->user = NULL;
418   page->pass = NULL;
419   page->digest = 0;
420   page->verify_peer = 1;
421   page->verify_host = 1;
422   page->response_time = 0;
423   page->response_code = 0;
424   page->timeout = -1;
425   page->stats = NULL;
426
427   page->instance = strdup(ci->values[0].value.string);
428   if (page->instance == NULL) {
429     ERROR("curl plugin: strdup failed.");
430     sfree(page);
431     return (-1);
432   }
433
434   /* Process all children */
435   status = 0;
436   for (int i = 0; i < ci->children_num; i++) {
437     oconfig_item_t *child = ci->children + i;
438
439     if (strcasecmp("URL", child->key) == 0)
440       status = cf_util_get_string(child, &page->url);
441     else if (strcasecmp("User", child->key) == 0)
442       status = cf_util_get_string(child, &page->user);
443     else if (strcasecmp("Password", child->key) == 0)
444       status = cf_util_get_string(child, &page->pass);
445     else if (strcasecmp("Digest", child->key) == 0)
446       status = cf_util_get_boolean(child, &page->digest);
447     else if (strcasecmp("VerifyPeer", child->key) == 0)
448       status = cf_util_get_boolean(child, &page->verify_peer);
449     else if (strcasecmp("VerifyHost", child->key) == 0)
450       status = cf_util_get_boolean(child, &page->verify_host);
451     else if (strcasecmp("MeasureResponseTime", child->key) == 0)
452       status = cf_util_get_boolean(child, &page->response_time);
453     else if (strcasecmp("MeasureResponseCode", child->key) == 0)
454       status = cf_util_get_boolean(child, &page->response_code);
455     else if (strcasecmp("CACert", child->key) == 0)
456       status = cf_util_get_string(child, &page->cacert);
457     else if (strcasecmp("Match", child->key) == 0)
458       /* Be liberal with failing matches => don't set `status'. */
459       cc_config_add_match(page, child);
460     else if (strcasecmp("Header", child->key) == 0)
461       status = cc_config_append_string("Header", &page->headers, child);
462     else if (strcasecmp("Post", child->key) == 0)
463       status = cf_util_get_string(child, &page->post_body);
464     else if (strcasecmp("Timeout", child->key) == 0)
465       status = cf_util_get_int(child, &page->timeout);
466     else if (strcasecmp("Statistics", child->key) == 0) {
467       page->stats = curl_stats_from_config(child);
468       if (page->stats == NULL)
469         status = -1;
470     } else {
471       WARNING("curl plugin: Option `%s' not allowed here.", child->key);
472       status = -1;
473     }
474
475     if (status != 0)
476       break;
477   } /* for (i = 0; i < ci->children_num; i++) */
478
479   /* Additionial sanity checks and libCURL initialization. */
480   while (status == 0) {
481     if (page->url == NULL) {
482       WARNING("curl plugin: `URL' missing in `Page' block.");
483       status = -1;
484     }
485
486     if (page->matches == NULL && page->stats == NULL && !page->response_time &&
487         !page->response_code) {
488       assert(page->instance != NULL);
489       WARNING("curl plugin: No (valid) `Match' block "
490               "or Statistics or MeasureResponseTime or MeasureResponseCode "
491               "within `Page' block `%s'.",
492               page->instance);
493       status = -1;
494     }
495
496     if (status == 0)
497       status = cc_page_init_curl(page);
498
499     break;
500   } /* while (status == 0) */
501
502   if (status != 0) {
503     cc_web_page_free(page);
504     return (status);
505   }
506
507   /* Add the new page to the linked list */
508   if (pages_g == NULL)
509     pages_g = page;
510   else {
511     web_page_t *prev;
512
513     prev = pages_g;
514     while (prev->next != NULL)
515       prev = prev->next;
516     prev->next = page;
517   }
518
519   return (0);
520 } /* }}} int cc_config_add_page */
521
522 static int cc_config(oconfig_item_t *ci) /* {{{ */
523 {
524   int success;
525   int errors;
526   int status;
527
528   success = 0;
529   errors = 0;
530
531   for (int i = 0; i < ci->children_num; i++) {
532     oconfig_item_t *child = ci->children + i;
533
534     if (strcasecmp("Page", child->key) == 0) {
535       status = cc_config_add_page(child);
536       if (status == 0)
537         success++;
538       else
539         errors++;
540     } else {
541       WARNING("curl plugin: Option `%s' not allowed here.", child->key);
542       errors++;
543     }
544   }
545
546   if ((success == 0) && (errors > 0)) {
547     ERROR("curl plugin: All statements failed.");
548     return (-1);
549   }
550
551   return (0);
552 } /* }}} int cc_config */
553
554 static int cc_init(void) /* {{{ */
555 {
556   if (pages_g == NULL) {
557     INFO("curl plugin: No pages have been defined.");
558     return (-1);
559   }
560   curl_global_init(CURL_GLOBAL_SSL);
561   return (0);
562 } /* }}} int cc_init */
563
564 static void cc_submit(const web_page_t *wp, const web_match_t *wm, /* {{{ */
565                       value_t value) {
566   value_list_t vl = VALUE_LIST_INIT;
567
568   vl.values = &value;
569   vl.values_len = 1;
570   sstrncpy(vl.plugin, "curl", sizeof(vl.plugin));
571   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
572   sstrncpy(vl.type, wm->type, sizeof(vl.type));
573   if (wm->instance != NULL)
574     sstrncpy(vl.type_instance, wm->instance, sizeof(vl.type_instance));
575
576   plugin_dispatch_values(&vl);
577 } /* }}} void cc_submit */
578
579 static void cc_submit_response_code(const web_page_t *wp, long code) /* {{{ */
580 {
581   value_list_t vl = VALUE_LIST_INIT;
582
583   vl.values = &(value_t){.gauge = (gauge_t)code};
584   vl.values_len = 1;
585   sstrncpy(vl.plugin, "curl", sizeof(vl.plugin));
586   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
587   sstrncpy(vl.type, "response_code", sizeof(vl.type));
588
589   plugin_dispatch_values(&vl);
590 } /* }}} void cc_submit_response_code */
591
592 static void cc_submit_response_time(const web_page_t *wp, /* {{{ */
593                                     gauge_t response_time) {
594   value_list_t vl = VALUE_LIST_INIT;
595
596   vl.values = &(value_t){.gauge = response_time};
597   vl.values_len = 1;
598   sstrncpy(vl.plugin, "curl", sizeof(vl.plugin));
599   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
600   sstrncpy(vl.type, "response_time", sizeof(vl.type));
601
602   plugin_dispatch_values(&vl);
603 } /* }}} void cc_submit_response_time */
604
605 static int cc_read_page(web_page_t *wp) /* {{{ */
606 {
607   int status;
608   cdtime_t start = 0;
609
610   if (wp->response_time)
611     start = cdtime();
612
613   wp->buffer_fill = 0;
614   status = curl_easy_perform(wp->curl);
615   if (status != CURLE_OK) {
616     ERROR("curl plugin: curl_easy_perform failed with status %i: %s", status,
617           wp->curl_errbuf);
618     return (-1);
619   }
620
621   if (wp->response_time)
622     cc_submit_response_time(wp, CDTIME_T_TO_DOUBLE(cdtime() - start));
623   if (wp->stats != NULL)
624     curl_stats_dispatch(wp->stats, wp->curl, hostname_g, "curl", wp->instance);
625
626   if (wp->response_code) {
627     long response_code = 0;
628     status =
629         curl_easy_getinfo(wp->curl, CURLINFO_RESPONSE_CODE, &response_code);
630     if (status != CURLE_OK) {
631       ERROR("curl plugin: Fetching response code failed with status %i: %s",
632             status, wp->curl_errbuf);
633     } else {
634       cc_submit_response_code(wp, response_code);
635     }
636   }
637
638   for (web_match_t *wm = wp->matches; wm != NULL; wm = wm->next) {
639     cu_match_value_t *mv;
640
641     status = match_apply(wm->match, wp->buffer);
642     if (status != 0) {
643       WARNING("curl plugin: match_apply failed.");
644       continue;
645     }
646
647     mv = match_get_user_data(wm->match);
648     if (mv == NULL) {
649       WARNING("curl plugin: match_get_user_data returned NULL.");
650       continue;
651     }
652
653     cc_submit(wp, wm, mv->value);
654     match_value_reset(mv);
655   } /* for (wm = wp->matches; wm != NULL; wm = wm->next) */
656
657   return (0);
658 } /* }}} int cc_read_page */
659
660 static int cc_read(void) /* {{{ */
661 {
662   for (web_page_t *wp = pages_g; wp != NULL; wp = wp->next)
663     cc_read_page(wp);
664
665   return (0);
666 } /* }}} int cc_read */
667
668 static int cc_shutdown(void) /* {{{ */
669 {
670   cc_web_page_free(pages_g);
671   pages_g = NULL;
672
673   return (0);
674 } /* }}} int cc_shutdown */
675
676 void module_register(void) {
677   plugin_register_complex_config("curl", cc_config);
678   plugin_register_init("curl", cc_init);
679   plugin_register_read("curl", cc_read);
680   plugin_register_shutdown("curl", cc_shutdown);
681 } /* void module_register */