/** * collectd - src/teamspeak2.c * Copyright (C) 2008 Stefan Hacker * Copyright (C) 2008 Florian Forster * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; only version 2 of the License is applicable. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * Authors: * Stefan Hacker * Florian Forster **/ #include "collectd.h" #include "plugin.h" #include "utils/common/common.h" #include #include #include #include /* * Defines */ /* Default host and port */ #define DEFAULT_HOST "127.0.0.1" #define DEFAULT_PORT "51234" /* * Variables */ /* Server linked list structure */ typedef struct vserver_list_s { int port; struct vserver_list_s *next; } vserver_list_t; static vserver_list_t *server_list; /* Host data */ static char *config_host; static char *config_port; static FILE *global_read_fh; static FILE *global_write_fh; /* Config data */ static const char *config_keys[] = {"Host", "Port", "Server"}; static int config_keys_num = STATIC_ARRAY_SIZE(config_keys); /* * Functions */ static int tss2_add_vserver(int vserver_port) { /* * Adds a new vserver to the linked list */ vserver_list_t *entry; /* Check port range */ if ((vserver_port <= 0) || (vserver_port > 65535)) { ERROR("teamspeak2 plugin: VServer port is invalid: %i", vserver_port); return -1; } /* Allocate memory */ entry = calloc(1, sizeof(*entry)); if (entry == NULL) { ERROR("teamspeak2 plugin: calloc failed."); return -1; } /* Save data */ entry->port = vserver_port; /* Insert to list */ if (server_list == NULL) { /* Add the server as the first element */ server_list = entry; } else { vserver_list_t *prev; /* Add the server to the end of the list */ prev = server_list; while (prev->next != NULL) prev = prev->next; prev->next = entry; } INFO("teamspeak2 plugin: Registered new vserver: %i", vserver_port); return 0; } /* int tss2_add_vserver */ static void tss2_submit_gauge(const char *plugin_instance, const char *type, const char *type_instance, gauge_t value) { /* * Submits a gauge value to the collectd daemon */ value_list_t vl = VALUE_LIST_INIT; vl.values = &(value_t){.gauge = value}; vl.values_len = 1; sstrncpy(vl.plugin, "teamspeak2", sizeof(vl.plugin)); if (plugin_instance != NULL) sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance)); sstrncpy(vl.type, type, sizeof(vl.type)); if (type_instance != NULL) sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance)); plugin_dispatch_values(&vl); } /* void tss2_submit_gauge */ static void tss2_submit_io(const char *plugin_instance, const char *type, derive_t rx, derive_t tx) { /* * Submits the io rx/tx tuple to the collectd daemon */ value_list_t vl = VALUE_LIST_INIT; value_t values[] = { {.derive = rx}, {.derive = tx}, }; vl.values = values; vl.values_len = STATIC_ARRAY_SIZE(values); sstrncpy(vl.plugin, "teamspeak2", sizeof(vl.plugin)); if (plugin_instance != NULL) sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance)); sstrncpy(vl.type, type, sizeof(vl.type)); plugin_dispatch_values(&vl); } /* void tss2_submit_gauge */ static void tss2_close_socket(void) { /* * Closes all sockets */ if (global_write_fh != NULL) { fputs("quit\r\n", global_write_fh); } if (global_read_fh != NULL) { fclose(global_read_fh); global_read_fh = NULL; } if (global_write_fh != NULL) { fclose(global_write_fh); global_write_fh = NULL; } } /* void tss2_close_socket */ static int tss2_get_socket(FILE **ret_read_fh, FILE **ret_write_fh) { /* * Returns connected file objects or establishes the connection * if it's not already present */ struct addrinfo *ai_head; int sd = -1; int status; /* Check if we already got opened connections */ if ((global_read_fh != NULL) && (global_write_fh != NULL)) { /* If so, use them */ if (ret_read_fh != NULL) *ret_read_fh = global_read_fh; if (ret_write_fh != NULL) *ret_write_fh = global_write_fh; return 0; } /* Get all addrs for this hostname */ struct addrinfo ai_hints = {.ai_family = AF_UNSPEC, .ai_flags = AI_ADDRCONFIG, .ai_socktype = SOCK_STREAM}; status = getaddrinfo((config_host != NULL) ? config_host : DEFAULT_HOST, (config_port != NULL) ? config_port : DEFAULT_PORT, &ai_hints, &ai_head); if (status != 0) { ERROR("teamspeak2 plugin: getaddrinfo failed: %s", gai_strerror(status)); return -1; } /* Try all given hosts until we can connect to one */ for (struct addrinfo *ai_ptr = ai_head; ai_ptr != NULL; ai_ptr = ai_ptr->ai_next) { /* Create socket */ sd = socket(ai_ptr->ai_family, ai_ptr->ai_socktype, ai_ptr->ai_protocol); if (sd < 0) { WARNING("teamspeak2 plugin: socket failed: %s", STRERRNO); continue; } /* Try to connect */ status = connect(sd, ai_ptr->ai_addr, ai_ptr->ai_addrlen); if (status != 0) { WARNING("teamspeak2 plugin: connect failed: %s", STRERRNO); close(sd); sd = -1; continue; } /* * Success, we can break. Don't need more than one connection */ break; } /* for (ai_ptr) */ freeaddrinfo(ai_head); /* Check if we really got connected */ if (sd < 0) return -1; /* Create file objects from sockets */ global_read_fh = fdopen(sd, "r"); if (global_read_fh == NULL) { ERROR("teamspeak2 plugin: fdopen failed: %s", STRERRNO); close(sd); return -1; } global_write_fh = fdopen(sd, "w"); if (global_write_fh == NULL) { ERROR("teamspeak2 plugin: fdopen failed: %s", STRERRNO); tss2_close_socket(); return -1; } { /* Check that the server correctly identifies itself. */ char buffer[4096]; char *buffer_ptr; buffer_ptr = fgets(buffer, sizeof(buffer), global_read_fh); if (buffer_ptr == NULL) { WARNING("teamspeak2 plugin: Unexpected EOF received " "from remote host %s:%s.", config_host ? config_host : DEFAULT_HOST, config_port ? config_port : DEFAULT_PORT); } buffer[sizeof(buffer) - 1] = '\0'; if (memcmp("[TS]\r\n", buffer, 6) != 0) { ERROR("teamspeak2 plugin: Unexpected response when connecting " "to server. Expected ``[TS]'', got ``%s''.", buffer); tss2_close_socket(); return -1; } DEBUG("teamspeak2 plugin: Server send correct banner, connected!"); } /* Copy the new filehandles to the given pointers */ if (ret_read_fh != NULL) *ret_read_fh = global_read_fh; if (ret_write_fh != NULL) *ret_write_fh = global_write_fh; return 0; } /* int tss2_get_socket */ static int tss2_send_request(FILE *fh, const char *request) { /* * This function puts a request to the server socket */ int status; status = fputs(request, fh); if (status < 0) { ERROR("teamspeak2 plugin: fputs failed."); tss2_close_socket(); return -1; } fflush(fh); return 0; } /* int tss2_send_request */ static int tss2_receive_line(FILE *fh, char *buffer, int buffer_size) { /* * Receive a single line from the given file object */ char *temp; /* * fgets is blocking but much easier then doing anything else * TODO: Non-blocking Version would be safer */ temp = fgets(buffer, buffer_size, fh); if (temp == NULL) { ERROR("teamspeak2 plugin: fgets failed: %s", STRERRNO); tss2_close_socket(); return -1; } buffer[buffer_size - 1] = '\0'; return 0; } /* int tss2_receive_line */ static int tss2_select_vserver(FILE *read_fh, FILE *write_fh, vserver_list_t *vserver) { /* * Tell the server to select the given vserver */ char command[128]; char response[128]; int status; /* Send request */ snprintf(command, sizeof(command), "sel %i\r\n", vserver->port); status = tss2_send_request(write_fh, command); if (status != 0) { ERROR("teamspeak2 plugin: tss2_send_request (%s) failed.", command); return -1; } /* Get answer */ status = tss2_receive_line(read_fh, response, sizeof(response)); if (status != 0) { ERROR("teamspeak2 plugin: tss2_receive_line failed."); return -1; } response[sizeof(response) - 1] = '\0'; /* Check answer */ if ((strncasecmp("OK", response, 2) == 0) && ((response[2] == 0) || (response[2] == '\n') || (response[2] == '\r'))) return 0; ERROR("teamspeak2 plugin: Command ``%s'' failed. " "Response received from server was: ``%s''.", command, response); return -1; } /* int tss2_select_vserver */ static int tss2_vserver_gapl(FILE *read_fh, FILE *write_fh, gauge_t *ret_value) { /* * Reads the vserver's average packet loss and submits it to collectd. * Be sure to run the tss2_read_vserver function before calling this so * the vserver is selected correctly. */ gauge_t packet_loss = NAN; int status; status = tss2_send_request(write_fh, "gapl\r\n"); if (status != 0) { ERROR("teamspeak2 plugin: tss2_send_request (gapl) failed."); return -1; } while (42) { char buffer[4096]; char *value; char *endptr = NULL; status = tss2_receive_line(read_fh, buffer, sizeof(buffer)); if (status != 0) { /* Set to NULL just to make sure no one uses these FHs anymore. */ read_fh = NULL; write_fh = NULL; ERROR("teamspeak2 plugin: tss2_receive_line failed."); return -1; } buffer[sizeof(buffer) - 1] = '\0'; if (strncmp("average_packet_loss=", buffer, strlen("average_packet_loss=")) == 0) { /* Got average packet loss, now interpret it */ value = &buffer[20]; /* Replace , with . */ while (*value != 0) { if (*value == ',') { *value = '.'; break; } value++; } value = &buffer[20]; packet_loss = strtod(value, &endptr); if (value == endptr) { /* Failed */ WARNING("teamspeak2 plugin: Could not read average package " "loss from string: %s", buffer); continue; } } else if (strncasecmp("OK", buffer, 2) == 0) { break; } else if (strncasecmp("ERROR", buffer, 5) == 0) { ERROR("teamspeak2 plugin: Server returned an error: %s", buffer); return -1; } else { WARNING("teamspeak2 plugin: Server returned unexpected string: %s", buffer); } } *ret_value = packet_loss; return 0; } /* int tss2_vserver_gapl */ static int tss2_read_vserver(vserver_list_t *vserver) { /* * Poll information for the given vserver and submit it to collect. * If vserver is NULL the global server information will be queried. */ int status; gauge_t users = NAN; gauge_t channels = NAN; gauge_t servers = NAN; derive_t rx_octets = 0; derive_t tx_octets = 0; derive_t rx_packets = 0; derive_t tx_packets = 0; gauge_t packet_loss = NAN; int valid = 0; char plugin_instance[DATA_MAX_NAME_LEN] = {0}; FILE *read_fh; FILE *write_fh; /* Get the send/receive sockets */ status = tss2_get_socket(&read_fh, &write_fh); if (status != 0) { ERROR("teamspeak2 plugin: tss2_get_socket failed."); return -1; } if (vserver == NULL) { /* Request global information */ status = tss2_send_request(write_fh, "gi\r\n"); } else { /* Request server information */ snprintf(plugin_instance, sizeof(plugin_instance), "vserver%i", vserver->port); /* Select the server */ status = tss2_select_vserver(read_fh, write_fh, vserver); if (status != 0) return status; status = tss2_send_request(write_fh, "si\r\n"); } if (status != 0) { ERROR("teamspeak2 plugin: tss2_send_request failed."); return -1; } /* Loop until break */ while (42) { char buffer[4096]; char *key; char *value; char *endptr = NULL; /* Read one line of the server's answer */ status = tss2_receive_line(read_fh, buffer, sizeof(buffer)); if (status != 0) { /* Set to NULL just to make sure no one uses these FHs anymore. */ read_fh = NULL; write_fh = NULL; ERROR("teamspeak2 plugin: tss2_receive_line failed."); break; } if (strncasecmp("ERROR", buffer, 5) == 0) { ERROR("teamspeak2 plugin: Server returned an error: %s", buffer); break; } else if (strncasecmp("OK", buffer, 2) == 0) { break; } /* Split line into key and value */ key = strchr(buffer, '_'); if (key == NULL) { DEBUG("teamspeak2 plugin: Cannot parse line: %s", buffer); continue; } key++; /* Evaluate assignment */ value = strchr(key, '='); if (value == NULL) { DEBUG("teamspeak2 plugin: Cannot parse line: %s", buffer); continue; } *value = 0; value++; /* Check for known key and save the given value */ /* global info: users_online, * server info: currentusers. */ if ((strcmp("currentusers", key) == 0) || (strcmp("users_online", key) == 0)) { users = strtod(value, &endptr); if (value != endptr) valid |= 0x01; } /* global info: channels, * server info: currentchannels. */ else if ((strcmp("currentchannels", key) == 0) || (strcmp("channels", key) == 0)) { channels = strtod(value, &endptr); if (value != endptr) valid |= 0x40; } /* global only */ else if (strcmp("servers", key) == 0) { servers = strtod(value, &endptr); if (value != endptr) valid |= 0x80; } else if (strcmp("bytesreceived", key) == 0) { rx_octets = strtoll(value, &endptr, 0); if (value != endptr) valid |= 0x02; } else if (strcmp("bytessend", key) == 0) { tx_octets = strtoll(value, &endptr, 0); if (value != endptr) valid |= 0x04; } else if (strcmp("packetsreceived", key) == 0) { rx_packets = strtoll(value, &endptr, 0); if (value != endptr) valid |= 0x08; } else if (strcmp("packetssend", key) == 0) { tx_packets = strtoll(value, &endptr, 0); if (value != endptr) valid |= 0x10; } else if ((strncmp("allow_codec_", key, strlen("allow_codec_")) == 0) || (strncmp("bwinlast", key, strlen("bwinlast")) == 0) || (strncmp("bwoutlast", key, strlen("bwoutlast")) == 0) || (strncmp("webpost_", key, strlen("webpost_")) == 0) || (strcmp("adminemail", key) == 0) || (strcmp("clan_server", key) == 0) || (strcmp("countrynumber", key) == 0) || (strcmp("id", key) == 0) || (strcmp("ispname", key) == 0) || (strcmp("linkurl", key) == 0) || (strcmp("maxusers", key) == 0) || (strcmp("name", key) == 0) || (strcmp("password", key) == 0) || (strcmp("platform", key) == 0) || (strcmp("server_platform", key) == 0) || (strcmp("server_uptime", key) == 0) || (strcmp("server_version", key) == 0) || (strcmp("udpport", key) == 0) || (strcmp("uptime", key) == 0) || (strcmp("users_maximal", key) == 0) || (strcmp("welcomemessage", key) == 0)) /* ignore */; else { INFO("teamspeak2 plugin: Unknown key-value-pair: " "key = %s; value = %s;", key, value); } } /* while (42) */ /* Collect vserver packet loss rates only if the loop above did not exit * with an error. */ if ((status == 0) && (vserver != NULL)) { status = tss2_vserver_gapl(read_fh, write_fh, &packet_loss); if (status == 0) { valid |= 0x20; } else { WARNING("teamspeak2 plugin: Reading package loss " "for vserver %i failed.", vserver->port); } } if ((valid & 0x01) == 0x01) tss2_submit_gauge(plugin_instance, "users", NULL, users); if ((valid & 0x06) == 0x06) tss2_submit_io(plugin_instance, "io_octets", rx_octets, tx_octets); if ((valid & 0x18) == 0x18) tss2_submit_io(plugin_instance, "io_packets", rx_packets, tx_packets); if ((valid & 0x20) == 0x20) tss2_submit_gauge(plugin_instance, "percent", "packet_loss", packet_loss); if ((valid & 0x40) == 0x40) tss2_submit_gauge(plugin_instance, "gauge", "channels", channels); if ((valid & 0x80) == 0x80) tss2_submit_gauge(plugin_instance, "gauge", "servers", servers); if (valid == 0) return -1; return 0; } /* int tss2_read_vserver */ static int tss2_config(const char *key, const char *value) { /* * Interpret configuration values */ if (strcasecmp("Host", key) == 0) { char *temp; temp = strdup(value); if (temp == NULL) { ERROR("teamspeak2 plugin: strdup failed."); return 1; } sfree(config_host); config_host = temp; } else if (strcasecmp("Port", key) == 0) { char *temp; temp = strdup(value); if (temp == NULL) { ERROR("teamspeak2 plugin: strdup failed."); return 1; } sfree(config_port); config_port = temp; } else if (strcasecmp("Server", key) == 0) { /* Server variable found */ int status; status = tss2_add_vserver(atoi(value)); if (status != 0) return 1; } else { /* Unknown variable found */ return -1; } return 0; } /* int tss2_config */ static int tss2_read(void) { /* * Poll function which collects global and vserver information * and submits it to collectd */ int success = 0; int status; /* Handle global server variables */ status = tss2_read_vserver(NULL); if (status == 0) { success++; } else { WARNING("teamspeak2 plugin: Reading global server variables failed."); } /* Handle vservers */ for (vserver_list_t *vserver = server_list; vserver != NULL; vserver = vserver->next) { status = tss2_read_vserver(vserver); if (status == 0) { success++; } else { WARNING("teamspeak2 plugin: Reading statistics " "for vserver %i failed.", vserver->port); continue; } } if (success == 0) return -1; return 0; } /* int tss2_read */ static int tss2_shutdown(void) { /* * Shutdown handler */ vserver_list_t *entry; tss2_close_socket(); entry = server_list; server_list = NULL; while (entry != NULL) { vserver_list_t *next; next = entry->next; sfree(entry); entry = next; } /* Get rid of the configuration */ sfree(config_host); sfree(config_port); return 0; } /* int tss2_shutdown */ void module_register(void) { /* * Mandatory module_register function */ plugin_register_config("teamspeak2", tss2_config, config_keys, config_keys_num); plugin_register_read("teamspeak2", tss2_read); plugin_register_shutdown("teamspeak2", tss2_shutdown); } /* void module_register */