/* metrics,c routines for sending client details to external server */ #include #include #include #include #include #include #include "bandwidth.h" #include "metrics.h" #include "protocol_shoutcastClient.h" #include "stats.h" #include "config.h" #include "services/stdServiceImpl.h" #include "file/fileUtils.h" #include "webNet/urlUtils.h" #include "aolxml/aolxml.h" using namespace std; using namespace uniString; using namespace stringUtil; #define LOGNAME "METRICS" #define DEBUG_LOG(...) do { if (gOptions.adMetricsDebug()) DLOG(__VA_ARGS__); } while (0) namespace metrics { #if 0 #define METRICS_LICENCE_URL "http://www.google.com" #define LICENCE_RESP "\ \ \ \ \ \ \ \ \ " #else #define METRICS_LICENCE_URL "https://dnas-licensing.shoutcast.com/registration/" #endif class service; struct metrics_info { httpHeaderMap_t vars; utf8 url; size_t id; unsigned int match; streamData::streamID_t sid; int group; int mode; metrics_info() : id(0), sid(0), group(0), mode(0) { match = 0; } }; struct metrics_data { utf8 post; utf8 url; virtual int post_callback() { return 0; } virtual int failed_callback() { return 0; } size_t id; int group; time_t m_schedule; int flags; streamData::streamID_t sid; metrics_data () : id(0), group(0), m_schedule((time_t)0), flags(0), sid(0) {} metrics_data (metrics_info &info) : id(0), group(0), m_schedule((time_t)0), flags(0) { sid = info.sid; } virtual ~metrics_data() {} virtual const char *name() { return "metrics"; } }; struct parse_response_data : public metrics_data { stringstream m_ss; size_t m_length; parse_response_data() : metrics_data(), m_length(0) {} parse_response_data(metrics_info &info) : metrics_data(info), m_length(0) {} virtual int post_callback() = 0; virtual int failed_callback() { WLOG ("failed " + utf8(name()) + " attempt with " + m_ss.str(), LOGNAME, sid); m_ss.str(""); return 0; } }; struct licence_data : public parse_response_data { licence_data() {;} licence_data (metrics_info &info) : parse_response_data (info) {;} const char *name() { return "licence"; } int post_callback(); void handleURLs (aolxml::node *root); void checkURLNode (aolxml::node *root, const char *ref, service_t s); }; struct YP_data : public parse_response_data { YP_data (metrics_info &info) : parse_response_data (info) {;} const char *name() { return "YP"; } int post_callback(); }; // libcurl related stuff #ifdef CURLOPT_PASSWDFUNCTION /* make sure that prompting at the console does not occur */ static int my_getpass(void *client, char *prompt, char *buffer, int buflen) { buffer[0] = '\0'; return 0; } #endif static int handle_returned_header(void * ptr, size_t size, size_t nmemb, void *stream) { int amount = (int)(size * nmemb); #if defined(_DEBUG) || defined(DEBUG) metrics_data *entry = (metrics_data *)stream; DEBUG_LOG (utf8(entry->name()) + " header [" + utf8 ((const char*)ptr, amount>2 ? amount-2 : 0) + "]", LOGNAME); #endif bandWidth::updateAmount(bandWidth::AUTH_AND_METRICS, amount); return amount; } static int handle_returned_data(void * ptr, size_t size, size_t nmemb, void * /*stream*/) { int amount = (int)(size * nmemb); #if defined(_DEBUG) || defined(DEBUG) DEBUG_LOG ("Body " + tos (amount) + ":" + utf8 ((const char*)ptr, amount), LOGNAME); #endif bandWidth::updateAmount(bandWidth::AUTH_AND_METRICS, amount); return amount; } static size_t handle_licence_body (void *ptr, size_t size, size_t nmemb, void *stream) { licence_data *entry = (licence_data *)stream; size_t length = size * nmemb; if (entry->m_ss) { if (entry->m_length > 20000) { WLOG ("response for was too large, ignoring for now"); return 0; } //DLOG ("Adding " + tos (length) + ":" + utf8 ((const char*)ptr, length)); entry->m_ss.write ((const char*)ptr, length); entry->m_length += length; } bandWidth::updateAmount (bandWidth::ADVERTS, length); return length; } time_t g_metrics_updated = 0, g_recheck_services = 0; AOL_namespace::mutex g_serversLock; utf8 g_licence_DID = ""; static short int g_uniqueMetricsId = 0; const short int getMetricsClientId() { return ++g_uniqueMetricsId; } class service { protected: int m_queueCount; const short int m_id; unsigned short m_flags; CURL *m_curl; string desc; bool in_use; bool running; utf8 url; const time_t updated; time_t stop_time; utf8 main_post; list queue; virtual void resetURL (utf8 &new_url) { if (new_url == "") return; url = new_url; m_curl = webClient::setupCurlDefaults (m_curl, LOGNAME, url, 5L); curl_easy_setopt (m_curl, CURLOPT_HEADERFUNCTION, handle_returned_header); curl_easy_setopt (m_curl, CURLOPT_WRITEFUNCTION, handle_returned_data); #ifdef CURLOPT_PASSWDFUNCTION curl_easy_setopt (m_curl, CURLOPT_PASSWDFUNCTION, my_getpass); #endif } public: friend void addToServices(metrics_info &info); friend void metrics_stop(); service(short int in_type, const char *in_url, const string in_desc) : m_id(getMetricsClientId()), in_use(false), running(true), updated(g_metrics_updated), stop_time(0) { httpHeaderMap_t vars; m_flags = in_type; m_queueCount = 0; url = in_url; desc = in_desc; m_curl = NULL; resetURL (url); vars["server"] = "Shoutcast v" + gOptions.getVersionBuildStrings(); vars["port"] = tos(g_portForClients); main_post = encodeVariables(vars); DEBUG_LOG ("using " + url + " : " + main_post, in_desc.c_str()); } virtual ~service(void) { metrics_cleanup(); curl_easy_cleanup (m_curl); } virtual metrics_data *metrics_node(metrics_info &info) { return new metrics_data (info); } virtual int failedEntry (metrics_data *) { return 0; } int addEntry (metrics_info &info); void metrics_cleanup(void); static THREAD_FUNC process(void* arg); }; class licenceService : public service { int m_initial; time_t m_nextCheck; // for licence void addCheckup(time_t when); metrics_data *metrics_node (metrics_info &info) { return new licence_data (info); } void resetURL (utf8 &new_url) { //DLOG ("in reset url with " + new_url); if (new_url == "") return; url = new_url; m_curl = webClient::setupCurlDefaults (m_curl, LOGNAME, url, 5L); curl_easy_setopt (m_curl, CURLOPT_HEADERFUNCTION, handle_returned_header); curl_easy_setopt (m_curl, CURLOPT_WRITEFUNCTION, handle_licence_body); #ifdef CURLOPT_PASSWDFUNCTION curl_easy_setopt (m_curl, CURLOPT_PASSWDFUNCTION, my_getpass); #endif } public: licenceService() : service (METRIC_LICENCE, "", "licence") { m_initial = 0; m_nextCheck = (time_t)0; utf8 s = METRICS_LICENCE_URL; resetURL (s); addCheckup (::time(NULL)); } int failedEntry (metrics_data *entry) { if (entry->sid == 0) // assume licence checker { if (entry->flags & 1) return -1; // no licence, so no retry int retry = -1; m_initial++; if (m_initial == 1) retry = 1; else if (m_initial == 2) { retry = 10; } else if (m_initial == 3) //retry = 60; addCheckup (::time(NULL) + 60); else { m_initial = 0; addCheckup (::time(NULL) + 3600); } return retry; } return 0; } }; class ypService : public service { void resetURL (utf8 &new_url) { //DLOG ("in reset url with " + new_url); if (new_url == "") return; url = new_url; m_curl = webClient::setupCurlDefaults (m_curl, LOGNAME, url, 15L, 4L); curl_easy_setopt (m_curl, CURLOPT_HEADERFUNCTION, handle_returned_header); curl_easy_setopt (m_curl, CURLOPT_WRITEFUNCTION, handle_licence_body); #ifdef CURLOPT_PASSWDFUNCTION curl_easy_setopt (m_curl, CURLOPT_PASSWDFUNCTION, my_getpass); #endif } public: ypService() : service (METRIC_YP, METRICS_YP_URL, "YP") { resetURL (url); } metrics_data *metrics_node(metrics_info &info) { return new YP_data (info); } int failedEntry (metrics_data *entry) { if (entry->flags) return -1; entry->flags |= 1; return 15; } }; list servers; int service::addEntry(metrics_info &info) { httpHeaderMap_t &vars = info.vars; bool start_thread = false, queue_it = false; if (vars.empty()) { if (running && (g_metrics_updated > updated)) { DEBUG_LOG("Service " + tos(m_id) + " expired, start clean up", LOGNAME, (size_t)info.sid); running = false; } if ((running == false) && queue.empty()) { DEBUG_LOG ("[METRICS] service to be removed, " + desc); return -1; // trigger a service removal } start_thread = true; } if (stop_time == 0 && start_thread == false) { if (running && queue.size() < gOptions.metricsMaxQueue()) queue_it = true; } if (info.mode || queue_it) { metrics_data *copy = metrics_node(info); copy->id = info.id; copy->sid = info.sid; copy->post = encodeVariables(vars); copy->url = info.url; start_thread = true; if (info.mode == 2) queue.push_front(copy); else queue.push_back(copy); m_queueCount++; DEBUG_LOG("[METRICS sid=" + tos(copy->sid) + "] Added " + utf8(desc) + " details to queue [count: " + tos(queue.size()) + "]", LOGNAME, copy->sid); } if ((in_use == false) && start_thread && !queue.empty()) { in_use = true; SimpleThread(service::process, this); } return 0; } void service::metrics_cleanup() { if (!queue.empty()) { DEBUG_LOG("Purging " + tos(queue.size()) + " entries from " + tos(m_id), LOGNAME); while (!queue.empty()) { list::iterator to_go = queue.begin(); metrics_data *m = *to_go; //DLOG ("erasing metric for " + m->post); queue.erase(to_go); delete m; m = NULL; } } } THREAD_FUNC service::process(void* arg) { try { service* m_service = reinterpret_cast(arg); if (m_service) { g_serversLock.lock(); if (m_service->stop_time) { time_t diff = ::time(NULL) - m_service->stop_time; if (diff > (12 * 3600)) // drop after 12 hours of no response { m_service->metrics_cleanup(); m_service->in_use = false; g_serversLock.unlock(); return 0; } DEBUG_LOG ("[METRICS] time since stopping " + tos ((long)diff)); } if (m_service->queue.size() < 5) { // allow for a build up of metrics over a small time, saves excessive thread creation g_serversLock.unlock(); safe_sleep (0, 80); g_serversLock.lock(); } int count = 0; int try_later = 0; time_t stop_time = (time_t)0; while (1) { utf8 post; char errormsg[CURL_ERROR_SIZE] = {0}; if (m_service->m_queueCount <= try_later) { if (count) DEBUG_LOG ("[METRICS] run queue " + m_service->desc + " complete"); break; } metrics_data *entry = m_service->queue.front(); m_service->queue.pop_front(); m_service->m_queueCount--; if (entry->m_schedule > 0 && entry->m_schedule > time(NULL)) { m_service->queue.push_back (entry); m_service->m_queueCount++; try_later++; continue; } g_serversLock.unlock(); if (entry->url.empty() == false) m_service->resetURL (entry->url); // update the URL bool failed = false; if (entry->post.empty()) { if (entry->url.empty()) DLOG ("empty Post/URL update on " + m_service->desc, LOGNAME); } else { CURLcode ret = CURLE_FAILED_INIT; if (m_service->m_curl) { post = entry->post + "&" + m_service->main_post; #if defined(_DEBUG) || defined(DEBUG) DEBUG_LOG(m_service->desc + utf8(" POST body: " + post), LOGNAME, entry->sid); #endif curl_easy_setopt (m_service->m_curl, CURLOPT_ERRORBUFFER, errormsg); curl_easy_setopt (m_service->m_curl, CURLOPT_HEADERDATA, entry); curl_easy_setopt (m_service->m_curl, CURLOPT_WRITEDATA, entry); curl_easy_setopt (m_service->m_curl, CURLOPT_POSTFIELDSIZE, post.size()); curl_easy_setopt (m_service->m_curl, CURLOPT_POSTFIELDS, post.c_str()); ret = curl_easy_perform (m_service->m_curl); ++count; } if (ret != CURLE_OK) { ELOG("Request failed on " + m_service->desc + " with " + (errormsg[0] ? errormsg : curl_easy_strerror(ret)), LOGNAME, entry->sid); failed = true; } else { long response_code = 0; curl_easy_getinfo (m_service->m_curl, CURLINFO_RESPONSE_CODE, &response_code); if (response_code >= 200 && response_code < 300) { entry->post_callback(); stop_time = 0; } else { ELOG("Request failed on " + m_service->desc + " with code " + tos (response_code)); entry->failed_callback(); failed = true; } } } if (failed && !iskilled()) // a failed metric, and server still running { int delay = m_service->failedEntry (entry); if (stop_time == 0) stop_time = ::time(NULL); if (delay >= 0) // do we drop this one? < 0 something else is done, else put back on queue { g_serversLock.lock(); m_service->queue.push_front (entry); m_service->m_queueCount++; if (delay == 0) { m_service->stop_time = stop_time; break; } g_serversLock.unlock(); DLOG ("sleeping for " + tos (delay) + "s on " + m_service->desc); safe_sleep (delay, 0); g_serversLock.lock(); continue; } } delete entry; entry = NULL; g_serversLock.lock(); m_service->stop_time = stop_time; } // while m_service->in_use = false; g_serversLock.unlock(); } } catch(exception &e) { service* m_service = reinterpret_cast(arg); DLOG ("abort in metric " + m_service->desc + ", " + e.what()); safe_sleep (0,500); if (m_service) { g_serversLock.lock(); m_service->in_use = false; g_serversLock.unlock(); } } return 0; } void addToServices(metrics_info &info) { g_serversLock.lock(); list ::iterator it = servers.begin(); while (it != servers.end()) { service &s = **it; // DLOG ("Applying metric to " + s.desc + "(" + tos((long)s.flags) + ", " + tos((long)info.match) + ")"); if ((s.m_flags & info.match) && s.addEntry(info) < 0) { list ::iterator to_go = it; service *s = *it; ++it; servers.erase(to_go); delete s; s = NULL; } else { ++it; } } g_serversLock.unlock(); } // assume only called from one thread. void metrics_wakeup(bool force) { time_t now = ::time(NULL); metrics_info info; info.match = METRIC_AUDIENCE | METRIC_ADVERT | METRIC_LICENCE | METRIC_YP; if ((now > g_recheck_services) || force) { if (force) { g_recheck_services = now + 60; } info.match |= METRIC_AUDIENCE | METRIC_ADVERT; g_recheck_services = now + 60; // next recheck in case stalled metrics } addToServices(info); } void metrics_listener_new(const protocol_shoutcastClient &client) { const streamData *sd = client.m_streamData; if (sd && !sd->radionomyID().empty()) { metrics_info info; const streamData::streamInfo &stream = sd->getInfo(); info.vars["action"] = "listener_add"; info.vars["tstamp"] = tos(::time(NULL)); info.vars["host"] = sd->streamPublicIP(); info.vars["radionomyid"] = info.vars["ref"] = sd->radionomyID(); info.vars["client"] = tos(client.m_unique); info.vars["group"] = tos(client.getGroup()); info.vars["ip"] = client.m_clientAddr; info.vars["srvid"] = stream.m_serverID; info.vars["mount"] = getStreamPath(client.m_streamID); info.vars["agent"] = client.m_userAgent; info.vars["referer"] = client.m_referer; info.vars["bitrate"] = tos(sd->streamBitrate()); info.vars["codec"] = sd->streamContentType(); info.vars["contr"] = client.getContainer(); info.id = client.m_unique; info.sid = client.m_streamID; info.match = METRIC_AUDIENCE; addToServices(info); } } void metrics_listener_drop(const protocol_shoutcastClient &client) { const streamData *sd = client.m_streamData; if (sd && !sd->radionomyID().empty()) { metrics_info info; const streamData::streamInfo &stream = sd->getInfo(); time_t now = ::time(NULL); info.vars["action"] = "listener_remove"; info.vars["tstamp"] = tos(now); info.vars["host"] = sd->streamPublicIP(); info.vars["radionomyid"] = info.vars["ref"] = sd->radionomyID(); info.vars["client"] = tos(client.m_unique); info.vars["ip"] = client.m_clientAddr; info.vars["srvid"] = stream.m_serverID; info.vars["mount"] = getStreamPath(client.m_streamID); info.vars["duration"] = tos(now - client.m_startTime); info.vars["agent"] = client.m_userAgent; info.vars["referer"] = client.m_referer; info.vars["bitrate"] = tos(sd->streamBitrate()); info.vars["codec"] = sd->streamContentType(); info.vars["contr"] = client.getContainer(); info.id = client.m_unique; info.sid = client.m_streamID; info.match = METRIC_AUDIENCE; addToServices(info); } } void metrics_adListener (const protocol_shoutcastClient &client, const adSummary &summary) { metrics_info info; const streamData::streamInfo &stream = summary.sd->getInfo(); info.vars["action"] = "listener_admetric"; info.vars["tstamp"] = tos(summary.tstamp); info.vars["host"] = summary.sd->streamPublicIP(); info.vars["radionomyid"] = info.vars["ref"] = summary.sd->radionomyID(); info.vars["srvid"] = stream.m_serverID; info.vars["id"] = summary.id; info.vars["mount"] = summary.path.hideAsString(); info.vars["client"] = tos(client.getUnique()); info.vars["group"] = tos(client.getGroup()); info.vars["sent"] = tos (client.getAdAccess().total_processed); info.vars["started"] = tos (client.getAdAccess().start_time); info.mode = 1; info.sid = summary.sid; info.match = METRIC_ADVERT; addToServices(info); } void metrics_advert_started (const adSummary &summary) { metrics_info info; const streamData::streamInfo &stream = summary.sd->getInfo(); info.vars["action"] = "ad_trigger"; info.vars["tstamp"] = tos(summary.tstamp); info.vars["host"] = summary.sd->streamPublicIP(); info.vars["radionomyid"] = info.vars["ref"] = summary.sd->radionomyID(); info.vars["srvid"] = stream.m_serverID; info.vars["mount"] = summary.path.hideAsString(); info.vars["id"] = summary.id; info.vars["listeners"] = tos(summary.count); info.vars["bitrate"] = tos(summary.sd->streamBitrate()); info.vars["codec"] = summary.sd->streamContentType(); info.mode = 1; info.sid = summary.sid; info.match = METRIC_ADVERT; addToServices(info); } void metrics_advert_stats(const adSummary &summary) { metrics_info info; const streamData::streamInfo &stream = summary.sd->getInfo(); info.vars["action"] = "ad_metric"; info.vars["tstamp"] = tos(summary.tstamp); info.vars["host"] = summary.sd->streamPublicIP(); info.vars["radionomyid"] = info.vars["ref"] = summary.sd->radionomyID(); info.vars["srvid"] = stream.m_serverID; info.vars["id"] = summary.id; info.vars["mount"] = summary.path.hideAsString(); info.vars["group"] = tos(summary.group); info.vars["file"] = summary.name; if (summary.missing) info.vars["missing"] = (summary.failed ? "failed" : "timeout"); else info.vars["listeners"] = tos(summary.count); info.vars["bitrate"] = tos(summary.sd->streamBitrate()); info.vars["codec"] = summary.sd->streamContentType(); info.mode = 1; info.sid = summary.sid; info.match = METRIC_ADVERT; addToServices(info); } static bool _filled_info_notify (metrics_info &info, const streamID_t sid, const utf8& radionomyID, const utf8& serverID, const utf8& publicip, time_t tm = ::time(NULL)) { if (radionomyID.empty() || serverID.empty() || publicip.empty()) return false; info.vars["action"] = iskilled() ? "shutdown" : "reset"; info.vars["tstamp"] = tos ((long)tm); info.vars["host"] = publicip; info.vars["radionomyid"] = info.vars["ref"] = radionomyID; info.vars["srvid"] = serverID; info.vars["mount"] = getStreamPath(sid); info.sid = sid; info.match = METRIC_NOTIFICATION; info.mode = 2; return true; } void metrics_stream_down (const streamID_t sid, const utf8& radionomyID, const utf8& serverID, const utf8& publicip, time_t tm) { metrics_info info; if (_filled_info_notify (info, sid, radionomyID, serverID, publicip)) { info.vars["action"] = iskilled() ? "shutdown" : "stopped"; info.vars["started"] = tos(tm); addToServices(info); } } void metrics_stream_up (const streamID_t sid, const utf8& radionomyID, const utf8& serverID, const utf8& publicip, time_t tm) { metrics_info info; if (_filled_info_notify (info, sid, radionomyID, serverID, publicip)) { info.vars["action"] = iskilled() ? "shutdown" : "reset"; addToServices(info); } } utf8 metrics_verifyDestIP(config &conf, bool full, uniString::utf8 url) { // we'll try to set this where possible but it depends on 'destip' or 'publicip' // being set in order for us to have something to be able to be used by the YP utf8 destBindAddr = stripWhitespace((url.empty() ? (full && !conf.publicIP().empty() ? conf.publicIP() : conf.destIP()) : url)); destBindAddr = stripHTTPprefix(destBindAddr); // with full then we're wanting to filter out some of the values // since this is then used for the public reponses / YP details if (full) { // cleanup things and only provide what should be valid i.e. nothing from a private network // and not send this even if provided just means the YP server will use the connection's IP if (isRemoteAddress(destBindAddr)) { return destBindAddr; } return ""; } // otherwise we just return the cleaned version as-is // since it'll allow for use in normal bindings, etc return destBindAddr; } utf8 createGuid() { #ifdef _WIN32 #define rand_r(x) rand() #else static unsigned int seed = time(NULL); #endif std::stringstream ss; for (int i = 0; i < 30; i++) ss << std::hex << (unsigned int)(rand_r(&seed) % 16); return ss.str(); } #ifdef _MSC_VER utf8 getWindowsRegKey (bool newone = false) { wchar_t *subKey = L"Software\\Microsoft\\Cryptography"; HKEY hKey; LONG nError = RegOpenKeyEx (HKEY_LOCAL_MACHINE, subKey, 0, KEY_READ, &hKey); if (nError == ERROR_FILE_NOT_FOUND) { #if 0 // maybe try to create a local guid to read from. subkey = L"Software\\SHOUTcast"; nError = RegOpenKeyEx (HKEY_LOCAL_MACHINE, subKey, 0, KEY_READ, &hKey); if (nError == ERROR_FILE_NOT_FOUND) { // create one and put id in there nError = RegCreateKeyEx (HKEY_LOCAL_MACHINE, subKey, NULL, NULL, REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL, &hKey, NULL); if (nError == ERROR_FILE_NOT_FOUND) return ""; string guid = createGuid (); DWORD dwSize = lstrlen(&guid[0]) * sizeof(TCHAR); nError = RegSetValueEx (hKey, lpValue, NULL, REG_SZ, (unsigned char *)&guid[0], dwSize); return guid; } #else return ""; #endif } char buff[100]; DWORD rdwSize = sizeof (buff); DWORD dwType = REG_SZ; nError = RegQueryValueExA (hKey, "MachineGuid", NULL, &dwType, (unsigned char*)buff, &rdwSize); RegCloseKey (hKey); return (nError) ? "" : buff; } #endif void hashDID (utf8 &ident) { // uses openssl for hashing a machine/installation Id unsigned char digest[SHA256_DIGEST_LENGTH]; SHA256((unsigned char*)&ident[0], ident.size(), (unsigned char*)&digest); std::stringstream ss; for(int i = 0; i < SHA256_DIGEST_LENGTH; i++) ss << std::hex << (unsigned int)digest[i]; g_licence_DID = ss.str(); DLOG ("ident is " + ident + ", DID is " + g_licence_DID); } void metricsCheckDID () { if (g_licence_DID.empty() == false) return; utf8 s = "DIDC"; // Dnas ID Code do { #ifdef _WIN32 utf8 key = getWindowsRegKey(); if (key.empty() == false) { s += key; break; } #endif #ifdef PLATFORM_LINUX ifstream myfile ("/etc/machine-id"); if (myfile.is_open()) { string line; getline (myfile, line); s += line; break; } #endif // get random sequence if all else fails ILOG ("failed to get a static unique number, falling back to random sequence"); s += createGuid(); } while (0); // append conf bits to string. XXXX[-publicip]-baseport s += "-"; s += tos(gOptions.portBase()); utf8 p = gOptions.publicIP(); if (p.empty() == false) { s += "-"; s += p; } hashDID (s); } void metrics_apply(config &conf) { bool same = (metrics_verifyDestIP(conf) == metrics_verifyDestIP(gOptions)); if (!same || servers.empty()) { if (!same) { metrics_stop(); } DEBUG_LOG(utf8(same ? "Adding" : "Updating") + " metrics details", LOGNAME); metricsCheckDID (); service *s = new service (METRIC_AUDIENCE, METRICS_AUDIENCE_URL, "audience"); servers.push_front(s); s = new service (METRIC_ADVERT, METRICS_ADVERTS_URL, "adservice"); servers.push_front(s); s = new service (METRIC_NOTIFICATION, METRICS_RESET_URL, "notification"); servers.push_front(s); #ifndef LICENCE_FREE s = new licenceService (); servers.push_front(s); #endif s = new ypService (); servers.push_front(s); } } void metrics_stop() { g_serversLock.lock(); if (!servers.empty()) { int loop = 50; while (loop > 0) { list ::iterator it = servers.begin(); if (it == servers.end()) { break; } service *s = *it; if (s) { if (s->in_use) { loop--; g_serversLock.unlock(); safe_sleep(0, 100); // thread active so wait and try again g_serversLock.lock(); continue; } } servers.erase(it); if (s) { delete s; s = NULL; } } } g_serversLock.unlock(); } // called near the start of the stream and each metadata update. // void updateMeta (const metaInfo &meta) { metrics_info info; utf8 uid = gOptions.userId (); utf8 ah = gOptions.stream_authHash (meta.m_sid); if (ah.empty()) return; info.vars ["uid"] = uid; info.vars ["ah"] = ah; info.vars ["did"] = g_licence_DID; info.vars ["tstamp"] = tos (time(NULL)); info.vars ["private"] = tos(meta.m_private ? 1 : 0); info.vars ["sid"] = tos(meta.m_sid); info.vars ["format"] = meta.m_format; info.vars ["audience"] = tos (stats::getUserCount (meta.m_sid)); info.vars ["maxlisteners"] = tos(meta.m_maxListeners); info.vars ["currentsong"] = meta.m_song; info.vars ["bitrate"] = tos(meta.m_bitrate); info.vars ["mount"] = getStreamPath (meta.m_sid); info.vars ["samplerate"] = tos (meta.m_samplerate); info.vars ["verinfo"] = meta.m_version; info.vars ["agent"] = meta.m_agent; info.vars ["sourceip"] = meta.m_sourceIP; info.vars ["publicip"] = gOptions.publicIP(); info.vars ["publicport"] = tos (gOptions.publicPort()); info.vars ["secure"] = tos (threadedRunner::isSSLCapable() ? 1 : 0); stats::statsData_t data; stats::getStats (meta.m_sid, data); info.vars ["peaklisteners"] = tos(data.peakListeners); info.match = METRIC_YP; info.sid = meta.m_sid; info.mode = 1; DLOG ("push to YP requested \"" + meta.m_song + "\"", LOGNAME, meta.m_sid); addToServices(info); } void licence_data::checkURLNode (aolxml::node *root, const char *ref, service_t s) { aolxml::node *n = aolxml::node::findNode (root, ref); if (n) { utf8 url = n->findAttributeString ("url"); if (url.empty() == false) { metrics_info info; info.url = url; info.match = s; info.mode = 2; addToServices (info); } } } void licence_data::handleURLs (aolxml::node *root) { if (root == NULL) return; checkURLNode (root, "/SHOUTCAST/METRICS", METRIC_AUDIENCE); checkURLNode (root, "/SHOUTCAST/METRICSAD", METRIC_ADVERT); checkURLNode (root, "/SHOUTCAST/YP", METRIC_YP); aolxml::node *n = aolxml::node::findNode (root, "/SHOUTCAST/AUTH"); if (n) { utf8 s = n->findAttributeString ("url"); if (s.empty() == false) { auth::g_authURL = s; auth::updateServices (); } } } int licence_data::post_callback () { vector<__uint8> v; #ifdef LICENCE_RESP utf8 s = LICENCE_RESP; v.assign (&s[0], &s[s.size()]); #else v.reserve (m_ss.tellp()); std::copy (std::istreambuf_iterator( m_ss ), std::istreambuf_iterator(), std::back_inserter(v)); #endif aolxml::node *n = NULL, *root = NULL; do { if (v.empty()) break; #if defined(_DEBUG) || defined(DEBUG) //DLOG ("response size is " + tos (v.size())); DEBUG_LOG ("Licence body " + tos (v.size()) + ":" + utf8 ((const char*)&v[0], v.size()), LOGNAME); #endif root = aolxml::node::parse (&v[0], v.size()); if (root) n = aolxml::node::findNode (root, "/SHOUTCAST/FUNCTION"); if (n == NULL) { ILOG ("license parse failed, skipping", LOGNAME); break; } utf8 s = n->findAttributeString ("level"); if (s == "10") { ILOG ("detected paying offer", LOGNAME); streamData::streamInfo::m_allowSSL_global = 1; streamData::streamInfo::m_allowAllFormats_global = 1; streamData::streamInfo::m_allowBackupURL_global = 1; streamData::streamInfo::m_allowMaxBitrate_global = 0; break; } ILOG ("free offer only", LOGNAME); streamData::streamInfo::m_allowSSL_global = 0; streamData::streamInfo::m_allowAllFormats_global = 0; streamData::streamInfo::m_allowBackupURL_global = 0; streamData::streamInfo::m_allowMaxBitrate_global = 128; } while (0); handleURLs (root); forget (root); return 0; } // Called at server start and every hour. void licenceService::addCheckup (time_t when) { // eg http://dnas-services.shoutcast.com:8500/registration/?lid=2&debug=yes // eg https://dnas-services.shoutcast.com/registration/?lid=2&debug=yes httpHeaderMap_t vars; bool licenceMissing = true; utf8 s = gOptions.userId(); if (s.empty()) return; vars ["uid"] = s; s = gOptions.licenceId(); if (s.empty() == false) { vars ["lid"] = s; licenceMissing = false; } vars ["did"] = g_licence_DID; vars ["tstamp"] = tos (time(NULL)); licence_data *copy = new licence_data(); copy->post = encodeVariables(vars); copy->m_schedule = when; if (licenceMissing) copy->flags |= 1; g_serversLock.lock(); queue.push_front (copy); m_queueCount++; g_serversLock.unlock(); } int YP_data::post_callback () { vector<__uint8> v; v.reserve (m_ss.tellp()); std::copy (std::istreambuf_iterator( m_ss ), std::istreambuf_iterator(), std::back_inserter(v)); aolxml::node *n = NULL, *root = NULL; do { //DLOG ("response size is " + tos (v.size())); if (v.empty()) break; #if defined(_DEBUG) || defined(DEBUG) DEBUG_LOG ("YP Body " + tos (v.size()) + ":" + utf8 ((const char*)&v[0], v.size()), name(), sid); #endif root = aolxml::node::parse (&v[0], v.size()); if (root) n = aolxml::node::findNode (root, "/response"); if (n == NULL) { ILOG ("response invalid, skipping", name(), sid); break; } int code = aolxml::subNodeText(n, "/response/statusCode", 400); if (code != 200) { utf8 msg = aolxml::subNodeText(n, "/response/statusText", (utf8)""); if (msg != (utf8)"") WLOG ("response returned " + tos(code) + ", " + msg, name(), sid); break; } n = aolxml::node::findNode (root, "/response/data"); if (n == NULL) { ILOG ("No special settings from YP for stream " + tos(sid), name(), sid); break; } yp2::stationInfo info; info.m_advertMode = aolxml::subNodeText(n, "/response/data/admode", 0); info.m_streamTitle = aolxml::subNodeText(n, "/response/data/stationname", (utf8)""); info.m_stationID = aolxml::subNodeText(n, "/response/data/stationid", (utf8)""); info.m_serverID = aolxml::subNodeText(n, "/response/data/serverid", (utf8)""); info.m_radionomyID = aolxml::subNodeText(n, "/response/data/callsign", (utf8)""); info.m_responseCode = code; info.m_advertType = aolxml::subNodeText(n, "/response/data/advert/type", (utf8)"fixed"); info.m_advertTrigger = aolxml::subNodeText(n, "/response/data/advert/trigger", (utf8)"meta"); streamData *sd = streamData::accessStream (sid); if (sd) { sd->YP2_updateInfo (info); sd->releaseStream(); } ILOG ("Stream #" + tos(sid) + " has been updated on the Shoutcast Directory.", name(), sid); } while (0); forget (root); return 0; } } // namespace