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"
25 #include "common.h"
26 #include "plugin.h"
28 #include <netinet/in.h>
29 #include <arpa/inet.h>
30 #include <sys/types.h>
31 #include <netdb.h>
33 /*
34 * Defines
35 */
36 /* Default host and port */
37 #define DEFAULT_HOST "127.0.0.1"
38 #define DEFAULT_PORT "51234"
40 /*
41 * Variables
42 */
43 /* Server linked list structure */
44 typedef struct vserver_list_s
45 {
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[] =
60 {
61 "Host",
62 "Port",
63 "Server"
64 };
65 static int config_keys_num = STATIC_ARRAY_SIZE (config_keys);
67 /*
68 * Functions
69 */
70 static int tss2_add_vserver (int vserver_port)
71 {
72 /*
73 * Adds a new vserver to the linked list
74 */
75 vserver_list_t *entry;
77 /* Check port range */
78 if ((vserver_port <= 0) || (vserver_port > 65535))
79 {
80 ERROR ("teamspeak2 plugin: VServer port is invalid: %i",
81 vserver_port);
82 return (-1);
83 }
85 /* Allocate memory */
86 entry = calloc (1, sizeof (*entry));
87 if (entry == NULL)
88 {
89 ERROR ("teamspeak2 plugin: calloc failed.");
90 return (-1);
91 }
93 /* Save data */
94 entry->port = vserver_port;
96 /* Insert to list */
97 if(server_list == NULL) {
98 /* Add the server as the first element */
99 server_list = entry;
100 }
101 else {
102 vserver_list_t *prev;
104 /* Add the server to the end of the list */
105 prev = server_list;
106 while (prev->next != NULL)
107 prev = prev->next;
108 prev->next = entry;
109 }
111 INFO ("teamspeak2 plugin: Registered new vserver: %i", vserver_port);
113 return (0);
114 } /* int tss2_add_vserver */
116 static void tss2_submit_gauge (const char *plugin_instance,
117 const char *type, const char *type_instance,
118 gauge_t value)
119 {
120 /*
121 * Submits a gauge value to the collectd daemon
122 */
123 value_t values[1];
124 value_list_t vl = VALUE_LIST_INIT;
126 values[0].gauge = value;
128 vl.values = values;
129 vl.values_len = 1;
130 sstrncpy (vl.host, hostname_g, sizeof (vl.host));
131 sstrncpy (vl.plugin, "teamspeak2", sizeof (vl.plugin));
133 if (plugin_instance != NULL)
134 sstrncpy (vl.plugin_instance, plugin_instance,
135 sizeof (vl.plugin_instance));
137 sstrncpy (vl.type, type, sizeof (vl.type));
139 if (type_instance != NULL)
140 sstrncpy (vl.type_instance, type_instance,
141 sizeof (vl.type_instance));
143 plugin_dispatch_values (&vl);
144 } /* void tss2_submit_gauge */
146 static void tss2_submit_io (const char *plugin_instance, const char *type,
147 derive_t rx, derive_t tx)
148 {
149 /*
150 * Submits the io rx/tx tuple to the collectd daemon
151 */
152 value_t values[2];
153 value_list_t vl = VALUE_LIST_INIT;
155 values[0].derive = rx;
156 values[1].derive = tx;
158 vl.values = values;
159 vl.values_len = 2;
160 sstrncpy (vl.host, hostname_g, sizeof (vl.host));
161 sstrncpy (vl.plugin, "teamspeak2", sizeof (vl.plugin));
163 if (plugin_instance != NULL)
164 sstrncpy (vl.plugin_instance, plugin_instance,
165 sizeof (vl.plugin_instance));
167 sstrncpy (vl.type, type, sizeof (vl.type));
169 plugin_dispatch_values (&vl);
170 } /* void tss2_submit_gauge */
172 static void tss2_close_socket (void)
173 {
174 /*
175 * Closes all sockets
176 */
177 if (global_write_fh != NULL)
178 {
179 fputs ("quit\r\n", global_write_fh);
180 }
182 if (global_read_fh != NULL)
183 {
184 fclose (global_read_fh);
185 global_read_fh = NULL;
186 }
188 if (global_write_fh != NULL)
189 {
190 fclose (global_write_fh);
191 global_write_fh = NULL;
192 }
193 } /* void tss2_close_socket */
195 static int tss2_get_socket (FILE **ret_read_fh, FILE **ret_write_fh)
196 {
197 /*
198 * Returns connected file objects or establishes the connection
199 * if it's not already present
200 */
201 struct addrinfo ai_hints = { 0 };
202 struct addrinfo *ai_head;
203 struct addrinfo *ai_ptr;
204 int sd = -1;
205 int status;
207 /* Check if we already got opened connections */
208 if ((global_read_fh != NULL) && (global_write_fh != NULL))
209 {
210 /* If so, use them */
211 if (ret_read_fh != NULL)
212 *ret_read_fh = global_read_fh;
213 if (ret_write_fh != NULL)
214 *ret_write_fh = global_write_fh;
215 return (0);
216 }
218 /* Get all addrs for this hostname */
219 #ifdef AI_ADDRCONFIG
220 ai_hints.ai_flags |= AI_ADDRCONFIG;
221 #endif
222 ai_hints.ai_family = AF_UNSPEC;
223 ai_hints.ai_socktype = SOCK_STREAM;
225 status = getaddrinfo ((config_host != NULL) ? config_host : DEFAULT_HOST,
226 (config_port != NULL) ? config_port : DEFAULT_PORT,
227 &ai_hints,
228 &ai_head);
229 if (status != 0)
230 {
231 ERROR ("teamspeak2 plugin: getaddrinfo failed: %s",
232 gai_strerror (status));
233 return (-1);
234 }
236 /* Try all given hosts until we can connect to one */
237 for (ai_ptr = ai_head; ai_ptr != NULL; ai_ptr = ai_ptr->ai_next)
238 {
239 /* Create socket */
240 sd = socket (ai_ptr->ai_family, ai_ptr->ai_socktype,
241 ai_ptr->ai_protocol);
242 if (sd < 0)
243 {
244 char errbuf[1024];
245 WARNING ("teamspeak2 plugin: socket failed: %s",
246 sstrerror (errno, errbuf, sizeof (errbuf)));
247 continue;
248 }
250 /* Try to connect */
251 status = connect (sd, ai_ptr->ai_addr, ai_ptr->ai_addrlen);
252 if (status != 0)
253 {
254 char errbuf[1024];
255 WARNING ("teamspeak2 plugin: connect failed: %s",
256 sstrerror (errno, errbuf, sizeof (errbuf)));
257 close (sd);
258 sd = -1;
259 continue;
260 }
262 /*
263 * Success, we can break. Don't need more than one connection
264 */
265 break;
266 } /* for (ai_ptr) */
268 freeaddrinfo (ai_head);
270 /* Check if we really got connected */
271 if (sd < 0)
272 return (-1);
274 /* Create file objects from sockets */
275 global_read_fh = fdopen (sd, "r");
276 if (global_read_fh == NULL)
277 {
278 char errbuf[1024];
279 ERROR ("teamspeak2 plugin: fdopen failed: %s",
280 sstrerror (errno, errbuf, sizeof (errbuf)));
281 close (sd);
282 return (-1);
283 }
285 global_write_fh = fdopen (sd, "w");
286 if (global_write_fh == NULL)
287 {
288 char errbuf[1024];
289 ERROR ("teamspeak2 plugin: fdopen failed: %s",
290 sstrerror (errno, errbuf, sizeof (errbuf)));
291 tss2_close_socket ();
292 return (-1);
293 }
295 { /* Check that the server correctly identifies itself. */
296 char buffer[4096];
297 char *buffer_ptr;
299 buffer_ptr = fgets (buffer, sizeof (buffer), global_read_fh);
300 if (buffer_ptr == NULL)
301 {
302 WARNING ("teamspeak2 plugin: Unexpected EOF received "
303 "from remote host %s:%s.",
304 config_host ? config_host : DEFAULT_HOST,
305 config_port ? config_port : DEFAULT_PORT);
306 }
307 buffer[sizeof (buffer) - 1] = 0;
309 if (memcmp ("[TS]\r\n", buffer, 6) != 0)
310 {
311 ERROR ("teamspeak2 plugin: Unexpected response when connecting "
312 "to server. Expected ``[TS]'', got ``%s''.",
313 buffer);
314 tss2_close_socket ();
315 return (-1);
316 }
317 DEBUG ("teamspeak2 plugin: Server send correct banner, connected!");
318 }
320 /* Copy the new filehandles to the given pointers */
321 if (ret_read_fh != NULL)
322 *ret_read_fh = global_read_fh;
323 if (ret_write_fh != NULL)
324 *ret_write_fh = global_write_fh;
325 return (0);
326 } /* int tss2_get_socket */
328 static int tss2_send_request (FILE *fh, const char *request)
329 {
330 /*
331 * This function puts a request to the server socket
332 */
333 int status;
335 status = fputs (request, fh);
336 if (status < 0)
337 {
338 ERROR ("teamspeak2 plugin: fputs failed.");
339 tss2_close_socket ();
340 return (-1);
341 }
342 fflush (fh);
344 return (0);
345 } /* int tss2_send_request */
347 static int tss2_receive_line (FILE *fh, char *buffer, int buffer_size)
348 {
349 /*
350 * Receive a single line from the given file object
351 */
352 char *temp;
354 /*
355 * fgets is blocking but much easier then doing anything else
356 * TODO: Non-blocking Version would be safer
357 */
358 temp = fgets (buffer, buffer_size, fh);
359 if (temp == NULL)
360 {
361 char errbuf[1024];
362 ERROR ("teamspeak2 plugin: fgets failed: %s",
363 sstrerror (errno, errbuf, sizeof(errbuf)));
364 tss2_close_socket ();
365 return (-1);
366 }
368 buffer[buffer_size - 1] = 0;
369 return (0);
370 } /* int tss2_receive_line */
372 static int tss2_select_vserver (FILE *read_fh, FILE *write_fh, vserver_list_t *vserver)
373 {
374 /*
375 * Tell the server to select the given vserver
376 */
377 char command[128];
378 char response[128];
379 int status;
381 /* Send request */
382 ssnprintf (command, sizeof (command), "sel %i\r\n", vserver->port);
384 status = tss2_send_request (write_fh, command);
385 if (status != 0)
386 {
387 ERROR ("teamspeak2 plugin: tss2_send_request (%s) failed.", command);
388 return (-1);
389 }
391 /* Get answer */
392 status = tss2_receive_line (read_fh, response, sizeof (response));
393 if (status != 0)
394 {
395 ERROR ("teamspeak2 plugin: tss2_receive_line failed.");
396 return (-1);
397 }
398 response[sizeof (response) - 1] = 0;
400 /* Check answer */
401 if ((strncasecmp ("OK", response, 2) == 0)
402 && ((response[2] == 0)
403 || (response[2] == '\n')
404 || (response[2] == '\r')))
405 return (0);
407 ERROR ("teamspeak2 plugin: Command ``%s'' failed. "
408 "Response received from server was: ``%s''.",
409 command, response);
410 return (-1);
411 } /* int tss2_select_vserver */
413 static int tss2_vserver_gapl (FILE *read_fh, FILE *write_fh,
414 gauge_t *ret_value)
415 {
416 /*
417 * Reads the vserver's average packet loss and submits it to collectd.
418 * Be sure to run the tss2_read_vserver function before calling this so
419 * the vserver is selected correctly.
420 */
421 gauge_t packet_loss = NAN;
422 int status;
424 status = tss2_send_request (write_fh, "gapl\r\n");
425 if (status != 0)
426 {
427 ERROR("teamspeak2 plugin: tss2_send_request (gapl) failed.");
428 return (-1);
429 }
431 while (42)
432 {
433 char buffer[4096];
434 char *value;
435 char *endptr = NULL;
437 status = tss2_receive_line (read_fh, buffer, sizeof (buffer));
438 if (status != 0)
439 {
440 /* Set to NULL just to make sure no one uses these FHs anymore. */
441 read_fh = NULL;
442 write_fh = NULL;
443 ERROR ("teamspeak2 plugin: tss2_receive_line failed.");
444 return (-1);
445 }
446 buffer[sizeof (buffer) - 1] = 0;
448 if (strncmp ("average_packet_loss=", buffer,
449 strlen ("average_packet_loss=")) == 0)
450 {
451 /* Got average packet loss, now interpret it */
452 value = &buffer[20];
453 /* Replace , with . */
454 while (*value != 0)
455 {
456 if (*value == ',')
457 {
458 *value = '.';
459 break;
460 }
461 value++;
462 }
464 value = &buffer[20];
466 packet_loss = strtod (value, &endptr);
467 if (value == endptr)
468 {
469 /* Failed */
470 WARNING ("teamspeak2 plugin: Could not read average package "
471 "loss from string: %s", buffer);
472 continue;
473 }
474 }
475 else if (strncasecmp ("OK", buffer, 2) == 0)
476 {
477 break;
478 }
479 else if (strncasecmp ("ERROR", buffer, 5) == 0)
480 {
481 ERROR ("teamspeak2 plugin: Server returned an error: %s", buffer);
482 return (-1);
483 }
484 else
485 {
486 WARNING ("teamspeak2 plugin: Server returned unexpected string: %s",
487 buffer);
488 }
489 }
491 *ret_value = packet_loss;
492 return (0);
493 } /* int tss2_vserver_gapl */
495 static int tss2_read_vserver (vserver_list_t *vserver)
496 {
497 /*
498 * Poll information for the given vserver and submit it to collect.
499 * If vserver is NULL the global server information will be queried.
500 */
501 int status;
503 gauge_t users = NAN;
504 gauge_t channels = NAN;
505 gauge_t servers = NAN;
506 derive_t rx_octets = 0;
507 derive_t tx_octets = 0;
508 derive_t rx_packets = 0;
509 derive_t tx_packets = 0;
510 gauge_t packet_loss = NAN;
511 int valid = 0;
513 char plugin_instance[DATA_MAX_NAME_LEN] = { 0 };
515 FILE *read_fh;
516 FILE *write_fh;
518 /* Get the send/receive sockets */
519 status = tss2_get_socket (&read_fh, &write_fh);
520 if (status != 0)
521 {
522 ERROR ("teamspeak2 plugin: tss2_get_socket failed.");
523 return (-1);
524 }
526 if (vserver == NULL)
527 {
528 /* Request global information */
529 status = tss2_send_request (write_fh, "gi\r\n");
530 }
531 else
532 {
533 /* Request server information */
534 ssnprintf (plugin_instance, sizeof (plugin_instance), "vserver%i",
535 vserver->port);
537 /* Select the server */
538 status = tss2_select_vserver (read_fh, write_fh, vserver);
539 if (status != 0)
540 return (status);
542 status = tss2_send_request (write_fh, "si\r\n");
543 }
545 if (status != 0)
546 {
547 ERROR ("teamspeak2 plugin: tss2_send_request failed.");
548 return (-1);
549 }
551 /* Loop until break */
552 while (42)
553 {
554 char buffer[4096];
555 char *key;
556 char *value;
557 char *endptr = NULL;
559 /* Read one line of the server's answer */
560 status = tss2_receive_line (read_fh, buffer, sizeof (buffer));
561 if (status != 0)
562 {
563 /* Set to NULL just to make sure no one uses these FHs anymore. */
564 read_fh = NULL;
565 write_fh = NULL;
566 ERROR ("teamspeak2 plugin: tss2_receive_line failed.");
567 break;
568 }
570 if (strncasecmp ("ERROR", buffer, 5) == 0)
571 {
572 ERROR ("teamspeak2 plugin: Server returned an error: %s",
573 buffer);
574 break;
575 }
576 else if (strncasecmp ("OK", buffer, 2) == 0)
577 {
578 break;
579 }
581 /* Split line into key and value */
582 key = strchr (buffer, '_');
583 if (key == NULL)
584 {
585 DEBUG ("teamspeak2 plugin: Cannot parse line: %s", buffer);
586 continue;
587 }
588 key++;
590 /* Evaluate assignment */
591 value = strchr (key, '=');
592 if (value == NULL)
593 {
594 DEBUG ("teamspeak2 plugin: Cannot parse line: %s", buffer);
595 continue;
596 }
597 *value = 0;
598 value++;
600 /* Check for known key and save the given value */
601 /* global info: users_online,
602 * server info: currentusers. */
603 if ((strcmp ("currentusers", key) == 0)
604 || (strcmp ("users_online", key) == 0))
605 {
606 users = strtod (value, &endptr);
607 if (value != endptr)
608 valid |= 0x01;
609 }
610 /* global info: channels,
611 * server info: currentchannels. */
612 else if ((strcmp ("currentchannels", key) == 0)
613 || (strcmp ("channels", key) == 0))
614 {
615 channels = strtod (value, &endptr);
616 if (value != endptr)
617 valid |= 0x40;
618 }
619 /* global only */
620 else if (strcmp ("servers", key) == 0)
621 {
622 servers = strtod (value, &endptr);
623 if (value != endptr)
624 valid |= 0x80;
625 }
626 else if (strcmp ("bytesreceived", key) == 0)
627 {
628 rx_octets = strtoll (value, &endptr, 0);
629 if (value != endptr)
630 valid |= 0x02;
631 }
632 else if (strcmp ("bytessend", key) == 0)
633 {
634 tx_octets = strtoll (value, &endptr, 0);
635 if (value != endptr)
636 valid |= 0x04;
637 }
638 else if (strcmp ("packetsreceived", key) == 0)
639 {
640 rx_packets = strtoll (value, &endptr, 0);
641 if (value != endptr)
642 valid |= 0x08;
643 }
644 else if (strcmp ("packetssend", key) == 0)
645 {
646 tx_packets = strtoll (value, &endptr, 0);
647 if (value != endptr)
648 valid |= 0x10;
649 }
650 else if ((strncmp ("allow_codec_", key, strlen ("allow_codec_")) == 0)
651 || (strncmp ("bwinlast", key, strlen ("bwinlast")) == 0)
652 || (strncmp ("bwoutlast", key, strlen ("bwoutlast")) == 0)
653 || (strncmp ("webpost_", key, strlen ("webpost_")) == 0)
654 || (strcmp ("adminemail", key) == 0)
655 || (strcmp ("clan_server", key) == 0)
656 || (strcmp ("countrynumber", key) == 0)
657 || (strcmp ("id", key) == 0)
658 || (strcmp ("ispname", key) == 0)
659 || (strcmp ("linkurl", key) == 0)
660 || (strcmp ("maxusers", key) == 0)
661 || (strcmp ("name", key) == 0)
662 || (strcmp ("password", key) == 0)
663 || (strcmp ("platform", key) == 0)
664 || (strcmp ("server_platform", key) == 0)
665 || (strcmp ("server_uptime", key) == 0)
666 || (strcmp ("server_version", key) == 0)
667 || (strcmp ("udpport", key) == 0)
668 || (strcmp ("uptime", key) == 0)
669 || (strcmp ("users_maximal", key) == 0)
670 || (strcmp ("welcomemessage", key) == 0))
671 /* ignore */;
672 else
673 {
674 INFO ("teamspeak2 plugin: Unknown key-value-pair: "
675 "key = %s; value = %s;", key, value);
676 }
677 } /* while (42) */
679 /* Collect vserver packet loss rates only if the loop above did not exit
680 * with an error. */
681 if ((status == 0) && (vserver != NULL))
682 {
683 status = tss2_vserver_gapl (read_fh, write_fh, &packet_loss);
684 if (status == 0)
685 {
686 valid |= 0x20;
687 }
688 else
689 {
690 WARNING ("teamspeak2 plugin: Reading package loss "
691 "for vserver %i failed.", vserver->port);
692 }
693 }
695 if ((valid & 0x01) == 0x01)
696 tss2_submit_gauge (plugin_instance, "users", NULL, users);
698 if ((valid & 0x06) == 0x06)
699 tss2_submit_io (plugin_instance, "io_octets", rx_octets, tx_octets);
701 if ((valid & 0x18) == 0x18)
702 tss2_submit_io (plugin_instance, "io_packets", rx_packets, tx_packets);
704 if ((valid & 0x20) == 0x20)
705 tss2_submit_gauge (plugin_instance, "percent", "packet_loss", packet_loss);
707 if ((valid & 0x40) == 0x40)
708 tss2_submit_gauge (plugin_instance, "gauge", "channels", channels);
710 if ((valid & 0x80) == 0x80)
711 tss2_submit_gauge (plugin_instance, "gauge", "servers", servers);
713 if (valid == 0)
714 return (-1);
715 return (0);
716 } /* int tss2_read_vserver */
718 static int tss2_config (const char *key, const char *value)
719 {
720 /*
721 * Interpret configuration values
722 */
723 if (strcasecmp ("Host", key) == 0)
724 {
725 char *temp;
727 temp = strdup (value);
728 if (temp == NULL)
729 {
730 ERROR("teamspeak2 plugin: strdup failed.");
731 return (1);
732 }
733 sfree (config_host);
734 config_host = temp;
735 }
736 else if (strcasecmp ("Port", key) == 0)
737 {
738 char *temp;
740 temp = strdup (value);
741 if (temp == NULL)
742 {
743 ERROR("teamspeak2 plugin: strdup failed.");
744 return (1);
745 }
746 sfree (config_port);
747 config_port = temp;
748 }
749 else if (strcasecmp ("Server", key) == 0)
750 {
751 /* Server variable found */
752 int status;
754 status = tss2_add_vserver (atoi (value));
755 if (status != 0)
756 return (1);
757 }
758 else
759 {
760 /* Unknown variable found */
761 return (-1);
762 }
764 return 0;
765 } /* int tss2_config */
767 static int tss2_read (void)
768 {
769 /*
770 * Poll function which collects global and vserver information
771 * and submits it to collectd
772 */
773 vserver_list_t *vserver;
774 int success = 0;
775 int status;
777 /* Handle global server variables */
778 status = tss2_read_vserver (NULL);
779 if (status == 0)
780 {
781 success++;
782 }
783 else
784 {
785 WARNING ("teamspeak2 plugin: Reading global server variables failed.");
786 }
788 /* Handle vservers */
789 for (vserver = server_list; vserver != NULL; vserver = vserver->next)
790 {
791 status = tss2_read_vserver (vserver);
792 if (status == 0)
793 {
794 success++;
795 }
796 else
797 {
798 WARNING ("teamspeak2 plugin: Reading statistics "
799 "for vserver %i failed.", vserver->port);
800 continue;
801 }
802 }
804 if (success == 0)
805 return (-1);
806 return (0);
807 } /* int tss2_read */
809 static int tss2_shutdown(void)
810 {
811 /*
812 * Shutdown handler
813 */
814 vserver_list_t *entry;
816 tss2_close_socket ();
818 entry = server_list;
819 server_list = NULL;
820 while (entry != NULL)
821 {
822 vserver_list_t *next;
824 next = entry->next;
825 sfree (entry);
826 entry = next;
827 }
829 /* Get rid of the configuration */
830 sfree (config_host);
831 sfree (config_port);
833 return (0);
834 } /* int tss2_shutdown */
836 void module_register(void)
837 {
838 /*
839 * Mandatory module_register function
840 */
841 plugin_register_config ("teamspeak2", tss2_config,
842 config_keys, config_keys_num);
843 plugin_register_read ("teamspeak2", tss2_read);
844 plugin_register_shutdown ("teamspeak2", tss2_shutdown);
845 } /* void module_register */
847 /* vim: set sw=4 ts=4 : */