Fix compile time issues
[collectd.git] / src / teamspeak2.c
1 /**
2  * collectd - src/teamspeak2.c
3  * Copyright (C) 2008  Stefan Hacker
4  * Copyright (C) 2008  Florian Forster
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  *   Stefan Hacker <d0t at dbclan dot de>
21  *   Florian Forster <octo at collectd.org>
22  **/
23
24 #include "collectd.h"
25
26 #include "plugin.h"
27 #include "utils/common/common.h"
28
29 #include <arpa/inet.h>
30 #include <netdb.h>
31 #include <netinet/in.h>
32 #include <sys/types.h>
33
34 /*
35  * Defines
36  */
37 /* Default host and port */
38 #define DEFAULT_HOST "127.0.0.1"
39 #define DEFAULT_PORT "51234"
40
41 /*
42  * Variables
43  */
44 /* Server linked list structure */
45 typedef struct vserver_list_s {
46   int port;
47   struct vserver_list_s *next;
48 } vserver_list_t;
49 static vserver_list_t *server_list;
50
51 /* Host data */
52 static char *config_host;
53 static char *config_port;
54
55 static FILE *global_read_fh;
56 static FILE *global_write_fh;
57
58 /* Config data */
59 static const char *config_keys[] = {"Host", "Port", "Server"};
60 static int config_keys_num = STATIC_ARRAY_SIZE(config_keys);
61
62 /*
63  * Functions
64  */
65 static int tss2_add_vserver(int vserver_port) {
66   /*
67    * Adds a new vserver to the linked list
68    */
69   vserver_list_t *entry;
70
71   /* Check port range */
72   if ((vserver_port <= 0) || (vserver_port > 65535)) {
73     ERROR("teamspeak2 plugin: VServer port is invalid: %i", vserver_port);
74     return -1;
75   }
76
77   /* Allocate memory */
78   entry = calloc(1, sizeof(*entry));
79   if (entry == NULL) {
80     ERROR("teamspeak2 plugin: calloc failed.");
81     return -1;
82   }
83
84   /* Save data */
85   entry->port = vserver_port;
86
87   /* Insert to list */
88   if (server_list == NULL) {
89     /* Add the server as the first element */
90     server_list = entry;
91   } else {
92     vserver_list_t *prev;
93
94     /* Add the server to the end of the list */
95     prev = server_list;
96     while (prev->next != NULL)
97       prev = prev->next;
98     prev->next = entry;
99   }
100
101   INFO("teamspeak2 plugin: Registered new vserver: %i", vserver_port);
102
103   return 0;
104 } /* int tss2_add_vserver */
105
106 static void tss2_submit_gauge(const char *plugin_instance, const char *type,
107                               const char *type_instance, gauge_t value) {
108   /*
109    * Submits a gauge value to the collectd daemon
110    */
111   value_list_t vl = VALUE_LIST_INIT;
112
113   vl.values = &(value_t){.gauge = value};
114   vl.values_len = 1;
115   sstrncpy(vl.plugin, "teamspeak2", sizeof(vl.plugin));
116
117   if (plugin_instance != NULL)
118     sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));
119
120   sstrncpy(vl.type, type, sizeof(vl.type));
121
122   if (type_instance != NULL)
123     sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance));
124
125   plugin_dispatch_values(&vl);
126 } /* void tss2_submit_gauge */
127
128 static void tss2_submit_io(const char *plugin_instance, const char *type,
129                            derive_t rx, derive_t tx) {
130   /*
131    * Submits the io rx/tx tuple to the collectd daemon
132    */
133   value_list_t vl = VALUE_LIST_INIT;
134   value_t values[] = {
135       {.derive = rx},
136       {.derive = tx},
137   };
138
139   vl.values = values;
140   vl.values_len = STATIC_ARRAY_SIZE(values);
141   sstrncpy(vl.plugin, "teamspeak2", sizeof(vl.plugin));
142
143   if (plugin_instance != NULL)
144     sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));
145
146   sstrncpy(vl.type, type, sizeof(vl.type));
147
148   plugin_dispatch_values(&vl);
149 } /* void tss2_submit_gauge */
150
151 static void tss2_close_socket(void) {
152   /*
153    * Closes all sockets
154    */
155   if (global_write_fh != NULL) {
156     fputs("quit\r\n", global_write_fh);
157   }
158
159   if (global_read_fh != NULL) {
160     fclose(global_read_fh);
161     global_read_fh = NULL;
162   }
163
164   if (global_write_fh != NULL) {
165     fclose(global_write_fh);
166     global_write_fh = NULL;
167   }
168 } /* void tss2_close_socket */
169
170 static int tss2_get_socket(FILE **ret_read_fh, FILE **ret_write_fh) {
171   /*
172    * Returns connected file objects or establishes the connection
173    * if it's not already present
174    */
175   struct addrinfo *ai_head;
176   int sd = -1;
177   int status;
178
179   /* Check if we already got opened connections */
180   if ((global_read_fh != NULL) && (global_write_fh != NULL)) {
181     /* If so, use them */
182     if (ret_read_fh != NULL)
183       *ret_read_fh = global_read_fh;
184     if (ret_write_fh != NULL)
185       *ret_write_fh = global_write_fh;
186     return 0;
187   }
188
189   /* Get all addrs for this hostname */
190   struct addrinfo ai_hints = {.ai_family = AF_UNSPEC,
191                               .ai_flags = AI_ADDRCONFIG,
192                               .ai_socktype = SOCK_STREAM};
193
194   status = getaddrinfo((config_host != NULL) ? config_host : DEFAULT_HOST,
195                        (config_port != NULL) ? config_port : DEFAULT_PORT,
196                        &ai_hints, &ai_head);
197   if (status != 0) {
198     ERROR("teamspeak2 plugin: getaddrinfo failed: %s", gai_strerror(status));
199     return -1;
200   }
201
202   /* Try all given hosts until we can connect to one */
203   for (struct addrinfo *ai_ptr = ai_head; ai_ptr != NULL;
204        ai_ptr = ai_ptr->ai_next) {
205     /* Create socket */
206     sd = socket(ai_ptr->ai_family, ai_ptr->ai_socktype, ai_ptr->ai_protocol);
207     if (sd < 0) {
208       WARNING("teamspeak2 plugin: socket failed: %s", STRERRNO);
209       continue;
210     }
211
212     /* Try to connect */
213     status = connect(sd, ai_ptr->ai_addr, ai_ptr->ai_addrlen);
214     if (status != 0) {
215       WARNING("teamspeak2 plugin: connect failed: %s", STRERRNO);
216       close(sd);
217       sd = -1;
218       continue;
219     }
220
221     /*
222      * Success, we can break. Don't need more than one connection
223      */
224     break;
225   } /* for (ai_ptr) */
226
227   freeaddrinfo(ai_head);
228
229   /* Check if we really got connected */
230   if (sd < 0)
231     return -1;
232
233   /* Create file objects from sockets */
234   global_read_fh = fdopen(sd, "r");
235   if (global_read_fh == NULL) {
236     ERROR("teamspeak2 plugin: fdopen failed: %s", STRERRNO);
237     close(sd);
238     return -1;
239   }
240
241   global_write_fh = fdopen(sd, "w");
242   if (global_write_fh == NULL) {
243     ERROR("teamspeak2 plugin: fdopen failed: %s", STRERRNO);
244     tss2_close_socket();
245     return -1;
246   }
247
248   { /* Check that the server correctly identifies itself. */
249     char buffer[4096];
250     char *buffer_ptr;
251
252     buffer_ptr = fgets(buffer, sizeof(buffer), global_read_fh);
253     if (buffer_ptr == NULL) {
254       WARNING("teamspeak2 plugin: Unexpected EOF received "
255               "from remote host %s:%s.",
256               config_host ? config_host : DEFAULT_HOST,
257               config_port ? config_port : DEFAULT_PORT);
258     }
259     buffer[sizeof(buffer) - 1] = '\0';
260
261     if (memcmp("[TS]\r\n", buffer, 6) != 0) {
262       ERROR("teamspeak2 plugin: Unexpected response when connecting "
263             "to server. Expected ``[TS]'', got ``%s''.",
264             buffer);
265       tss2_close_socket();
266       return -1;
267     }
268     DEBUG("teamspeak2 plugin: Server send correct banner, connected!");
269   }
270
271   /* Copy the new filehandles to the given pointers */
272   if (ret_read_fh != NULL)
273     *ret_read_fh = global_read_fh;
274   if (ret_write_fh != NULL)
275     *ret_write_fh = global_write_fh;
276   return 0;
277 } /* int tss2_get_socket */
278
279 static int tss2_send_request(FILE *fh, const char *request) {
280   /*
281    * This function puts a request to the server socket
282    */
283   int status;
284
285   status = fputs(request, fh);
286   if (status < 0) {
287     ERROR("teamspeak2 plugin: fputs failed.");
288     tss2_close_socket();
289     return -1;
290   }
291   fflush(fh);
292
293   return 0;
294 } /* int tss2_send_request */
295
296 static int tss2_receive_line(FILE *fh, char *buffer, int buffer_size) {
297   /*
298    * Receive a single line from the given file object
299    */
300   char *temp;
301
302   /*
303    * fgets is blocking but much easier then doing anything else
304    * TODO: Non-blocking Version would be safer
305    */
306   temp = fgets(buffer, buffer_size, fh);
307   if (temp == NULL) {
308     ERROR("teamspeak2 plugin: fgets failed: %s", STRERRNO);
309     tss2_close_socket();
310     return -1;
311   }
312
313   buffer[buffer_size - 1] = '\0';
314   return 0;
315 } /* int tss2_receive_line */
316
317 static int tss2_select_vserver(FILE *read_fh, FILE *write_fh,
318                                vserver_list_t *vserver) {
319   /*
320    * Tell the server to select the given vserver
321    */
322   char command[128];
323   char response[128];
324   int status;
325
326   /* Send request */
327   snprintf(command, sizeof(command), "sel %i\r\n", vserver->port);
328
329   status = tss2_send_request(write_fh, command);
330   if (status != 0) {
331     ERROR("teamspeak2 plugin: tss2_send_request (%s) failed.", command);
332     return -1;
333   }
334
335   /* Get answer */
336   status = tss2_receive_line(read_fh, response, sizeof(response));
337   if (status != 0) {
338     ERROR("teamspeak2 plugin: tss2_receive_line failed.");
339     return -1;
340   }
341   response[sizeof(response) - 1] = '\0';
342
343   /* Check answer */
344   if ((strncasecmp("OK", response, 2) == 0) &&
345       ((response[2] == 0) || (response[2] == '\n') || (response[2] == '\r')))
346     return 0;
347
348   ERROR("teamspeak2 plugin: Command ``%s'' failed. "
349         "Response received from server was: ``%s''.",
350         command, response);
351   return -1;
352 } /* int tss2_select_vserver */
353
354 static int tss2_vserver_gapl(FILE *read_fh, FILE *write_fh,
355                              gauge_t *ret_value) {
356   /*
357    * Reads the vserver's average packet loss and submits it to collectd.
358    * Be sure to run the tss2_read_vserver function before calling this so
359    * the vserver is selected correctly.
360    */
361   gauge_t packet_loss = NAN;
362   int status;
363
364   status = tss2_send_request(write_fh, "gapl\r\n");
365   if (status != 0) {
366     ERROR("teamspeak2 plugin: tss2_send_request (gapl) failed.");
367     return -1;
368   }
369
370   while (42) {
371     char buffer[4096];
372     char *value;
373     char *endptr = NULL;
374
375     status = tss2_receive_line(read_fh, buffer, sizeof(buffer));
376     if (status != 0) {
377       /* Set to NULL just to make sure no one uses these FHs anymore. */
378       read_fh = NULL;
379       write_fh = NULL;
380       ERROR("teamspeak2 plugin: tss2_receive_line failed.");
381       return -1;
382     }
383     buffer[sizeof(buffer) - 1] = '\0';
384
385     if (strncmp("average_packet_loss=", buffer,
386                 strlen("average_packet_loss=")) == 0) {
387       /* Got average packet loss, now interpret it */
388       value = &buffer[20];
389       /* Replace , with . */
390       while (*value != 0) {
391         if (*value == ',') {
392           *value = '.';
393           break;
394         }
395         value++;
396       }
397
398       value = &buffer[20];
399
400       packet_loss = strtod(value, &endptr);
401       if (value == endptr) {
402         /* Failed */
403         WARNING("teamspeak2 plugin: Could not read average package "
404                 "loss from string: %s",
405                 buffer);
406         continue;
407       }
408     } else if (strncasecmp("OK", buffer, 2) == 0) {
409       break;
410     } else if (strncasecmp("ERROR", buffer, 5) == 0) {
411       ERROR("teamspeak2 plugin: Server returned an error: %s", buffer);
412       return -1;
413     } else {
414       WARNING("teamspeak2 plugin: Server returned unexpected string: %s",
415               buffer);
416     }
417   }
418
419   *ret_value = packet_loss;
420   return 0;
421 } /* int tss2_vserver_gapl */
422
423 static int tss2_read_vserver(vserver_list_t *vserver) {
424   /*
425    * Poll information for the given vserver and submit it to collect.
426    * If vserver is NULL the global server information will be queried.
427    */
428   int status;
429
430   gauge_t users = NAN;
431   gauge_t channels = NAN;
432   gauge_t servers = NAN;
433   derive_t rx_octets = 0;
434   derive_t tx_octets = 0;
435   derive_t rx_packets = 0;
436   derive_t tx_packets = 0;
437   gauge_t packet_loss = NAN;
438   int valid = 0;
439
440   char plugin_instance[DATA_MAX_NAME_LEN] = {0};
441
442   FILE *read_fh;
443   FILE *write_fh;
444
445   /* Get the send/receive sockets */
446   status = tss2_get_socket(&read_fh, &write_fh);
447   if (status != 0) {
448     ERROR("teamspeak2 plugin: tss2_get_socket failed.");
449     return -1;
450   }
451
452   if (vserver == NULL) {
453     /* Request global information */
454     status = tss2_send_request(write_fh, "gi\r\n");
455   } else {
456     /* Request server information */
457     snprintf(plugin_instance, sizeof(plugin_instance), "vserver%i",
458              vserver->port);
459
460     /* Select the server */
461     status = tss2_select_vserver(read_fh, write_fh, vserver);
462     if (status != 0)
463       return status;
464
465     status = tss2_send_request(write_fh, "si\r\n");
466   }
467
468   if (status != 0) {
469     ERROR("teamspeak2 plugin: tss2_send_request failed.");
470     return -1;
471   }
472
473   /* Loop until break */
474   while (42) {
475     char buffer[4096];
476     char *key;
477     char *value;
478     char *endptr = NULL;
479
480     /* Read one line of the server's answer */
481     status = tss2_receive_line(read_fh, buffer, sizeof(buffer));
482     if (status != 0) {
483       /* Set to NULL just to make sure no one uses these FHs anymore. */
484       read_fh = NULL;
485       write_fh = NULL;
486       ERROR("teamspeak2 plugin: tss2_receive_line failed.");
487       break;
488     }
489
490     if (strncasecmp("ERROR", buffer, 5) == 0) {
491       ERROR("teamspeak2 plugin: Server returned an error: %s", buffer);
492       break;
493     } else if (strncasecmp("OK", buffer, 2) == 0) {
494       break;
495     }
496
497     /* Split line into key and value */
498     key = strchr(buffer, '_');
499     if (key == NULL) {
500       DEBUG("teamspeak2 plugin: Cannot parse line: %s", buffer);
501       continue;
502     }
503     key++;
504
505     /* Evaluate assignment */
506     value = strchr(key, '=');
507     if (value == NULL) {
508       DEBUG("teamspeak2 plugin: Cannot parse line: %s", buffer);
509       continue;
510     }
511     *value = 0;
512     value++;
513
514     /* Check for known key and save the given value */
515     /* global info: users_online,
516      * server info: currentusers. */
517     if ((strcmp("currentusers", key) == 0) ||
518         (strcmp("users_online", key) == 0)) {
519       users = strtod(value, &endptr);
520       if (value != endptr)
521         valid |= 0x01;
522     }
523     /* global info: channels,
524      * server info: currentchannels. */
525     else if ((strcmp("currentchannels", key) == 0) ||
526              (strcmp("channels", key) == 0)) {
527       channels = strtod(value, &endptr);
528       if (value != endptr)
529         valid |= 0x40;
530     }
531     /* global only */
532     else if (strcmp("servers", key) == 0) {
533       servers = strtod(value, &endptr);
534       if (value != endptr)
535         valid |= 0x80;
536     } else if (strcmp("bytesreceived", key) == 0) {
537       rx_octets = strtoll(value, &endptr, 0);
538       if (value != endptr)
539         valid |= 0x02;
540     } else if (strcmp("bytessend", key) == 0) {
541       tx_octets = strtoll(value, &endptr, 0);
542       if (value != endptr)
543         valid |= 0x04;
544     } else if (strcmp("packetsreceived", key) == 0) {
545       rx_packets = strtoll(value, &endptr, 0);
546       if (value != endptr)
547         valid |= 0x08;
548     } else if (strcmp("packetssend", key) == 0) {
549       tx_packets = strtoll(value, &endptr, 0);
550       if (value != endptr)
551         valid |= 0x10;
552     } else if ((strncmp("allow_codec_", key, strlen("allow_codec_")) == 0) ||
553                (strncmp("bwinlast", key, strlen("bwinlast")) == 0) ||
554                (strncmp("bwoutlast", key, strlen("bwoutlast")) == 0) ||
555                (strncmp("webpost_", key, strlen("webpost_")) == 0) ||
556                (strcmp("adminemail", key) == 0) ||
557                (strcmp("clan_server", key) == 0) ||
558                (strcmp("countrynumber", key) == 0) ||
559                (strcmp("id", key) == 0) || (strcmp("ispname", key) == 0) ||
560                (strcmp("linkurl", key) == 0) ||
561                (strcmp("maxusers", key) == 0) || (strcmp("name", key) == 0) ||
562                (strcmp("password", key) == 0) ||
563                (strcmp("platform", key) == 0) ||
564                (strcmp("server_platform", key) == 0) ||
565                (strcmp("server_uptime", key) == 0) ||
566                (strcmp("server_version", key) == 0) ||
567                (strcmp("udpport", key) == 0) || (strcmp("uptime", key) == 0) ||
568                (strcmp("users_maximal", key) == 0) ||
569                (strcmp("welcomemessage", key) == 0))
570       /* ignore */;
571     else {
572       INFO("teamspeak2 plugin: Unknown key-value-pair: "
573            "key = %s; value = %s;",
574            key, value);
575     }
576   } /* while (42) */
577
578   /* Collect vserver packet loss rates only if the loop above did not exit
579    * with an error. */
580   if ((status == 0) && (vserver != NULL)) {
581     status = tss2_vserver_gapl(read_fh, write_fh, &packet_loss);
582     if (status == 0) {
583       valid |= 0x20;
584     } else {
585       WARNING("teamspeak2 plugin: Reading package loss "
586               "for vserver %i failed.",
587               vserver->port);
588     }
589   }
590
591   if ((valid & 0x01) == 0x01)
592     tss2_submit_gauge(plugin_instance, "users", NULL, users);
593
594   if ((valid & 0x06) == 0x06)
595     tss2_submit_io(plugin_instance, "io_octets", rx_octets, tx_octets);
596
597   if ((valid & 0x18) == 0x18)
598     tss2_submit_io(plugin_instance, "io_packets", rx_packets, tx_packets);
599
600   if ((valid & 0x20) == 0x20)
601     tss2_submit_gauge(plugin_instance, "percent", "packet_loss", packet_loss);
602
603   if ((valid & 0x40) == 0x40)
604     tss2_submit_gauge(plugin_instance, "gauge", "channels", channels);
605
606   if ((valid & 0x80) == 0x80)
607     tss2_submit_gauge(plugin_instance, "gauge", "servers", servers);
608
609   if (valid == 0)
610     return -1;
611   return 0;
612 } /* int tss2_read_vserver */
613
614 static int tss2_config(const char *key, const char *value) {
615   /*
616    * Interpret configuration values
617    */
618   if (strcasecmp("Host", key) == 0) {
619     char *temp;
620
621     temp = strdup(value);
622     if (temp == NULL) {
623       ERROR("teamspeak2 plugin: strdup failed.");
624       return 1;
625     }
626     sfree(config_host);
627     config_host = temp;
628   } else if (strcasecmp("Port", key) == 0) {
629     char *temp;
630
631     temp = strdup(value);
632     if (temp == NULL) {
633       ERROR("teamspeak2 plugin: strdup failed.");
634       return 1;
635     }
636     sfree(config_port);
637     config_port = temp;
638   } else if (strcasecmp("Server", key) == 0) {
639     /* Server variable found */
640     int status;
641
642     status = tss2_add_vserver(atoi(value));
643     if (status != 0)
644       return 1;
645   } else {
646     /* Unknown variable found */
647     return -1;
648   }
649
650   return 0;
651 } /* int tss2_config */
652
653 static int tss2_read(void) {
654   /*
655    * Poll function which collects global and vserver information
656    * and submits it to collectd
657    */
658   int success = 0;
659   int status;
660
661   /* Handle global server variables */
662   status = tss2_read_vserver(NULL);
663   if (status == 0) {
664     success++;
665   } else {
666     WARNING("teamspeak2 plugin: Reading global server variables failed.");
667   }
668
669   /* Handle vservers */
670   for (vserver_list_t *vserver = server_list; vserver != NULL;
671        vserver = vserver->next) {
672     status = tss2_read_vserver(vserver);
673     if (status == 0) {
674       success++;
675     } else {
676       WARNING("teamspeak2 plugin: Reading statistics "
677               "for vserver %i failed.",
678               vserver->port);
679       continue;
680     }
681   }
682
683   if (success == 0)
684     return -1;
685   return 0;
686 } /* int tss2_read */
687
688 static int tss2_shutdown(void) {
689   /*
690    * Shutdown handler
691    */
692   vserver_list_t *entry;
693
694   tss2_close_socket();
695
696   entry = server_list;
697   server_list = NULL;
698   while (entry != NULL) {
699     vserver_list_t *next;
700
701     next = entry->next;
702     sfree(entry);
703     entry = next;
704   }
705
706   /* Get rid of the configuration */
707   sfree(config_host);
708   sfree(config_port);
709
710   return 0;
711 } /* int tss2_shutdown */
712
713 void module_register(void) {
714   /*
715    * Mandatory module_register function
716    */
717   plugin_register_config("teamspeak2", tss2_config, config_keys,
718                          config_keys_num);
719   plugin_register_read("teamspeak2", tss2_read);
720   plugin_register_shutdown("teamspeak2", tss2_shutdown);
721 } /* void module_register */