/* * static char *rcsid_metaserver_c = * "$Id: metaserver.c 11578 2009-02-23 22:02:27Z lalo $"; */ /* CrossFire, A Multiplayer game for X-windows Copyright (C) 2002,2007 Mark Wedel & Crossfire Development Team Copyright (C) 1992 Frank Tore Johansen 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; either version 2 of the License, or (at your option) any later version. 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., 675 Mass Ave, Cambridge, MA 02139, USA. The authors can be reached via e-mail at crossfire-devel@real-time.com */ /** * \file * \date 2003-12-02 * Meta-server related functions. */ #include #ifndef WIN32 /* ---win32 exclude unix header files */ #include #include #include #include #include #endif /* end win32 */ #include #include #include #ifdef HAVE_CURL_CURL_H #include #include #endif static int metafd = -1; static struct sockaddr_in sock; /** * Connects to metaserver. * * Its only called once. If we are not * trying to contact the metaserver of the connection attempt fails, metafd will be * set to -1. We use this instead of messing with the settings.meta_on so that * that can be examined to at least see what the user was trying to do. */ void metaserver_init(void) { #ifdef WIN32 /* ***win32 metaserver_init(): init win32 socket */ struct hostent *hostbn; int temp = 1; #endif if (!settings.meta_on) { metafd = -1; return; } if (isdigit(settings.meta_server[0])) sock.sin_addr.s_addr = inet_addr(settings.meta_server); else { struct hostent *hostbn = gethostbyname(settings.meta_server); if (hostbn == NULL) { LOG(llevDebug, "metaserver_init: Unable to resolve hostname %s\n", settings.meta_server); return; } memcpy(&sock.sin_addr, hostbn->h_addr, hostbn->h_length); } #ifdef WIN32 /* ***win32 metaserver_init(): init win32 socket */ ioctlsocket(metafd, FIONBIO , &temp); #else fcntl(metafd, F_SETFL, O_NONBLOCK); #endif if ((metafd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { LOG(llevDebug, "metaserver_init: Unable to create socket, err %d\n", errno); return; } sock.sin_family = AF_INET; sock.sin_port = htons(settings.meta_port); /* No hostname specified, so lets try to figure one out */ if (settings.meta_host[0] == 0) { char hostname[MAX_BUF], domain[MAX_BUF]; if (gethostname(hostname, MAX_BUF-1)) { LOG(llevDebug, "metaserver_init: gethostname failed - will not report hostname\n"); return; } #ifdef WIN32 /* ***win32 metaserver_init(): gethostbyname! */ hostbn = gethostbyname(hostname); if (hostbn != (struct hostent *)NULL) /* quick hack */ memcpy(domain, hostbn->h_addr, hostbn->h_length); if (hostbn == (struct hostent *)NULL) { #else if (getdomainname(domain, MAX_BUF-1)) { #endif /* win32 */ LOG(llevDebug, "metaserver_init: getdomainname failed - will not report hostname\n"); return; } /* Potential overrun here but unlikely to occur */ sprintf(settings.meta_host, "%s.%s", hostname, domain); } } /** * Updates our info in the metaserver * Note that this is used for both metaserver1 and metaserver2 - * for metaserver2, it just copies dynamic data into private * data structure, doing locking in the process. */ void metaserver_update(void) { char data[MAX_BUF], num_players = 0; player *pl; /* We could use socket_info.nconns, but that is not quite as accurate, * as connections in the progress of being established, are listening * but don't have a player, etc. The checks below are basically the * same as for the who commands with the addition that WIZ, AFK, and BOT * players are not counted. */ for (pl = first_player; pl != NULL; pl = pl->next) { if (pl->ob->map == NULL) continue; if (pl->hidden) continue; if (QUERY_FLAG(pl->ob, FLAG_WIZ)) continue; if (QUERY_FLAG(pl->ob, FLAG_AFK)) continue; if (pl->state != ST_PLAYING && pl->state != ST_GET_PARTY_PASSWORD) continue; if (pl->socket.is_bot) continue; num_players++; } /* Only do this if we have a valid connection */ if (metafd != -1) { snprintf(data, sizeof(data), "%s|%d|%s|%s|%d|%d|%ld", settings.meta_host, num_players, FULL_VERSION, settings.meta_comment, cst_tot.ibytes, cst_tot.obytes, (long)time(NULL)-cst_tot.time_start); if (sendto(metafd, data, strlen(data), 0, (struct sockaddr *)&sock, sizeof(sock)) < 0) { LOG(llevDebug, "metaserver_update: sendto failed, err = %d\n", errno); } } /* Everything inside the pthread lock/unlock is related * to metaserver2 synchronization. */ pthread_mutex_lock(&ms2_info_mutex); metaserver2_updateinfo.num_players = num_players; metaserver2_updateinfo.in_bytes = cst_tot.ibytes; metaserver2_updateinfo.out_bytes = cst_tot.obytes; metaserver2_updateinfo.uptime = (long)time(NULL)-cst_tot.time_start; pthread_mutex_unlock(&ms2_info_mutex); } /** * Start of metaserver2 logic * Note: All static structures in this file should be treated as strictly * private. The metaserver2 update logic runs in its own thread, * so if something needs to modify these structures, proper locking * is needed. */ /** * This is a linked list of all the metaservers - * never really know how many we have. */ typedef struct _MetaServer2 { char *hostname; struct _MetaServer2 *next; } MetaServer2; static MetaServer2 *metaserver2; /** * LocalMeta2Info basically holds all the non metaserver2 information * that we read from the metaserver2 file. Could just have * individual variables, but doing it as a structure is cleaner * I think, and might be more flexible if in the future, we * want to pass this information to other functions (like plugin * or something) */ typedef struct _LocalMeta2Info { int notification; /* if true, do updates to metaservers */ char *hostname; /* Hostname of this server */ int portnumber; /* Portnumber of this server */ char *html_comment; /* html comment to send to metaservers */ char *text_comment; /* text comment to send to metaservers */ char *archbase; /* Different sources for arches, maps */ char *mapbase; /* and server */ char *codebase; char *flags; /* Short flags to send to metaserver */ } LocalMeta2Info; static LocalMeta2Info local_info; /* These two are globals, but we declare them here. */ pthread_mutex_t ms2_info_mutex; MetaServer2_UpdateInfo metaserver2_updateinfo; /** * This frees any data associated with the MetaServer2 info, * including the pointer itself. Caller is responsible for updating * pointers (ms->next) - really only used when wanting to free * all data. */ static void free_metaserver2(MetaServer2 *ms) { free(ms->hostname); free(ms); } /** * This initializes the metaserver2 logic - it reads * the metaserver2 file, storing the values * away. Note that it may be possible/desirable for the * server to re-read the values and restart connections (for example, * a new metaserver has been added and you want to start updates to * it immediately and not restart the server). Because of that, * there is some extra logic (has_init) to try to take that * into account. * * @return * 1 if we will be updating the metaserver, 0 if no * metaserver updates */ int metaserver2_init(void) { static int has_init = 0; FILE *fp; char buf[MAX_BUF], *cp; MetaServer2 *ms2, *msnext; int comp; pthread_t thread_id; #ifdef HAVE_CURL_CURL_H if (!has_init) { memset(&local_info, 0, sizeof(LocalMeta2Info)); memset(&metaserver2_updateinfo, 0, sizeof(MetaServer2_UpdateInfo)); local_info.portnumber = settings.csport; metaserver2 = NULL; pthread_mutex_init(&ms2_info_mutex, NULL); curl_global_init(CURL_GLOBAL_ALL); } else { local_info.notification = 0; if (local_info.hostname) FREE_AND_CLEAR(local_info.hostname); if (local_info.html_comment) FREE_AND_CLEAR(local_info.html_comment); if (local_info.text_comment) FREE_AND_CLEAR(local_info.text_comment); if (local_info.archbase) FREE_AND_CLEAR(local_info.archbase); if (local_info.mapbase) FREE_AND_CLEAR(local_info.mapbase); if (local_info.codebase) FREE_AND_CLEAR(local_info.codebase); if (local_info.flags) FREE_AND_CLEAR(local_info.flags); for (ms2 = metaserver2; ms2; ms2 = msnext) { msnext = ms2->next; free_metaserver2(ms2); } metaserver2 = NULL; } #endif /* Now load up the values from the file */ snprintf(buf, sizeof(buf), "%s/metaserver2", settings.confdir); if ((fp = open_and_uncompress(buf, 0, &comp)) == NULL) { LOG(llevError, "Warning: No metaserver2 file found\n"); return 0; } while (fgets(buf, MAX_BUF-1, fp) != NULL) { if (buf[0] == '#') continue; /* eliminate newline */ if ((cp = strrchr(buf, '\n')) != NULL) *cp = '\0'; /* Skip over empty lines */ if (buf[0] == 0) continue; /* Find variable pairs */ if ((cp = strpbrk(buf, " \t")) != NULL) { while (isspace(*cp)) *cp++ = 0; } else { /* This makes it so we don't have to do NULL checks against * cp everyplace */ cp = ""; } if (!strcasecmp(buf, "metaserver2_notification")) { if (!strcasecmp(cp, "on") || !strcasecmp(cp, "true")) { local_info.notification = TRUE; } else if (!strcasecmp(cp, "off") || !strcasecmp(cp, "false")) { local_info.notification = FALSE; } else { LOG(llevError, "metaserver2: Unknown value for metaserver2_notification: %s\n", cp); } } else if (!strcasecmp(buf, "metaserver2_server")) { if (*cp != 0) { ms2 = calloc(1, sizeof(MetaServer2)); ms2->hostname = strdup(cp); ms2->next = metaserver2; metaserver2 = ms2; } else { LOG(llevError, "metaserver2: metaserver2_server must have a value.\n"); } } else if (!strcasecmp(buf, "localhostname")) { if (*cp != 0) { local_info.hostname = strdup_local(cp); } else { LOG(llevError, "metaserver2: localhostname must have a value.\n"); } } else if (!strcasecmp(buf, "portnumber")) { if (*cp != 0) { local_info.portnumber = atoi(cp); } else { LOG(llevError, "metaserver2: portnumber must have a value.\n"); } /* For the following values, it is easier to make sure * the pointers are set to something, even if it is a blank * string, so don't care if there is data in the string or not. */ } else if (!strcasecmp(buf, "html_comment")) { local_info.html_comment = strdup(cp); } else if (!strcasecmp(buf, "text_comment")) { local_info.text_comment = strdup(cp); } else if (!strcasecmp(buf, "archbase")) { local_info.archbase = strdup(cp); } else if (!strcasecmp(buf, "mapbase")) { local_info.mapbase = strdup(cp); } else if (!strcasecmp(buf, "codebase")) { local_info.codebase = strdup(cp); } else if (!strcasecmp(buf, "flags")) { local_info.flags = strdup(cp); } else { LOG(llevError, "Unknown value in metaserver2 file: %s\n", buf); } } close_and_delete(fp, comp); /* If no hostname is set, can't do updates */ if (!local_info.hostname) local_info.notification = 0; #ifndef HAVE_CURL_CURL_H if (local_info.notification) { LOG(llevError, "metaserver2 file is set to do notification, but libcurl is not found.\n"); LOG(llevError, "Either fix your compilation, or turn of metaserver2 notification in \n"); LOG(llevError, "the %s/metaserver2 file.\n", settings.confdir); LOG(llevError, "Exiting program.\n"); exit(1); } #endif if (local_info.notification) { /* As noted above, it is much easier for the rest of the code * to not have to check for null pointers. So we do that * here, and anything that is null, we just allocate * an empty string. */ if (!local_info.html_comment) local_info.html_comment = strdup(""); if (!local_info.text_comment) local_info.text_comment = strdup(""); if (!local_info.archbase) local_info.archbase = strdup(""); if (!local_info.mapbase) local_info.mapbase = strdup(""); if (!local_info.codebase) local_info.codebase = strdup(""); if (!local_info.flags) local_info.flags = strdup(""); comp = pthread_create(&thread_id, NULL, metaserver2_thread, NULL); if (comp) { LOG(llevError, "metaserver2_init: return code from pthread_create() is %d\n", comp); /* Effectively true - we're not going to update the metaserver */ local_info.notification = 0; } } return local_info.notification; } /** * Handles writing of HTTP request data from the metaserver2. * We treat the data as a string. We should really pay attention to the * header data, and do something clever if we get 404 codes * or the like. */ static size_t metaserver2_writer(void *ptr, size_t size, size_t nmemb, void *data) { size_t realsize = size*nmemb; LOG(llevDebug, "metaserver2_writer- Start of text:\n%s\n", (const char*)ptr); LOG(llevDebug, "metaserver2_writer- End of text:\n"); return realsize; } /** * This sends an update to the various metaservers. * It generates the form, and then sends it to the * server */ static void metaserver2_updates(void) { #ifdef HAVE_CURL_CURL_H MetaServer2 *ms2; struct curl_httppost *formpost = NULL; struct curl_httppost *lastptr = NULL; char buf[MAX_BUF]; /* First, fill in the form - note that everything has to be a string, * so we convert as needed with snprintf. * The order of fields here really isn't important. * The string after CURLFORM_COPYNAME is the name of the POST variable * as the */ curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "hostname", CURLFORM_COPYCONTENTS, local_info.hostname, CURLFORM_END); snprintf(buf, MAX_BUF-1, "%d", local_info.portnumber); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "port", CURLFORM_COPYCONTENTS, buf, CURLFORM_END); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "html_comment", CURLFORM_COPYCONTENTS, local_info.html_comment, CURLFORM_END); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "text_comment", CURLFORM_COPYCONTENTS, local_info.text_comment, CURLFORM_END); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "archbase", CURLFORM_COPYCONTENTS, local_info.archbase, CURLFORM_END); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "mapbase", CURLFORM_COPYCONTENTS, local_info.mapbase, CURLFORM_END); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "codebase", CURLFORM_COPYCONTENTS, local_info.codebase, CURLFORM_END); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "flags", CURLFORM_COPYCONTENTS, local_info.flags, CURLFORM_END); pthread_mutex_lock(&ms2_info_mutex); snprintf(buf, MAX_BUF-1, "%d", metaserver2_updateinfo.num_players); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "num_players", CURLFORM_COPYCONTENTS, buf, CURLFORM_END); snprintf(buf, MAX_BUF-1, "%d", metaserver2_updateinfo.in_bytes); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "in_bytes", CURLFORM_COPYCONTENTS, buf, CURLFORM_END); snprintf(buf, MAX_BUF-1, "%d", metaserver2_updateinfo.out_bytes); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "out_bytes", CURLFORM_COPYCONTENTS, buf, CURLFORM_END); snprintf(buf, MAX_BUF-1, "%ld", metaserver2_updateinfo.uptime); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "uptime", CURLFORM_COPYCONTENTS, buf, CURLFORM_END); pthread_mutex_unlock(&ms2_info_mutex); /* Following few fields are global variables, * but are really defines, so won't ever change. */ curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "version", CURLFORM_COPYCONTENTS, FULL_VERSION, CURLFORM_END); snprintf(buf, MAX_BUF-1, "%d", VERSION_SC); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "sc_version", CURLFORM_COPYCONTENTS, buf, CURLFORM_END); snprintf(buf, MAX_BUF-1, "%d", VERSION_CS); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "cs_version", CURLFORM_COPYCONTENTS, buf, CURLFORM_END); for (ms2 = metaserver2; ms2; ms2 = ms2->next) { CURL *curl; CURLcode res; curl = curl_easy_init(); if (curl) { /* what URL that receives this POST */ curl_easy_setopt(curl, CURLOPT_URL, ms2->hostname); curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost); /* Almost always, we will get HTTP data returned * to us - instead of it going to stderr, * we want to take care of it ourselves. */ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, metaserver2_writer); res = curl_easy_perform(curl); if (res) fprintf(stderr, "easy_perform got error %d\n", res); /* always cleanup */ curl_easy_cleanup(curl); } } /* then cleanup the formpost chain */ curl_formfree(formpost); #endif } /** * metserver2_thread is the function called from pthread_create. * it is a trivial function - it just sleeps and calls * the update function. The sleep time here is really * quite arbitrary, but once a minute is probably often * enough. A better approach might be to * do a time() call and see how long the update takes, * and sleep according to that. * * @return * This function should never return/exit. */ void *metaserver2_thread(void *junk) { while (1) { metaserver2_updates(); sleep(60); } }