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