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