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 */
tss2_add_vserver(int vserver_port)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
tss2_submit_gauge(const char * plugin_instance,const char * type,const char * type_instance,gauge_t value)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
tss2_submit_io(const char * plugin_instance,const char * type,derive_t rx,derive_t tx)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
tss2_close_socket(void)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
tss2_get_socket(FILE ** ret_read_fh,FILE ** ret_write_fh)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
tss2_send_request(FILE * fh,const char * request)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
tss2_receive_line(FILE * fh,char * buffer,int buffer_size)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
tss2_select_vserver(FILE * read_fh,FILE * write_fh,vserver_list_t * vserver)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
tss2_vserver_gapl(FILE * read_fh,FILE * write_fh,gauge_t * ret_value)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
tss2_read_vserver(vserver_list_t * vserver)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
tss2_config(const char * key,const char * value)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
tss2_read(void)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
tss2_shutdown(void)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
module_register(void)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 */
722