#ifdef _WIN32 #undef _WIN32_WINNT #define _WIN32_WINNT 0x0601 #include #include #else #include #include #endif #include #include #include #include #include "services/stdServiceImpl.h" #include "file/fileUtils.h" #include "global.h" #include "threadedRunner.h" #include "protocol_relay.h" #include "w3cLog.h" #include "yp2.h" #include "updater.h" #include "auth.h" #include "streamData.h" #include "adminList.h" #include "banList.h" #include "ripList.h" #include "agentList.h" #include "cpucount.h" #include "stats.h" #include "bandwidth.h" #include "cache.h" #ifdef _WIN32 #define _WSPIAPI_H_ #endif //#include using namespace std; using namespace uniString; using namespace stringUtil; #define LOGNAME "[MAIN] " #ifdef _WIN32 #include static void win32SocketSetup() throw(runtime_error) { WSADATA wsaData = {0}; WORD wVersionRequested = MAKEWORD( 2, 2 ); int err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { throw runtime_error("Could not find usable Winsock DLL"); } /* Confirm that the WinSock DLL supports 2.2.*/ /* Note that if the DLL supports versions greater */ /* than 2.2 in addition to 2.2, it will still return */ /* 2.2 in wVersion since that is the version we */ /* requested. */ if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { /* Tell the user that we could not find a usable */ /* WinSock DLL. */ WSACleanup(); throw runtime_error("Could not find appropriate winsock dll version"); } /* The WinSock DLL is acceptable. Proceed. */ // if we can detect WSAPoll(..) then we'll use that over // select which means Vista+ should be similar to Linux. HINSTANCE ws2_32 = GetModuleHandle(TEXT("WS2_32.DLL")); if (ws2_32 != NULL) { typedef INT (WSAAPI *LPFN_WSAPOLL)(LPWSAPOLLFD fdarray, ULONG nfds, INT timeout); extern LPFN_WSAPOLL fnWSAPoll; fnWSAPoll = (LPFN_WSAPOLL)GetProcAddress(ws2_32, "WSAPoll"); } } static void win32SocketCleanup() throw() { WSACleanup(); } #else int _kbhit(void) { struct termios oldt, newt; tcgetattr(STDIN_FILENO, &oldt); newt = oldt; newt.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &newt); int oldf = fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); int ch = getchar(); tcsetattr(STDIN_FILENO, TCSANOW, &oldt); fcntl(STDIN_FILENO, F_SETFL, oldf); return (ch != EOF); } #endif void scheduleRelay(const config::streamConfig &info) throw() { // only attempt to run if not indicated as having moved if(gOptions.stream_movedUrl(info.m_streamID).empty()) { threadedRunner::scheduleRunnable(new protocol_relay(info)); } } utf8 getLogFile(utf8 fileName) { #ifdef _WIN32 // this will fill in the default log path as required wchar_t s_defaultFileName[MAX_PATH] = {0}; ExpandEnvironmentStringsW(DEFAULT_LOGW, s_defaultFileName, MAX_PATH); utf8 m_defaultFilename(utf32(s_defaultFileName).toUtf8()), m_fileName = fileName; HANDLE m_file = ::CreateFileW(m_fileName.toWString().c_str(),GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); if (m_file == INVALID_HANDLE_VALUE) { m_file = ::CreateFileW(m_defaultFilename.toWString().c_str(),GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); if (m_file == INVALID_HANDLE_VALUE) { uniFile::filenameType fallbackFilename("%temp%\\sc_serv_" + tos(getpid()) + ".log"); wchar_t s_fallbackFileName[MAX_PATH] = {0}; ExpandEnvironmentStringsW(fallbackFilename.toWString().c_str(), s_fallbackFileName, MAX_PATH); utf8 m_fallbackFilename(utf32(s_fallbackFileName).toUtf8()); m_file = ::CreateFileW(m_fallbackFilename.toWString().c_str(),GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); if (m_file != INVALID_HANDLE_VALUE) { forgetHandleInvalid(m_file); } else { m_fileName = m_fallbackFilename; } } else { m_fileName = m_defaultFilename; } } #else utf8 m_defaultFilename = gOptions.logFile_Default(), m_fileName = fileName; int m_file = ::open(m_fileName.hideAsString().c_str(),O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (m_file == -1) { m_file = ::open(m_defaultFilename.hideAsString().c_str(),O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (m_file == -1) { uniFile::filenameType fallbackFilename("/tmp/sc_serv_" + tos(getpid()) + ".log"); m_file = ::open(fallbackFilename.hideAsString().c_str(),O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (m_file == -1) { throw runtime_error("Logger could not open the log file \"" + m_fileName.hideAsString() + "\" for writing [" + errMessage().hideAsString() + "]. Check the directory exists and another instance is not already running."); } else { m_fileName = fallbackFilename; } } else { m_fileName = m_defaultFilename; } } #endif return m_fileName; } // event triggers for main loop static vector s_ControlEvents; class sc_serv2_service { // log messages that have been stuffed in config objects deferred list. // this only need be called at startup, to display messages that were created during // system initialization, but could not be displayed because the loggers didn't yet exist. static void logDeferredMessages() { const vector &deferred_error_log_messages(gOptions.deferredErrorLogMessages()); for (vector::const_iterator i = deferred_error_log_messages.begin(); i != deferred_error_log_messages.end(); ++i) { ELOG((*i).hideAsString()); } const vector &deferred_warn_log_messages(gOptions.deferredWarnLogMessages()); for (vector::const_iterator i = deferred_warn_log_messages.begin(); i != deferred_warn_log_messages.end(); ++i) { WLOG((*i).hideAsString()); } gOptions.clearDeferredErrorLogMessages(); gOptions.clearDeferredWarnLogMessages(); } public: static void addCustomLogElements() {} sc_serv2_service() {} // main application loop static int go(stdServiceBase &base) throw() { g_upTime = ::time(NULL); g_userAgent = g_userAgentBase + gOptions.getVersionBuildStrings(); #ifdef _WIN32 bool win32_socket_cleanup_required(false); #endif int mainResult = 0; // result of main loop utf8 pidFn; vector runners; try { runserver: #ifdef CONFIG_BUILDER bool builderMode = (gOptions.confFile() == "builder" || gOptions.confFile() == "-b"); bool setupMode = (gOptions.confFile() == "setup" || gOptions.confFile() == "-s" || builderMode); #else bool setupMode = (gOptions.confFile() == "setup" || gOptions.confFile() == "-s"); #endif bool gotConfig = (!gOptions.confFile().empty() && uniFile::fileExists(gOptions.confFile())); // look for sc_serv.conf or sc_serv.ini to emulate v1 DNAS behaviour as // too many people cannot get on with having to specify a config file :( if (gotConfig == false && setupMode == false) { utf8 currentLogFile = gOptions.logFile(); #ifdef _WIN32 vector fileList = fileUtil::directoryFileList(gStartupDirectory.toWString() + L"sc_serv.ini", L"", true, true); vector fileListConf = fileUtil::directoryFileList(gStartupDirectory.toWString() + L"sc_serv.conf", L"", true, true); #else vector fileList = fileUtil::directoryFileList(gStartupDirectory.hideAsString() + "sc_serv.ini", ""); vector fileListConf = fileUtil::directoryFileList(gStartupDirectory.hideAsString() + "sc_serv.conf", ""); #endif if (!fileList.empty()) { fileList.insert(fileList.end(), fileListConf.begin(),fileListConf.end()); } else { fileList = fileListConf; } if (!fileList.empty()) { #ifdef _WIN32 utf32 u32file(fileList[0]); utf8 u8f(u32file.toUtf8()); gotConfig = gOptions.load(u8f); #else gotConfig = gOptions.load(fileList[0]); #endif } // if these do not match then we need to update the log file being used // as otherwise it reports the wrong thing on the admin pages, etc if (gOptions.logFile() != currentLogFile) { base.startNormalLog(false); } } if (isPostSetup() == false) { if (gOptions.screenLog()) { base.startScreenLog(); } // during initial startup some messages are produced but cannot be logged because // the loggers do not yet exist. These are in the deferred list. At this point they // can be safely logged logDeferredMessages(); gOptions.m_certPath = fileUtil::getFullFilePath(gStartupDirectory + "cacert.pem"); if (!uniFile::fileExists(gOptions.m_certPath)) { WLOG(LOGNAME "Cannot find `" + gOptions.m_certPath + "'"); WLOG(LOGNAME "Without `cacert.pem' the DNAS may not be able to contact the Directory"); WLOG(LOGNAME "The latest can be downloaded from `http://curl.haxx.se/ca/cacert.pem'" + eol()); } #ifdef _WIN32 ILOG("*********************" + string(!sDaemon ? "***************************" : "<>") + "*********************"); #else ILOG("*********************" + string(!sDaemon ? "***************************" : "<>") + "*********************"); #endif ILOG("** Shoutcast Distributed Network Audio Server (DNAS) **"); ILOG("** Copyright (C) 2014-2023 Radionomy SA, All Rights Reserved **"); if (gotConfig == false && setupMode == false) { ILOG("** Use \"sc_serv [filename]\" to specify a config file **"); } ILOG("*********************************************************************"); utf8 version = gOptions.getVersionBuildStrings(); #if defined(_DEBUG) || defined(DEBUG) version += "[DBUG]"; #endif ILOG(LOGNAME "Shoutcast DNAS/" SERV_OSNAME " v" + version + " (" __DATE__ ")"); ILOG(LOGNAME "PID: " + tos(getpid())); } if (gotConfig) { ILOG(LOGNAME "Saving log output to `" + fileUtil::getFullFilePath(gOptions.realLogFile()) + "'"); ILOG(LOGNAME "Automatic log rotation " + (!gOptions.rotateInterval() ? utf8("disabled") : utf8("interval: ") + timeString(gOptions.rotateInterval()))); ILOG(LOGNAME "Loaded config from `" + fileUtil::getFullFilePath(gOptions.confFile()) + "'"); } else { uniFile::filenameType oldLogFile = gOptions.logFile(); // if we get to this state then to make things easier, we attempt to offer // some possible configuration files to attempt to load as the config file int mode = (setupMode ? 2 : gOptions.promptConfigFile()); if (mode <= 0) { if (mode == -1) { ELOG(LOGNAME "Aborting as no valid config files could be found."); throwEx(LOGNAME "Try running setup mode to create a valid config file."); } else if (mode == -2) { throwEx(LOGNAME "Aborting at user request."); } else { if (!gOptions.confFile().empty() && !uniFile::fileExists(gOptions.confFile())) { throwEx(LOGNAME "Passed config file does not exist (check the file path exists)"); } else { throwEx(LOGNAME "No config file passed"); } } } else if (mode == 1) { // if these do not match then we need to update the log file being used utf8 newLogFile = getLogFile(gOptions.logFile()); if (newLogFile != oldLogFile) { uniString::utf8 file = newLogFile.substr(0,gOptions.logFile().rfind(fileUtil::getFilePathDelimiter())).c_str(); if ((file == newLogFile) || fileUtil::directoryExists(fileUtil::onlyPath(newLogFile))) { #ifdef _WIN32 // see if we can create the file, if not then just keep using the temp folder for the log file as we know it's ok HANDLE m_file = ::CreateFileW(newLogFile.toWString().c_str(),GENERIC_WRITE, FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); if (m_file != INVALID_HANDLE_VALUE) { gOptions.setOption(utf8("reallogfile"), newLogFile); forgetHandleInvalid(m_file); base.startNormalLog(true); } // make sure it's showing the correct log file else { // this will fill in the default log path as required wchar_t m_fileName[MAX_PATH] = {0}; ExpandEnvironmentStringsW(DEFAULT_LOGW, m_fileName, MAX_PATH); utf8 log(utf32(m_fileName).toUtf8()); gOptions.setOption(utf8("logfile"), log); gOptions.setOption(utf8("reallogfile"), log); ILOG(LOGNAME "Logger keeping log file as `" + fileUtil::getFullFilePath(log) + "'"); ILOG(LOGNAME "Check you have write permissions for the folder(s) set in the config file."); } #else // see if we can create the file, if not then just keep using the temp folder for the log file as we know it's ok int m_file = ::open(newLogFile.hideAsString().c_str(),O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (m_file != -1) { ::close(m_file); gOptions.setOption(utf8("reallogfile"), newLogFile); base.startNormalLog(true); } // make sure it's showing the correct log file else { // this will fill in the default log path as required utf8 log(DEFAULT_LOG); gOptions.setOption(utf8("logfile"), log); gOptions.setOption(utf8("reallogfile"), log); ILOG(LOGNAME "Logger keeping log file as `" + fileUtil::getFullFilePath(log) + "'"); ILOG(LOGNAME "Check you have write permissions for the folder(s) set in the config file."); } #endif } else { throwEx(LOGNAME "Log file path does not exist (check the folder path exists)"); } } ILOG(LOGNAME "Saving log output to `" + fileUtil::getFullFilePath(gOptions.realLogFile()) + "'"); ILOG(LOGNAME "Automatic log rotation " + (!gOptions.rotateInterval() ? utf8("disabled") : utf8("interval: ") + timeString(gOptions.rotateInterval()))); ILOG(LOGNAME "Loaded config from `" + fileUtil::getFullFilePath(gOptions.confFile()) + "'"); } else if (mode == 2) { if (setupMode) { #ifdef CONFIG_BUILDER if (!builderMode) { #endif ILOG(LOGNAME "Entering setup mode - Open 127.0.0.1:8000/setup in a"); ILOG(LOGNAME "browser on the same machine the DNAS was started on"); #ifdef CONFIG_BUILDER } else { ILOG(LOGNAME "Entering builder mode - Open 127.0.0.1:8000/builder in"); ILOG(LOGNAME "a browser on the same machine the DNAS was started on"); } #endif ILOG(LOGNAME "if the browser does not automatically open the page."); ILOG(LOGNAME "If working remotely then replace 127.0.0.1 with the"); ILOG(LOGNAME "IP / address of the remote system to use this mode."); } // attempt to open the setup / builder page on the system's browser i.e. kiss option #ifdef _WIN32 #ifdef CONFIG_BUILDER if (!builderMode) { #endif ::system("start http://127.0.0.1:8000/setup"); #ifdef CONFIG_BUILDER } else { ::system("start http://127.0.0.1:8000/builder"); } #endif #else #ifdef CONFIG_BUILDER if (!builderMode) { #endif ::system("(if(which xdg-open > /dev/null)then(xdg-open http://127.0.0.1:8000/setup)elif(which gnome-open > /dev/null)then(gnome-open http://127.0.0.1:8000/setup)fi)&>/dev/null"); #ifdef CONFIG_BUILDER } else { ::system("(if(which xdg-open > /dev/null)then(xdg-open http://127.0.0.1:8000/builder)elif(which gnome-open > /dev/null)then(gnome-open http://127.0.0.1:8000/builder)fi)&>/dev/null"); } #endif #endif #ifdef _WIN32 win32SocketSetup(); win32_socket_cleanup_required = true; #endif threadedRunner *tr = new threadedRunner; runners.push_back(tr); tr->start(); threadedRunner::scheduleRunnable(new microServer("", gOptions.portBase(), (microServer::AllowableProtocols_t)(P_WEB_SETUP), microServer::L_MISC)); s_ControlEvents.push_back(serviceMain::sStop); while (!iskilled()) { #if defined(_WIN32) && defined(_WIN64) if (WaitForMultipleObjects((DWORD)s_ControlEvents.size(), &(s_ControlEvents[0]), 0, 1000) == WAIT_OBJECT_0) #else if (WaitForMultipleObjects(s_ControlEvents.size(), &(s_ControlEvents[0]), 0, 1000) == WAIT_OBJECT_0) #endif { // stop signal setkill(1); break; } } if (iskilled() == 2) { ILOG(LOGNAME "Stopping setup mode. Preparing for broadcasting..."); } else { ILOG(LOGNAME "Stopping setup mode. Shutting down..."); } for (vector::const_iterator i = runners.begin(); i != runners.end(); ++i) { (*i)->stop(); (*i)->join(); delete (*i); } runners.clear(); #ifdef _WIN32 if (win32_socket_cleanup_required) { win32SocketCleanup(); } #endif s_ControlEvents.clear(); // if we get here, we effectively restart the server post-setup if (iskilled() == 2) { #ifdef _WIN32 gOptions.load(gStartupDirectory + "sc_serv.conf"); #else gOptions.load(gStartupDirectory.hideAsString() + "sc_serv.conf"); #endif setPostSetup(true); setkill(0); goto runserver; } setkill(0); return mainResult; } } // just make sure that we can do this else we need to abort if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) { throwEx(LOGNAME "Unable to load libcurl & / or its dependencies - cannot continue."); } config::streams_t streams; gOptions.getStreamConfigs(streams); // the old sc_serv had two sets of passwords, mimic this if (gOptions.adminPassword().empty()) { WLOG(gOptions.logSectionName() + "A dedicated `adminpassword' should be specified in the configuration."); WLOG(gOptions.logSectionName() + "Legacy handling has been enabled to map `adminpassword' to `password'."); WLOG(gOptions.logSectionName() + "This is not deemed safe and `adminpassword' should be directly set."); gOptions.setOption(utf8("adminpassword"), gOptions.password()); } else { // otherwise if explicitly set as the same then we abort if (gOptions.adminPassword() == gOptions.password()) { throwEx(gOptions.logSectionName() + "You must specify different passwords for `adminpassword' and `password'."); } } // abort no matter what as without passwords we can be at risk if (gOptions.adminPassword().empty() && gOptions.password().empty()) { throwEx(gOptions.logSectionName() + "You must specify a password for `adminpassword' and `password'."); } if (!streams.empty()) { if (gOptions.setupPasswords(streams)) { // if there was any error on the passwords then we need to abort // so it can be fixed. important if using multi-stream hosting! throwEx(gOptions.logSectionName() + "Check the stream configurations above or ensure `adminpassword' and `password' have been set."); } } else { // if there are no stream configs and no main password // then we'll also need to abort as that's not allowed if (gOptions.password().empty()) { throwEx(gOptions.logSectionName() + "You must specify a password for `password'."); } } if (!gOptions.cdn().empty()) { ILOG(LOGNAME "CDN " + utf8(gOptions.cdn() == "on" ? "opt-in" : "opt-out") + " mode enabled -> ensure all stream(s) are properly configured"); } const int cpu_count = gOptions.getCPUCount(); // check options ILOG(LOGNAME "Calculated CPU count is " + tos(cpucount()) + " -> " + (cpu_count == cpucount() ? "using " + utf8(cpu_count > 1 ? "all" : "the") + " available CPU" + (cpu_count > 1 ? "s" : "") : tos(cpu_count) + " CPU" + (cpu_count > 1 ? "s" : "") + " specified to be used")); #ifndef _WIN32 rlimit rlim = {0}; if(!getrlimit(RLIMIT_NOFILE, &rlim)) { ILOG(LOGNAME "Limited to " + tos(rlim.rlim_cur) + " file descriptors [relates to ulimit -n]"); } #endif // calculate defaults for yp // if 'publicport' is not the same as 'portbase' // then we use it instead of the normal portbase g_portForClients = ((gOptions.publicPort() != -1 && (gOptions.publicPort() != gOptions.portBase())) ? gOptions.publicPort() : gOptions.portBase()); // before anything else starts up we'll start this as needed metrics::metrics_apply(gOptions); auth::init(); // load any SSL certificates threadedRunner::SSL_init(); #ifdef _WIN32 win32SocketSetup(); win32_socket_cleanup_required = true; #endif ILOG(LOGNAME "Starting " + tos(cpu_count) + " network thread" + (cpu_count > 1 ? "s" : "")); for (int x = 0; x < cpu_count; ++x) { threadedRunner *tr = new threadedRunner; runners.push_back(tr); tr->start(); } constructMessageResponses(); // w3c logging if (gOptions.w3cEnable()) { w3cLog::open(gOptions.w3cLog()); } // load geoIP database if able to be found /*{ static utf8 dir = gStartupDirectory; char path[1024] = {0}; GeoIP_setup_custom_directory(strncpy(path, (char*)dir.hideAsString().c_str(), sizeof(path))); GeoIP * gi = GeoIP_new(GEOIP_MEMORY_CACHE); if(gi != NULL) { ILOG(LOGNAME "Loaded GeoIP database - Ban and Reserve by country is enabled"); const char* returnedCountry, * ips[] = {"58.218.199.227","77.76.181.71","87.104.93.85","195.238.117.56", "114.244.60.232","108.52.34.116","91.121.164.186","78.46.75.50", "62.75.139.39","172.17.200.143","205.188.215.228","82.30.80.183" }; for (int ip = 0; ip < sizeof(ips)/sizeof(ips[0]); ip++) { utf8 details; returnedCountry = GeoIP_country_code_by_addr(gi, ips[ip]); details = (returnedCountry ? returnedCountry : utf8("UNKNOWN")); returnedCountry = GeoIP_country_name_by_addr(gi, ips[ip]); details += " (" + (returnedCountry ? returnedCountry : utf8("UNKNOWN")) + ")"; WLOG(details); } GeoIP_delete(gi); } }*/ // load up stream branding artwork gOptions.m_artworkBody[0] = loadLocalFile(fileUtil::getFullFilePath(gOptions.artworkFile()), LOGNAME, 523872/*32 x (16384 - 6 - 6 - 1)*/); // load up ban file g_banList.load(gOptions.banFile(),0); // load up rip file g_ripList.load(gOptions.ripFile(),0); // load up admin access file g_adminList.load(gOptions.adminFile()); // load up agent file g_agentList.load(gOptions.agentFile(),0); // per-stream options gOptions.getStreamConfigs(streams); for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i) { // w3c logging if (gOptions.read_stream_w3cLog((*i).first)) { w3cLog::open(gOptions.stream_w3cLog((*i).first),(*i).first); } // load up ban file if (gOptions.read_stream_banFile((*i).first)) { g_banList.load(gOptions.stream_banFile((*i).first),(*i).first); } // load up rip file if (gOptions.read_stream_ripFile((*i).first)) { g_ripList.load(gOptions.stream_ripFile((*i).first),(*i).first); } // load up agent file if (gOptions.read_stream_agentFile((*i).first)) { g_agentList.load(gOptions.stream_agentFile((*i).first),(*i).first); } // load up stream branding artwork if (gOptions.read_stream_artworkFile((*i).first)) { gOptions.m_artworkBody[(*i).first] = loadLocalFile(fileUtil::getFullFilePath(gOptions.stream_artworkFile((*i).first)), LOGNAME, 523872/*32 x (16384 - 6 - 6 - 1)*/); } } utf8 srcBindAddr = stripHTTPprefix(stripWhitespace(gOptions.srcIP())); utf8 destBindAddr = metrics::metrics_verifyDestIP(gOptions, false); if (g_IPAddressForClients.empty()) { char s[MAXHOSTNAMELEN] = {0}; // paranoia if (!::gethostname(s, MAXHOSTNAMELEN - 1)) { // changed to not throw (build 43) as this will still run correctly // and has caused a number of users to go back to v1 unnecessarily. g_IPAddressForClients = socketOps::hostNameToAddress(s, g_portForClients); } } else { utf8 addr = socketOps::hostNameToAddress(destBindAddr.hideAsString().c_str()); if (!addr.empty()) { destBindAddr = addr; } /*hostent *host; if ((host = ::gethostbyname((const char *)destBindAddr.c_str())) != NULL){ destBindAddr = (inet_ntoa(*( (struct in_addr *)host->h_addr))); }*/ } if (!srcBindAddr.empty()) { utf8 addr = socketOps::hostNameToAddress(srcBindAddr.hideAsString().c_str()); if (!addr.empty()) { srcBindAddr = addr; } /*hostent *host; if ((host = ::gethostbyname((const char *)srcBindAddr.c_str())) != NULL){ srcBindAddr = (inet_ntoa(*( (struct in_addr *)host->h_addr))); }*/ } // for legacy sources (with optional portbase override / disable) g_legacyPort = ((gOptions.portLegacy() != gOptions.portBase() + 1) && (gOptions.portLegacy() != -1) ? gOptions.portLegacy() : gOptions.portBase() + 1); // if src and dst are same we configure ourselves a bit differently if ((srcBindAddr == destBindAddr) || (inet_addr((const char *)srcBindAddr.c_str()) == inet_addr((const char *)destBindAddr.c_str()))) { // for clients and sources threadedRunner::scheduleRunnable(new microServer(srcBindAddr.hideAsString(), gOptions.portBase(), (microServer::AllowableProtocols_t)(P_SHOUTCAST1CLIENT | P_SHOUTCAST2CLIENT | P_SHOUTCAST1SOURCE | P_SHOUTCAST2SOURCE | P_WEB), (microServer::ListenTypes_t)(microServer::L_SOURCE | microServer::L_CLIENT))); } else { // changed in b71 as it's possible to specify a destip which does not correctly bind but the // value of destip could be correct and able to work in all other areas e.g. via listen.pls // so if there is an issue with the destip specific bind, then attempt a destip==srcip bind microServer *r = 0; try { // for clients // changed in b79 r = new microServer(destBindAddr.hideAsString(), gOptions.portBase(), (microServer::AllowableProtocols_t)(P_SHOUTCAST1CLIENT | P_SHOUTCAST2CLIENT | P_SHOUTCAST1SOURCE | P_WEB), (microServer::ListenTypes_t)(microServer::L_SOURCE | microServer::L_CLIENT)); threadedRunner::scheduleRunnable(r); // for sources threadedRunner::scheduleRunnable(new microServer(srcBindAddr.hideAsString(), gOptions.portBase(), (microServer::AllowableProtocols_t)(P_SHOUTCAST1SOURCE | P_SHOUTCAST2SOURCE | P_WEB), microServer::L_SOURCE)); } catch(const exception &ex) { if (r == 0) { WLOG(ex.what()); } try { // for clients and sources threadedRunner::scheduleRunnable(new microServer(srcBindAddr.hideAsString(), gOptions.portBase(), (microServer::AllowableProtocols_t)(P_SHOUTCAST1CLIENT | P_SHOUTCAST2CLIENT | P_SHOUTCAST1SOURCE | P_SHOUTCAST2SOURCE | P_WEB), (microServer::ListenTypes_t)(microServer::L_SOURCE | microServer::L_CLIENT))); } catch(const exception &exx) { // changed in b79 // if we get to here, if we've been able to bind at least on the destip // but the srcip bind fails then attempt to allow v2 sources on destip. if (r != 0) { r->updateProtocols((microServer::AllowableProtocols_t)(P_SHOUTCAST1CLIENT | P_SHOUTCAST2CLIENT | P_SHOUTCAST1SOURCE | P_SHOUTCAST2SOURCE | P_WEB), (microServer::ListenTypes_t)(microServer::L_SOURCE | microServer::L_SOURCE2), gOptions.portBase()); } else { throwEx(exx.what()); } } } } if (g_legacyPort > 0) { // for v1 sources threadedRunner::scheduleRunnable(new microServer(srcBindAddr.hideAsString(), g_legacyPort, (microServer::AllowableProtocols_t)(P_SHOUTCAST1SOURCE), microServer::L_SOURCE)); } else { ILOG("[MICROSERVER] Legacy v1 source support not enabled"); } // for flash policy file server if (gOptions.flashPolicyServerPort() != -1) { threadedRunner::scheduleRunnable(new microServer(srcBindAddr.hideAsString(), gOptions.flashPolicyServerPort(), (microServer::AllowableProtocols_t)(P_FLASHPOLICYFILE), microServer::L_FLASH)); } else { ILOG("[MICROSERVER] Flash policy file server not enabled"); } // and finally look at adding any extra client ports e.g. so you can run on 80 // and 8000 and anything else for clients that cannot connect to the main port if (!gOptions.alternatePorts().empty()) { vector tokens = tokenizer(gOptions.alternatePorts(), ','); for (size_t tok = 0; tok < tokens.size(); tok++) { u_short port = (u_short)atoi(tokens[tok].hideAsString().c_str()); // make sure we're only allowing a valid port number and it isn't already been registered if (port && (port != gOptions.portBase()) && (port != g_portForClients)) { microServer *r = 0; try { r = new microServer(srcBindAddr.hideAsString(), port, (microServer::AllowableProtocols_t)(P_SHOUTCAST1CLIENT | P_SHOUTCAST2CLIENT), (microServer::ListenTypes_t)(microServer::L_CLIENT_ALT)); threadedRunner::scheduleRunnable(r); gOptions.m_usedAlternatePorts += "," + tokens[tok]; } catch(const exception &ex) { if (r == 0) { WLOG(ex.what()); WLOG("[MICROSERVER] Alternate client connections on port " + tokens[tok] + " will not work"); } } } else { if (port != gOptions.portBase() && port != g_portForClients) { WLOG("[MICROSERVER] Skipping `" + tokens[tok] + "' as it is an invalid alternate client port"); } else { WLOG("[MICROSERVER] Skipping alternate port " + tokens[tok] + " as it is already a used port"); } } } } if (gOptions.pidFile().empty() || (gOptions.pidFile() == "sc_serv_$.pid")) { pidFn = gStartupDirectory + "sc_serv_" + tos(gOptions.portBase()) + ".pid"; } else if (!gOptions.pidFile().empty()) { pidFn = gOptions.pidFile(); } if (!pidFn.empty()) { FILE *f = uniFile::fopen(pidFn, "wb"); if (f) { try { utf8 s(tos(getpid()) + eol()); if (fwrite(s.c_str(),1,s.size(),f) != s.size()) { ELOG(LOGNAME "I/O error writing PID file `" + fileUtil::getFullFilePath(pidFn) + "'"); } } catch(...) { if (f) { ::fclose(f); } ELOG(LOGNAME "Error writing to PID file `" + fileUtil::getFullFilePath(pidFn) + "'"); } if (f) { ::fclose(f); } } else { ELOG(LOGNAME "Could not open PID file `" + fileUtil::getFullFilePath(pidFn) + "' for writing (" + errMessage().hideAsString() + ")"); } } // schedule relays if (!gOptions.startInactive()) { vector relayList(gOptions.getRelayList()); if (!relayList.empty()) { for_each(relayList.begin(), relayList.end(), scheduleRelay); } } //threadedRunner::scheduleRunnable(new yp2); threadedRunner::scheduleRunnable(new updater); s_ControlEvents.push_back(serviceMain::sStop); #ifndef _WIN32 s_ControlEvents.push_back(serviceMain::sWINCH); s_ControlEvents.push_back(serviceMain::sHUP); s_ControlEvents.push_back(serviceMain::sUSR1); s_ControlEvents.push_back(serviceMain::sUSR2); #endif #ifdef OPEN_PORT_CHECKER //BOOL CheckPortTCP(short int dwPort , char*ipAddressStr) { struct sockaddr_in client; int sock; client.sin_family = AF_INET; client.sin_port = htons(gOptions.portBase()-1000); //client.sin_addr.s_addr = inet_addr(g_IPAddressForClients/*destBindAddr*/.hideAsString().c_str()/*"127.0.0.1"); client.sin_addr.s_addr = inet_addr("192.168.2.3"/*"127.0.0.1"*/); sock = (int) socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); ELOG(g_IPAddressForClients + ":" + tos(gOptions.portBase()) + /*destBindAddr*/ + " " + tos(connect(sock, (struct sockaddr *) &client,sizeof(client)) == 0)); } #endif while (!iskilled()) { metrics::metrics_wakeup(); #if defined(_WIN32) && defined(_WIN64) int waitResult = WaitForMultipleObjects((DWORD)s_ControlEvents.size(),&(s_ControlEvents[0]),0,1000); #else int waitResult = WaitForMultipleObjects(s_ControlEvents.size(),&(s_ControlEvents[0]),0,1000); #endif switch (waitResult) { // stop signal case WAIT_OBJECT_0: { setkill(true); // remove the pid file on successful exit if (!pidFn.empty() && uniFile::fileExists(pidFn)) { uniFile::unlink(pidFn); } break; } #ifdef _WIN32 case WAIT_OBJECT_0+1: // admin server break; case WAIT_OBJECT_0+2: // main msg loop break; #else case WAIT_OBJECT_0+1: // winch { // reserved, banned and blocked user agent list(s) reload reloadBanLists(); reloadRipLists(); reloadAdminAccessList(); reloadAgentLists(); break; } case WAIT_OBJECT_0+2: // hup { ILOG(LOGNAME "Rotating log and W3C files [PID: " + tos(getpid()) + "]"); ROTATE; printUpdateMessage(); rotatew3cFiles("w3c"); ILOG(LOGNAME "Rotated log and W3C files [PID: " + tos(getpid()) + "]"); break; } case WAIT_OBJECT_0+3: // usr1 { // config reload reloadConfig(false); break; } case WAIT_OBJECT_0+4: // usr2 { // forced config reload reloadConfig(true); break; } case WAIT_OBJECT_0+5: // admin server break; case WAIT_OBJECT_0+6: // main msg loop break; #endif default: { } } } ILOG(LOGNAME "Exiting threads..."); if (gOptions.configRewrite()) { gOptions.rewriteConfigurationFile((gOptions.configRewrite() == 1), true); } } catch(const exception &ex) { ELOG(ex.what()); mainResult = -2; } catch(...) { ELOG(LOGNAME "Unknown exception caught"); mainResult = -1; } if (stats::getTotalUniqueListeners() > 0) { ILOG(LOGNAME "Terminating listeners..."); stats::kickAllClients(0, true); } // Stop all sources and wait up to ten seconds for everything to clear out const streamData::streamIDs_t streamIds = streamData::getStreamIds(true); if (!streamIds.empty()) { ILOG(LOGNAME "Terminating sources..."); for (streamData::streamIDs_t::const_iterator i = streamIds.begin(); i != streamIds.end(); ++i) { // kick source off system streamData::killStreamSource((*i)); } } if (yp2::requestsInQueue()) { ILOG(LOGNAME "Running Directory cleanup..."); // now wait for YP to clear out for (int x = 0; (x < 100); ++x) { if (yp2::requestsInQueue()) { safe_sleep(0, 10000); } else { break; } } } for (vector::const_iterator i = runners.begin(); i != runners.end(); ++i) { (*i)->stop(); (*i)->join(); delete (*i); } runners.clear(); metrics::metrics_wakeup(true); metrics::metrics_stop(); auth::cleanup(); // general files to save if (gOptions.saveBanListOnExit()) { g_banList.save(gOptions.banFile(),0); } if (gOptions.saveRipListOnExit()) { g_ripList.save(gOptions.ripFile(),0); } if (gOptions.saveAgentListOnExit()) { g_agentList.save(gOptions.agentFile(),0); } if (gOptions.w3cEnable()) { w3cLog::close(0); } // per-stream files to save config::streams_t streams; gOptions.getStreamConfigs(streams); for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i) { if (gOptions.saveBanListOnExit()) { if (gOptions.read_stream_banFile((*i).first) && !gOptions.stream_banFile((*i).first).empty()) { g_banList.save(gOptions.stream_banFile((*i).first),(*i).first); } } if (gOptions.saveRipListOnExit()) { if (gOptions.read_stream_ripFile((*i).first) && !gOptions.stream_ripFile((*i).first).empty()) { g_ripList.save(gOptions.stream_ripFile((*i).first),(*i).first); } } if (gOptions.saveAgentListOnExit()) { if (gOptions.read_stream_agentFile((*i).first) && !gOptions.stream_agentFile((*i).first).empty()) { g_agentList.save(gOptions.stream_agentFile((*i).first),(*i).first); } } if (gOptions.read_stream_w3cLog((*i).first)) { w3cLog::close((*i).first); } } #ifdef _WIN32 if (win32_socket_cleanup_required) { win32SocketCleanup(); } #endif s_ControlEvents.clear(); DeleteAllCaches(); stats::getFinalStats(); bandWidth::getFinalAmounts(); utf8 t = timeString(::time(NULL) - g_upTime); ILOG(LOGNAME + (t.empty() ? "Shutdown" : "Shutdown after " + t + " running") + eol()); curl_global_cleanup(); /*#ifdef _WIN32 { wchar_t buf[MAX_PATH] = L"\""; STARTUPINFO si = {sizeof(si), }; PROCESS_INFORMATION pi; GetModuleFileName(NULL, buf + 1, sizeof(buf) - 1); wcsncat(buf, L"\"", MAX_PATH); CreateProcess(NULL, buf, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); } #else #endif*/ // if we're in 'user' mode then if there was an exception // we'll not immediately abort and instead keep things on // display so it's easier for noobs, etc to see the error if (!sDaemon && gOptions.screenLog() && (mainResult < 0)) { ILOG(LOGNAME "Press any key to continue . . ."); while(!_kbhit()) {} #ifdef _WIN32 // do this so we consume the input which // as we already have a custom _kbhit() // on non-Windows already does it for us _getch(); #endif } return mainResult; } }; // create the appropriate handler for the app/daemon/service framework int sm_main(const vector &args) throw() { #ifdef _WIN32 stdServiceWin32 s("Shoutcast DNAS"); #else stdServiceUnix s; #endif return s.sm_main(args); }