write_stackdriver plugin: Centralize HTTP calls, add API error handling.
[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
83 /*
84  * Private functions
85  */
86 static int cc_read_page(user_data_t *ud);
87
88 static size_t cc_curl_callback(void *buf, /* {{{ */
89                                size_t size, size_t nmemb, void *user_data) {
90   web_page_t *wp;
91   size_t len;
92
93   len = size * nmemb;
94   if (len == 0)
95     return len;
96
97   wp = user_data;
98   if (wp == NULL)
99     return 0;
100
101   if ((wp->buffer_fill + len) >= wp->buffer_size) {
102     char *temp;
103     size_t temp_size;
104
105     temp_size = wp->buffer_fill + len + 1;
106     temp = realloc(wp->buffer, temp_size);
107     if (temp == NULL) {
108       ERROR("curl plugin: realloc failed.");
109       return 0;
110     }
111     wp->buffer = temp;
112     wp->buffer_size = temp_size;
113   }
114
115   memcpy(wp->buffer + wp->buffer_fill, (char *)buf, len);
116   wp->buffer_fill += len;
117   wp->buffer[wp->buffer_fill] = 0;
118
119   return len;
120 } /* }}} size_t cc_curl_callback */
121
122 static void cc_web_match_free(web_match_t *wm) /* {{{ */
123 {
124   if (wm == NULL)
125     return;
126
127   sfree(wm->regex);
128   sfree(wm->type);
129   sfree(wm->instance);
130   match_destroy(wm->match);
131   cc_web_match_free(wm->next);
132   sfree(wm);
133 } /* }}} void cc_web_match_free */
134
135 static void cc_web_page_free(void *arg) /* {{{ */
136 {
137   web_page_t *wp = (web_page_t *)arg;
138   if (wp == NULL)
139     return;
140
141   if (wp->curl != NULL)
142     curl_easy_cleanup(wp->curl);
143   wp->curl = NULL;
144
145   sfree(wp->plugin_name);
146   sfree(wp->instance);
147
148   sfree(wp->url);
149   sfree(wp->user);
150   sfree(wp->pass);
151   sfree(wp->credentials);
152   sfree(wp->cacert);
153   sfree(wp->post_body);
154   curl_slist_free_all(wp->headers);
155   curl_stats_destroy(wp->stats);
156
157   sfree(wp->buffer);
158
159   cc_web_match_free(wp->matches);
160   sfree(wp);
161 } /* }}} void cc_web_page_free */
162
163 static int cc_config_append_string(const char *name,
164                                    struct curl_slist **dest, /* {{{ */
165                                    oconfig_item_t *ci) {
166   struct curl_slist *temp = NULL;
167   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
168     WARNING("curl plugin: `%s' needs exactly one string argument.", name);
169     return -1;
170   }
171
172   temp = curl_slist_append(*dest, ci->values[0].value.string);
173   if (temp == NULL)
174     return -1;
175
176   *dest = temp;
177
178   return 0;
179 } /* }}} int cc_config_append_string */
180
181 static int cc_config_add_match_dstype(int *dstype_ret, /* {{{ */
182                                       oconfig_item_t *ci) {
183   int dstype;
184
185   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
186     WARNING("curl plugin: `DSType' needs exactly one string argument.");
187     return -1;
188   }
189
190   if (strncasecmp("Gauge", ci->values[0].value.string, strlen("Gauge")) == 0) {
191     dstype = UTILS_MATCH_DS_TYPE_GAUGE;
192     if (strcasecmp("GaugeAverage", ci->values[0].value.string) == 0)
193       dstype |= UTILS_MATCH_CF_GAUGE_AVERAGE;
194     else if (strcasecmp("GaugeMin", ci->values[0].value.string) == 0)
195       dstype |= UTILS_MATCH_CF_GAUGE_MIN;
196     else if (strcasecmp("GaugeMax", ci->values[0].value.string) == 0)
197       dstype |= UTILS_MATCH_CF_GAUGE_MAX;
198     else if (strcasecmp("GaugeLast", ci->values[0].value.string) == 0)
199       dstype |= UTILS_MATCH_CF_GAUGE_LAST;
200     else
201       dstype = 0;
202   } else if (strncasecmp("Counter", ci->values[0].value.string,
203                          strlen("Counter")) == 0) {
204     dstype = UTILS_MATCH_DS_TYPE_COUNTER;
205     if (strcasecmp("CounterSet", ci->values[0].value.string) == 0)
206       dstype |= UTILS_MATCH_CF_COUNTER_SET;
207     else if (strcasecmp("CounterAdd", ci->values[0].value.string) == 0)
208       dstype |= UTILS_MATCH_CF_COUNTER_ADD;
209     else if (strcasecmp("CounterInc", ci->values[0].value.string) == 0)
210       dstype |= UTILS_MATCH_CF_COUNTER_INC;
211     else
212       dstype = 0;
213   } else if (strncasecmp("Derive", ci->values[0].value.string,
214                          strlen("Derive")) == 0) {
215     dstype = UTILS_MATCH_DS_TYPE_DERIVE;
216     if (strcasecmp("DeriveSet", ci->values[0].value.string) == 0)
217       dstype |= UTILS_MATCH_CF_DERIVE_SET;
218     else if (strcasecmp("DeriveAdd", ci->values[0].value.string) == 0)
219       dstype |= UTILS_MATCH_CF_DERIVE_ADD;
220     else if (strcasecmp("DeriveInc", ci->values[0].value.string) == 0)
221       dstype |= UTILS_MATCH_CF_DERIVE_INC;
222     else
223       dstype = 0;
224   } else if (strncasecmp("Absolute", ci->values[0].value.string,
225                          strlen("Absolute")) == 0) {
226     dstype = UTILS_MATCH_DS_TYPE_ABSOLUTE;
227     if (strcasecmp("AbsoluteSet", ci->values[0].value.string) ==
228         0) /* Absolute DS is reset-on-read so no sense doin anything else but
229               set */
230       dstype |= UTILS_MATCH_CF_ABSOLUTE_SET;
231     else
232       dstype = 0;
233   }
234
235   else {
236     dstype = 0;
237   }
238
239   if (dstype == 0) {
240     WARNING("curl plugin: `%s' is not a valid argument to `DSType'.",
241             ci->values[0].value.string);
242     return -1;
243   }
244
245   *dstype_ret = dstype;
246   return 0;
247 } /* }}} int cc_config_add_match_dstype */
248
249 static int cc_config_add_match(web_page_t *page, /* {{{ */
250                                oconfig_item_t *ci) {
251   web_match_t *match;
252   int status;
253
254   if (ci->values_num != 0) {
255     WARNING("curl plugin: Ignoring arguments for the `Match' block.");
256   }
257
258   match = calloc(1, sizeof(*match));
259   if (match == NULL) {
260     ERROR("curl plugin: calloc failed.");
261     return -1;
262   }
263
264   status = 0;
265   for (int i = 0; i < ci->children_num; i++) {
266     oconfig_item_t *child = ci->children + i;
267
268     if (strcasecmp("Regex", child->key) == 0)
269       status = cf_util_get_string(child, &match->regex);
270     else if (strcasecmp("ExcludeRegex", child->key) == 0)
271       status = cf_util_get_string(child, &match->exclude_regex);
272     else if (strcasecmp("DSType", child->key) == 0)
273       status = cc_config_add_match_dstype(&match->dstype, child);
274     else if (strcasecmp("Type", child->key) == 0)
275       status = cf_util_get_string(child, &match->type);
276     else if (strcasecmp("Instance", child->key) == 0)
277       status = cf_util_get_string(child, &match->instance);
278     else {
279       WARNING("curl plugin: Option `%s' not allowed here.", child->key);
280       status = -1;
281     }
282
283     if (status != 0)
284       break;
285   } /* for (i = 0; i < ci->children_num; i++) */
286
287   while (status == 0) {
288     if (match->regex == NULL) {
289       WARNING("curl plugin: `Regex' missing in `Match' block.");
290       status = -1;
291     }
292
293     if (match->type == NULL) {
294       WARNING("curl plugin: `Type' missing in `Match' block.");
295       status = -1;
296     }
297
298     if (match->dstype == 0) {
299       WARNING("curl plugin: `DSType' missing in `Match' block.");
300       status = -1;
301     }
302
303     break;
304   } /* while (status == 0) */
305
306   if (status != 0) {
307     cc_web_match_free(match);
308     return status;
309   }
310
311   match->match =
312       match_create_simple(match->regex, match->exclude_regex, match->dstype);
313   if (match->match == NULL) {
314     ERROR("curl plugin: match_create_simple failed.");
315     cc_web_match_free(match);
316     return -1;
317   } else {
318     web_match_t *prev;
319
320     prev = page->matches;
321     while ((prev != NULL) && (prev->next != NULL))
322       prev = prev->next;
323
324     if (prev == NULL)
325       page->matches = match;
326     else
327       prev->next = match;
328   }
329
330   return 0;
331 } /* }}} int cc_config_add_match */
332
333 static int cc_page_init_curl(web_page_t *wp) /* {{{ */
334 {
335   wp->curl = curl_easy_init();
336   if (wp->curl == NULL) {
337     ERROR("curl plugin: curl_easy_init failed.");
338     return -1;
339   }
340
341   curl_easy_setopt(wp->curl, CURLOPT_NOSIGNAL, 1L);
342   curl_easy_setopt(wp->curl, CURLOPT_WRITEFUNCTION, cc_curl_callback);
343   curl_easy_setopt(wp->curl, CURLOPT_WRITEDATA, wp);
344   curl_easy_setopt(wp->curl, CURLOPT_USERAGENT, COLLECTD_USERAGENT);
345   curl_easy_setopt(wp->curl, CURLOPT_ERRORBUFFER, wp->curl_errbuf);
346   curl_easy_setopt(wp->curl, CURLOPT_FOLLOWLOCATION, 1L);
347   curl_easy_setopt(wp->curl, CURLOPT_MAXREDIRS, 50L);
348
349   if (wp->user != NULL) {
350 #ifdef HAVE_CURLOPT_USERNAME
351     curl_easy_setopt(wp->curl, CURLOPT_USERNAME, wp->user);
352     curl_easy_setopt(wp->curl, CURLOPT_PASSWORD,
353                      (wp->pass == NULL) ? "" : wp->pass);
354 #else
355     size_t credentials_size;
356
357     credentials_size = strlen(wp->user) + 2;
358     if (wp->pass != NULL)
359       credentials_size += strlen(wp->pass);
360
361     wp->credentials = malloc(credentials_size);
362     if (wp->credentials == NULL) {
363       ERROR("curl plugin: malloc failed.");
364       return -1;
365     }
366
367     snprintf(wp->credentials, credentials_size, "%s:%s", wp->user,
368              (wp->pass == NULL) ? "" : wp->pass);
369     curl_easy_setopt(wp->curl, CURLOPT_USERPWD, wp->credentials);
370 #endif
371
372     if (wp->digest)
373       curl_easy_setopt(wp->curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
374   }
375
376   curl_easy_setopt(wp->curl, CURLOPT_SSL_VERIFYPEER, (long)wp->verify_peer);
377   curl_easy_setopt(wp->curl, CURLOPT_SSL_VERIFYHOST, wp->verify_host ? 2L : 0L);
378   if (wp->cacert != NULL)
379     curl_easy_setopt(wp->curl, CURLOPT_CAINFO, wp->cacert);
380   if (wp->headers != NULL)
381     curl_easy_setopt(wp->curl, CURLOPT_HTTPHEADER, wp->headers);
382   if (wp->post_body != NULL)
383     curl_easy_setopt(wp->curl, CURLOPT_POSTFIELDS, wp->post_body);
384
385 #ifdef HAVE_CURLOPT_TIMEOUT_MS
386   if (wp->timeout >= 0)
387     curl_easy_setopt(wp->curl, CURLOPT_TIMEOUT_MS, (long)wp->timeout);
388   else
389     curl_easy_setopt(wp->curl, CURLOPT_TIMEOUT_MS,
390                      (long)CDTIME_T_TO_MS(plugin_get_interval()));
391 #endif
392
393   return 0;
394 } /* }}} int cc_page_init_curl */
395
396 static int cc_config_add_page(oconfig_item_t *ci) /* {{{ */
397 {
398   cdtime_t interval = 0;
399   web_page_t *page;
400   int status;
401
402   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
403     WARNING("curl plugin: `Page' blocks need exactly one string argument.");
404     return -1;
405   }
406
407   page = calloc(1, sizeof(*page));
408   if (page == NULL) {
409     ERROR("curl plugin: calloc failed.");
410     return -1;
411   }
412   page->plugin_name = NULL;
413   page->url = NULL;
414   page->user = NULL;
415   page->pass = NULL;
416   page->digest = false;
417   page->verify_peer = true;
418   page->verify_host = true;
419   page->response_time = false;
420   page->response_code = false;
421   page->timeout = -1;
422   page->stats = NULL;
423
424   page->instance = strdup(ci->values[0].value.string);
425   if (page->instance == NULL) {
426     ERROR("curl plugin: strdup failed.");
427     sfree(page);
428     return -1;
429   }
430
431   /* Process all children */
432   status = 0;
433   for (int i = 0; i < ci->children_num; i++) {
434     oconfig_item_t *child = ci->children + i;
435
436     if (strcasecmp("Plugin", child->key) == 0)
437       status = cf_util_get_string(child, &page->plugin_name);
438     else 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("Interval", child->key) == 0)
464       status = cf_util_get_cdtime(child, &interval);
465     else if (strcasecmp("Timeout", child->key) == 0)
466       status = cf_util_get_int(child, &page->timeout);
467     else if (strcasecmp("Statistics", child->key) == 0) {
468       page->stats = curl_stats_from_config(child);
469       if (page->stats == NULL)
470         status = -1;
471     } else {
472       WARNING("curl plugin: Option `%s' not allowed here.", child->key);
473       status = -1;
474     }
475
476     if (status != 0)
477       break;
478   } /* for (i = 0; i < ci->children_num; i++) */
479
480   /* Additionial sanity checks and libCURL initialization. */
481   while (status == 0) {
482     if (page->url == NULL) {
483       WARNING("curl plugin: `URL' missing in `Page' block.");
484       status = -1;
485     }
486
487     if (page->matches == NULL && page->stats == NULL && !page->response_time &&
488         !page->response_code) {
489       assert(page->instance != NULL);
490       WARNING("curl plugin: No (valid) `Match' block "
491               "or Statistics or MeasureResponseTime or MeasureResponseCode "
492               "within `Page' block `%s'.",
493               page->instance);
494       status = -1;
495     }
496
497     if (status == 0)
498       status = cc_page_init_curl(page);
499
500     break;
501   } /* while (status == 0) */
502
503   if (status != 0) {
504     cc_web_page_free(page);
505     return status;
506   }
507
508   /* If all went well, register this page for reading */
509   char *cb_name = ssnprintf_alloc("curl-%s-%s", page->instance, page->url);
510
511   plugin_register_complex_read(/* group = */ NULL, cb_name, cc_read_page,
512                                interval,
513                                &(user_data_t){
514                                    .data = page, .free_func = cc_web_page_free,
515                                });
516   sfree(cb_name);
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   curl_global_init(CURL_GLOBAL_SSL);
556   return 0;
557 } /* }}} int cc_init */
558
559 static void cc_submit(const web_page_t *wp, const web_match_t *wm, /* {{{ */
560                       value_t value) {
561   value_list_t vl = VALUE_LIST_INIT;
562
563   vl.values = &value;
564   vl.values_len = 1;
565   sstrncpy(vl.plugin, (wp->plugin_name != NULL) ? wp->plugin_name : "curl",
566            sizeof(vl.plugin));
567   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
568   sstrncpy(vl.type, wm->type, sizeof(vl.type));
569   if (wm->instance != NULL)
570     sstrncpy(vl.type_instance, wm->instance, sizeof(vl.type_instance));
571
572   plugin_dispatch_values(&vl);
573 } /* }}} void cc_submit */
574
575 static void cc_submit_response_code(const web_page_t *wp, long code) /* {{{ */
576 {
577   value_list_t vl = VALUE_LIST_INIT;
578
579   vl.values = &(value_t){.gauge = (gauge_t)code};
580   vl.values_len = 1;
581   sstrncpy(vl.plugin, (wp->plugin_name != NULL) ? wp->plugin_name : "curl",
582            sizeof(vl.plugin));
583   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
584   sstrncpy(vl.type, "response_code", sizeof(vl.type));
585
586   plugin_dispatch_values(&vl);
587 } /* }}} void cc_submit_response_code */
588
589 static void cc_submit_response_time(const web_page_t *wp, /* {{{ */
590                                     gauge_t response_time) {
591   value_list_t vl = VALUE_LIST_INIT;
592
593   vl.values = &(value_t){.gauge = response_time};
594   vl.values_len = 1;
595   sstrncpy(vl.plugin, (wp->plugin_name != NULL) ? wp->plugin_name : "curl",
596            sizeof(vl.plugin));
597   sstrncpy(vl.plugin_instance, wp->instance, sizeof(vl.plugin_instance));
598   sstrncpy(vl.type, "response_time", sizeof(vl.type));
599
600   plugin_dispatch_values(&vl);
601 } /* }}} void cc_submit_response_time */
602
603 static int cc_read_page(user_data_t *ud) /* {{{ */
604 {
605
606   if ((ud == NULL) || (ud->data == NULL)) {
607     ERROR("curl plugin: cc_read_page: Invalid user data.");
608     return -1;
609   }
610
611   web_page_t *wp = (web_page_t *)ud->data;
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 void module_register(void) {
670   plugin_register_complex_config("curl", cc_config);
671   plugin_register_init("curl", cc_init);
672 } /* void module_register */