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_list_t vl = VALUE_LIST_INIT;
113 vl.values = &(value_t){.gauge = value};
114 vl.values_len = 1;
115 sstrncpy(vl.plugin, "teamspeak2", sizeof(vl.plugin));
117 if (plugin_instance != NULL)
118 sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));
120 sstrncpy(vl.type, type, sizeof(vl.type));
122 if (type_instance != NULL)
123 sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance));
125 plugin_dispatch_values(&vl);
126 } /* void tss2_submit_gauge */
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 };
138 vl.values = values;
139 vl.values_len = STATIC_ARRAY_SIZE(values);
140 sstrncpy(vl.plugin, "teamspeak2", sizeof(vl.plugin));
142 if (plugin_instance != NULL)
143 sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));
145 sstrncpy(vl.type, type, sizeof(vl.type));
147 plugin_dispatch_values(&vl);
148 } /* void tss2_submit_gauge */
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 }
158 if (global_read_fh != NULL) {
159 fclose(global_read_fh);
160 global_read_fh = NULL;
161 }
163 if (global_write_fh != NULL) {
164 fclose(global_write_fh);
165 global_write_fh = NULL;
166 }
167 } /* void tss2_close_socket */
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;
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 }
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};
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 }
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 }
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 }
224 /*
225 * Success, we can break. Don't need more than one connection
226 */
227 break;
228 } /* for (ai_ptr) */
230 freeaddrinfo(ai_head);
232 /* Check if we really got connected */
233 if (sd < 0)
234 return (-1);
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 }
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 }
255 { /* Check that the server correctly identifies itself. */
256 char buffer[4096];
257 char *buffer_ptr;
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;
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 }
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 */
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;
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);
300 return (0);
301 } /* int tss2_send_request */
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;
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 }
322 buffer[buffer_size - 1] = 0;
323 return (0);
324 } /* int tss2_receive_line */
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;
335 /* Send request */
336 ssnprintf(command, sizeof(command), "sel %i\r\n", vserver->port);
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 }
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;
352 /* Check answer */
353 if ((strncasecmp("OK", response, 2) == 0) &&
354 ((response[2] == 0) || (response[2] == '\n') || (response[2] == '\r')))
355 return (0);
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 */
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;
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 }
379 while (42) {
380 char buffer[4096];
381 char *value;
382 char *endptr = NULL;
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;
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 }
407 value = &buffer[20];
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 }
428 *ret_value = packet_loss;
429 return (0);
430 } /* int tss2_vserver_gapl */
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;
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;
449 char plugin_instance[DATA_MAX_NAME_LEN] = {0};
451 FILE *read_fh;
452 FILE *write_fh;
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 }
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 ssnprintf(plugin_instance, sizeof(plugin_instance), "vserver%i",
467 vserver->port);
469 /* Select the server */
470 status = tss2_select_vserver(read_fh, write_fh, vserver);
471 if (status != 0)
472 return (status);
474 status = tss2_send_request(write_fh, "si\r\n");
475 }
477 if (status != 0) {
478 ERROR("teamspeak2 plugin: tss2_send_request failed.");
479 return (-1);
480 }
482 /* Loop until break */
483 while (42) {
484 char buffer[4096];
485 char *key;
486 char *value;
487 char *endptr = NULL;
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 }
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 }
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++;
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++;
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) */
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 }
600 if ((valid & 0x01) == 0x01)
601 tss2_submit_gauge(plugin_instance, "users", NULL, users);
603 if ((valid & 0x06) == 0x06)
604 tss2_submit_io(plugin_instance, "io_octets", rx_octets, tx_octets);
606 if ((valid & 0x18) == 0x18)
607 tss2_submit_io(plugin_instance, "io_packets", rx_packets, tx_packets);
609 if ((valid & 0x20) == 0x20)
610 tss2_submit_gauge(plugin_instance, "percent", "packet_loss", packet_loss);
612 if ((valid & 0x40) == 0x40)
613 tss2_submit_gauge(plugin_instance, "gauge", "channels", channels);
615 if ((valid & 0x80) == 0x80)
616 tss2_submit_gauge(plugin_instance, "gauge", "servers", servers);
618 if (valid == 0)
619 return (-1);
620 return (0);
621 } /* int tss2_read_vserver */
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;
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;
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;
651 status = tss2_add_vserver(atoi(value));
652 if (status != 0)
653 return (1);
654 } else {
655 /* Unknown variable found */
656 return (-1);
657 }
659 return 0;
660 } /* int tss2_config */
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;
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 }
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 }
692 if (success == 0)
693 return (-1);
694 return (0);
695 } /* int tss2_read */
697 static int tss2_shutdown(void) {
698 /*
699 * Shutdown handler
700 */
701 vserver_list_t *entry;
703 tss2_close_socket();
705 entry = server_list;
706 server_list = NULL;
707 while (entry != NULL) {
708 vserver_list_t *next;
710 next = entry->next;
711 sfree(entry);
712 entry = next;
713 }
715 /* Get rid of the configuration */
716 sfree(config_host);
717 sfree(config_port);
719 return (0);
720 } /* int tss2_shutdown */
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 */
732 /* vim: set sw=4 ts=4 : */