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 **/
24 #include "collectd.h"
26 #include "common.h"
27 #include "plugin.h"
29 #include <arpa/inet.h>
30 #include <netdb.h>
31 #include <netinet/in.h>
32 #include <sys/types.h>
34 /*
35 * Defines
36 */
37 /* Default host and port */
38 #define DEFAULT_HOST "127.0.0.1"
39 #define DEFAULT_PORT "51234"
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;
51 /* Host data */
52 static char *config_host = NULL;
53 static char *config_port = NULL;
55 static FILE *global_read_fh = NULL;
56 static FILE *global_write_fh = NULL;
58 /* Config data */
59 static const char *config_keys[] = {"Host", "Port", "Server"};
60 static int config_keys_num = STATIC_ARRAY_SIZE(config_keys);
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;
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 }
77 /* Allocate memory */
78 entry = calloc(1, sizeof(*entry));
79 if (entry == NULL) {
80 ERROR("teamspeak2 plugin: calloc failed.");
81 return (-1);
82 }
84 /* Save data */
85 entry->port = vserver_port;
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;
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 }
101 INFO("teamspeak2 plugin: Registered new vserver: %i", vserver_port);
103 return (0);
104 } /* int tss2_add_vserver */
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;
114 values[0].gauge = value;
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));
121 if (plugin_instance != NULL)
122 sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));
124 sstrncpy(vl.type, type, sizeof(vl.type));
126 if (type_instance != NULL)
127 sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance));
129 plugin_dispatch_values(&vl);
130 } /* void tss2_submit_gauge */
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;
140 values[0].derive = rx;
141 values[1].derive = tx;
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));
148 if (plugin_instance != NULL)
149 sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));
151 sstrncpy(vl.type, type, sizeof(vl.type));
153 plugin_dispatch_values(&vl);
154 } /* void tss2_submit_gauge */
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 }
164 if (global_read_fh != NULL) {
165 fclose(global_read_fh);
166 global_read_fh = NULL;
167 }
169 if (global_write_fh != NULL) {
170 fclose(global_write_fh);
171 global_write_fh = NULL;
172 }
173 } /* void tss2_close_socket */
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;
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 }
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};
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 }
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 }
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 }
230 /*
231 * Success, we can break. Don't need more than one connection
232 */
233 break;
234 } /* for (ai_ptr) */
236 freeaddrinfo(ai_head);
238 /* Check if we really got connected */
239 if (sd < 0)
240 return (-1);
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 }
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 }
261 { /* Check that the server correctly identifies itself. */
262 char buffer[4096];
263 char *buffer_ptr;
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;
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 }
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 */
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;
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);
306 return (0);
307 } /* int tss2_send_request */
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;
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 }
328 buffer[buffer_size - 1] = 0;
329 return (0);
330 } /* int tss2_receive_line */
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;
341 /* Send request */
342 ssnprintf(command, sizeof(command), "sel %i\r\n", vserver->port);
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 }
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;
358 /* Check answer */
359 if ((strncasecmp("OK", response, 2) == 0) &&
360 ((response[2] == 0) || (response[2] == '\n') || (response[2] == '\r')))
361 return (0);
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 */
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;
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 }
385 while (42) {
386 char buffer[4096];
387 char *value;
388 char *endptr = NULL;
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;
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 }
413 value = &buffer[20];
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 }
434 *ret_value = packet_loss;
435 return (0);
436 } /* int tss2_vserver_gapl */
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;
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;
455 char plugin_instance[DATA_MAX_NAME_LEN] = {0};
457 FILE *read_fh;
458 FILE *write_fh;
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 }
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);
475 /* Select the server */
476 status = tss2_select_vserver(read_fh, write_fh, vserver);
477 if (status != 0)
478 return (status);
480 status = tss2_send_request(write_fh, "si\r\n");
481 }
483 if (status != 0) {
484 ERROR("teamspeak2 plugin: tss2_send_request failed.");
485 return (-1);
486 }
488 /* Loop until break */
489 while (42) {
490 char buffer[4096];
491 char *key;
492 char *value;
493 char *endptr = NULL;
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 }
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 }
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++;
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++;
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) */
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 }
606 if ((valid & 0x01) == 0x01)
607 tss2_submit_gauge(plugin_instance, "users", NULL, users);
609 if ((valid & 0x06) == 0x06)
610 tss2_submit_io(plugin_instance, "io_octets", rx_octets, tx_octets);
612 if ((valid & 0x18) == 0x18)
613 tss2_submit_io(plugin_instance, "io_packets", rx_packets, tx_packets);
615 if ((valid & 0x20) == 0x20)
616 tss2_submit_gauge(plugin_instance, "percent", "packet_loss", packet_loss);
618 if ((valid & 0x40) == 0x40)
619 tss2_submit_gauge(plugin_instance, "gauge", "channels", channels);
621 if ((valid & 0x80) == 0x80)
622 tss2_submit_gauge(plugin_instance, "gauge", "servers", servers);
624 if (valid == 0)
625 return (-1);
626 return (0);
627 } /* int tss2_read_vserver */
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;
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;
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;
657 status = tss2_add_vserver(atoi(value));
658 if (status != 0)
659 return (1);
660 } else {
661 /* Unknown variable found */
662 return (-1);
663 }
665 return 0;
666 } /* int tss2_config */
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;
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 }
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 }
698 if (success == 0)
699 return (-1);
700 return (0);
701 } /* int tss2_read */
703 static int tss2_shutdown(void) {
704 /*
705 * Shutdown handler
706 */
707 vserver_list_t *entry;
709 tss2_close_socket();
711 entry = server_list;
712 server_list = NULL;
713 while (entry != NULL) {
714 vserver_list_t *next;
716 next = entry->next;
717 sfree(entry);
718 entry = next;
719 }
721 /* Get rid of the configuration */
722 sfree(config_host);
723 sfree(config_port);
725 return (0);
726 } /* int tss2_shutdown */
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 */
738 /* vim: set sw=4 ts=4 : */