mirror of
https://github.com/WinampDesktop/winamp.git
synced 2024-09-24 15:54:12 +00:00
6390 lines
245 KiB
C++
6390 lines
245 KiB
C++
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#endif
|
|
#include <stdio.h>
|
|
#include "protocol_shoutcastClient.h"
|
|
#include "protocol_admincgi.h"
|
|
#include "protocol_HTTPStyle.h"
|
|
#include "protocol_relay.h"
|
|
#include "base64.h"
|
|
#include "banList.h"
|
|
#include "ripList.h"
|
|
#include "adminList.h"
|
|
#include "agentList.h"
|
|
#include "uvox2Common.h"
|
|
#include "w3cLog.h"
|
|
#include "yp2.h"
|
|
#include "updater.h"
|
|
#include "aolxml/aolxml.h"
|
|
#include "webNet/urlUtils.h"
|
|
#include "file/fileUtils.h"
|
|
#include "services/stdServiceImpl.h"
|
|
#include "bandwidth.h"
|
|
#include "cpucount.h"
|
|
|
|
using namespace std;
|
|
using namespace stringUtil;
|
|
using namespace uniString;
|
|
|
|
time_t last_update_check = 0;
|
|
utf8 logId, logTailId, listenerId;
|
|
|
|
#define DEBUG_LOG(...) do { if (gOptions.httpStyleDebug()) DLOG(__VA_ARGS__); } while (0)
|
|
#define LOG_NAME "ADMINCGI"
|
|
#define LOGNAME "[" LOG_NAME "] "
|
|
|
|
#define HEAD_REQUEST (m_httpRequestInfo.m_request == protocol_HTTPStyle::HTTP_HEAD)
|
|
#define SHRINK (m_httpRequestInfo.m_AcceptEncoding & protocol_HTTPStyle::ACCEPT_GZIP)
|
|
#define COMPRESS(header, body) if (SHRINK && compressData(body)) header += "Content-Encoding:gzip\r\n"
|
|
|
|
static bool sortUniqueClientDataByTime(const stats::uniqueClientData_t &a, const stats::uniqueClientData_t &b)
|
|
{
|
|
return (a.m_connectTime < b.m_connectTime);
|
|
}
|
|
|
|
utf8 getStreamAdminHeader(const streamData::streamID_t sid, const utf8& headerTitle,
|
|
const int refreshRequired = 0, const bool style = false)
|
|
{
|
|
return "<!DOCTYPE html><html><head>"
|
|
"<meta charset=\"utf-8\">"
|
|
"<meta name=viewport content=\"width=device-width, initial-scale=1\">"
|
|
"<title>Shoutcast Administrator</title>"
|
|
"<link href=\"index.css\" rel=\"stylesheet\" type=\"text/css\">"
|
|
"<link href=\"images/favicon.ico\" rel=\"shortcut icon\" type=\"" +
|
|
gOptions.faviconFileMimeType() + "\">" + (abs(refreshRequired) > 0 ?
|
|
"<meta http-equiv=\"refresh\"content=\"3; url=admin.cgi?sid=" + tos(sid) + "\">" : "") +
|
|
|
|
(style ? "<style type=\"text/css\">"
|
|
"li img{vertical-align:bottom;}li{padding-bottom:0.5em;}"
|
|
"</style>" : (utf8)"") +
|
|
|
|
"</head><body style=\"margin:0;\">"
|
|
"<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr>"
|
|
"<td><div class=\"logo\">Shoutcast " + headerTitle + "</div></td>"
|
|
"<td style=\"text-align:right;vertical-align:bottom;padding-right:0.1em;\">"
|
|
"<div id=\"up\"></div><a target=\"_blank\" title=\"Built: " __DATE__"\" "
|
|
"href=\"http://www.shoutcast.com\">Shoutcast Server v" +
|
|
addWBR(gOptions.getVersionBuildStrings() + "/" SERV_OSNAME) + "</a></td>"
|
|
"</tr><tr><td class=\"thr\" align=\"center\" colspan=\"3\"><table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">"
|
|
"<tr><td class=\"thr\" align=\"center\"><div id=\"hdrbox\" class=\"tnl\" "
|
|
"style=\"justify-content:space-around;display:flex;flex-flow:row wrap;\">"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?sid=" + tos(sid) + "\">Status & Listeners</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?sid=" + tos(sid) + "&mode=history\">History "
|
|
"<img border=\"0\" title=\"History\" alt=\"History\" style=\"vertical-align:middle\" src=\"images/history.png\"></a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
+ (!(gOptions.read_stream_adminPassword(sid) && !gOptions.stream_adminPassword(sid).empty()) ?
|
|
"<div class=\"thr\"><a href=\"admin.cgi?sid=" + tos(sid) + "&mode=viewlog\">Log</a> "
|
|
"(<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=viewlog&viewlog=tail\">Tailing</a>"
|
|
" | <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=viewlog&viewlog=save\">Save</a>)</div>"
|
|
"<div class=\"thr\"> | </div>" : "") +
|
|
/*+ utf8(info.m_radionomyID.empty() ? warningImage(false) + " <b>Please Register Your Authhash</b><br><br>" : "") +*/
|
|
"<div class=\"thr\"><a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Authhash</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?sid=" + tos(sid) + "&mode=viewban\">Ban List</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?sid=" + tos(sid) + "&mode=viewrip\">Reserved List</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?sid=" + tos(sid) + "&mode=viewagent\">User Agent List</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"index.html?sid=" + tos(sid) + "\">Stream Logout</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi\">Server Login "
|
|
"<img border=\"0\" title=\"Server Login\nPassword Required\" alt=\"Server Login\nPassword Required"
|
|
"\" style=\"vertical-align:middle\" src=\"images/lock.png\"></a></div>"
|
|
"</div></td></tr></table></td></tr></table>";
|
|
}
|
|
|
|
utf8 getServerAdminHeader(const utf8& headerTitle, const int refreshRequired = 0,
|
|
const utf8& childPage = "", const int style = 0)
|
|
{
|
|
return "<!DOCTYPE html><html><head>"
|
|
"<meta charset=\"utf-8\">"
|
|
"<meta name=viewport content=\"width=device-width, initial-scale=1\">"
|
|
"<title>Shoutcast Server Administrator</title>"
|
|
"<link href=\"index.css\" rel=\"stylesheet\" type=\"text/css\">"
|
|
"<link href=\"images/favicon.ico\" rel=\"shortcut icon\" type=\"" +
|
|
gOptions.faviconFileMimeType() + "\">" +
|
|
|
|
(abs(refreshRequired) > 0 ? "<meta http-equiv=\"refresh\"content=\"" +
|
|
tos(abs(refreshRequired)) + "; url=admin.cgi?sid=0" + childPage + "\">" : "") +
|
|
|
|
(style ? "<style type=\"text/css\">" +
|
|
(style == 1 ? ".s,.t,.st{border-style:solid;border-color:#CCCCCC;padding:0.2em 1em;text-align:center;}"
|
|
".s,.t,.st{border-width:1px;}"
|
|
/* this fixes a FF quirk with some of the edges being hidden*/
|
|
".infh{position:static;}" :
|
|
(style == 2 ? "li img{vertical-align:bottom;}li{padding-bottom:0.5em;}" : (utf8)"")) + "</style>" : (utf8)"") +
|
|
|
|
"</head><body style=\"margin:0;\">"
|
|
"<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr>"
|
|
"<td><div class=\"logo\">Shoutcast " + headerTitle + "</div></td>"
|
|
"<td style=\"text-align:right;vertical-align:bottom;padding-right:0.1em;\">"
|
|
"<div id=\"up\"></div><a target=\"_blank\" title=\"Built: " __DATE__"\" "
|
|
"href=\"http://www.shoutcast.com\">Shoutcast Server v" +
|
|
addWBR(gOptions.getVersionBuildStrings() + "/" SERV_OSNAME) + "</a></td></tr>"
|
|
"<tr><td class=\"thr\" align=\"center\" colspan=\"3\">"
|
|
"<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">"
|
|
"<tr><td class=\"thr\" align=\"center\"><div id=\"hdrbox\" class=\"tnl\" "
|
|
"style=\"justify-content:space-around;display:flex;flex-flow:row wrap;\">"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?mode=help\">Help & Documentation</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?mode=bandwidth\">Bandwidth Usage</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?mode=viewlog&server=" + randomId(logId) + "\">Log</a> "
|
|
"(<a href=\"admin.cgi?mode=viewlog&server=" + logId + "&viewlog=tail\">Tailing</a> | "
|
|
"<a href=\"admin.cgi?mode=viewlog&server=" + logId + "&viewlog=save\">Save</a>)</div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi\">Summary</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?mode=viewban\">Ban List</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?mode=viewrip\">Reserved List</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"admin.cgi?mode=viewagent\">User Agent List</a></div>"
|
|
"<div class=\"thr\"> | </div>"
|
|
"<div class=\"thr\"><a href=\"index.html\">Server Logout </a></div>"
|
|
"</div></td></tr></table></td></tr></table>";
|
|
}
|
|
|
|
static utf8 formatSizeString(const __uint64 size)
|
|
{
|
|
utf8::value_type buf[128] = {0};
|
|
if (size < 1024)
|
|
{
|
|
snprintf((char *)buf, sizeof(buf), "%llu B", size);
|
|
}
|
|
else if(size < 1048576)
|
|
{
|
|
snprintf((char *)buf, sizeof(buf), "%.02f KiB", size/1024.0f);
|
|
}
|
|
else if(size < 1073741824)
|
|
{
|
|
snprintf((char *)buf, sizeof(buf), "%.02f MiB", size/1048576.0f);
|
|
}
|
|
else if(size < 1099511627776LL)
|
|
{
|
|
snprintf((char *)buf, sizeof(buf), "%.02f GiB", size/1073741824.0f);
|
|
}
|
|
else
|
|
{
|
|
snprintf((char *)buf, sizeof(buf), "%.02f TiB", size/1099511627776.0f);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
utf8 getCheckedDuration(const size_t time)
|
|
{
|
|
if (time >= 60)
|
|
{
|
|
if (time < 3600)
|
|
{
|
|
size_t min = (time / 60);
|
|
return (tos(min) + " minute" + (min != 1 ? "s" : "") + " ago");
|
|
}
|
|
else if (time < 86400)
|
|
{
|
|
size_t hour = (time / 3600);
|
|
return (tos(hour) + " hour" + (hour != 1 ? "s" : "") + " ago");
|
|
}
|
|
else
|
|
{
|
|
size_t week = (time / 86400);
|
|
return (tos(week) + " week" + (week != 1 ? "s" : "") + " ago");
|
|
}
|
|
}
|
|
return "less than a minute ago";
|
|
}
|
|
|
|
utf8 niceURL(utf8 srcAddr)
|
|
{
|
|
if (!srcAddr.empty())
|
|
{
|
|
utf8::size_type pos = srcAddr.find(utf8("://"));
|
|
if (pos != utf8::npos && ((pos == 4) || (pos == 5)))
|
|
{
|
|
srcAddr = srcAddr.substr(pos + 3);
|
|
}
|
|
srcAddr = aolxml::escapeXML(srcAddr);
|
|
|
|
// look for a /stream/x/ path and strip off the end / so the
|
|
// link goes to the admin page instead of playing the stream
|
|
if (!srcAddr.empty())
|
|
{
|
|
utf8::size_type pos2 = srcAddr.find(utf8("/stream/")),
|
|
pos3 = srcAddr.rfind(utf8("/"));
|
|
if ((pos2 != utf8::npos) &&
|
|
((pos3 != utf8::npos) && (pos3 > pos2) &&
|
|
(pos3 == srcAddr.size()-1)))
|
|
{
|
|
srcAddr = srcAddr.substr(0, pos3);
|
|
}
|
|
}
|
|
}
|
|
return srcAddr;
|
|
}
|
|
|
|
void restartRelay(const config::streamConfig &info) throw()
|
|
{
|
|
bool noEntry = false;
|
|
const int relayActive = (streamData::isRelayActive(info.m_streamID, noEntry) & 12);
|
|
if (!relayActive || !noEntry)
|
|
{
|
|
threadedRunner::scheduleRunnable(new protocol_relay(info));
|
|
}
|
|
}
|
|
|
|
void checkVersion(const time_t t)
|
|
{
|
|
utf8 tempId;
|
|
httpHeaderMap_t queryParameters;
|
|
queryParameters["id"] = randomId(tempId);
|
|
|
|
yp2::runAuthHashAction(tempId, yp2::VER_CHECK, "/yp2", queryParameters,
|
|
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"\
|
|
"<yp version=\"2\"><cmd op=\"version\" seq=\"1\">"\
|
|
"<dnas>" + gOptions.getVersionBuildStrings() + "/" SERV_OSNAME "</dnas></cmd></yp>");
|
|
|
|
last_update_check = t;
|
|
}
|
|
|
|
utf8 getUptimeScript(const bool base = false, const bool stream = false, const time_t streamUptime = 0)
|
|
{
|
|
// TODO need to consider improving this to better deal with leap years, etc
|
|
// used to increment the uptime value on the server admin page
|
|
return (!base ? "<script type=\"text/javascript\">"
|
|
"function $(id){return document.getElementById(id);}" EL : (utf8)"") +
|
|
"function pad(num){return(num<10?'0':'')+num;}" EL
|
|
"var i=" + tos((::time(NULL) - g_upTime)) + ";" EL
|
|
|
|
"function time(t,slim){" EL
|
|
"var min=parseInt(t/60);" EL
|
|
"var sec=t-parseInt(min*60);" EL
|
|
"var hours=parseInt(min/60);" EL
|
|
"min-=parseInt(hours*60);" EL
|
|
|
|
"var r=\"\";" EL
|
|
"var days=parseInt(hours/24);" EL
|
|
"hours-=parseInt(days*24);" EL
|
|
"var weeks=parseInt(days/7);" EL
|
|
"days-=parseInt(weeks*7);" EL
|
|
"var years=parseInt(weeks/52);" EL
|
|
"weeks-=parseInt(years*52);" EL
|
|
"if(years)r+=years+\" year\"+(years!=1?\"s\":\"\")+\" \";" EL
|
|
"if(weeks)r+=weeks+\" week\"+(weeks!=1?\"s\":\"\")+\" \";" EL
|
|
"if(days)r+=days+\" day\"+(days!=1?\"s\":\"\")+\" \";" EL
|
|
"if(slim){" EL
|
|
"r+=pad(hours)+\":\"+pad(min)+\":\"+pad(sec);" EL
|
|
"}else{" EL
|
|
"if(hours)r+=hours+\" hour\"+(hours!=1?\"s\":\"\")+\" \";" EL
|
|
"if(min)r+=min+\" minute\"+(min!=1?\"s\":\"\")+\" \";" EL
|
|
"if(sec)r+=sec+\" second\"+(sec!=1?\"s\":\"\");" EL
|
|
"}" EL
|
|
"return r;" EL
|
|
"}" EL
|
|
|
|
"function count(){i++;$('up').innerHTML=\"" + (sDaemon ?
|
|
#ifdef _WIN32
|
|
"Service"
|
|
#else
|
|
"Daemon"
|
|
#endif
|
|
: "") + " Uptime: \"+(!i?\"Starting…\":time(i,1));}" EL
|
|
"count();setInterval(count,1000);" EL +
|
|
|
|
// used to increment the uptime value on the stream admin pages
|
|
(stream ?
|
|
"var is=" + tos(streamUptime) + ";" EL
|
|
"function counts(){is++;$('up2').innerHTML=\"<b>\"+(!is?\"Starting…\":time(is,0))+\"<\\/b>\";}" EL
|
|
"counts();setInterval(counts,1000);" EL : "") +
|
|
|
|
(!base ? "</script>" : "");
|
|
}
|
|
|
|
const bool reloadConfig(const int force)
|
|
{
|
|
bool m_reloadRefresh = false;
|
|
|
|
ILOG(gOptions.logSectionName() + "Starting stream config reload from `" + fileUtil::getFullFilePath(gOptions.confFile()) + "'");
|
|
config newOptions;
|
|
newOptions.load(gOptions.confFile());
|
|
vector<config::streamConfig> relays;
|
|
|
|
// to ease testing, especially on remote systems, will
|
|
// allow toggling of the debugging options for v2.1+
|
|
ILOG(gOptions.logSectionName() + "Processing global configuration settings...");
|
|
|
|
if (newOptions.cdn() != gOptions.cdn())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing CDN mode from " + (!gOptions.cdn().empty() ? gOptions.cdn() : "off") +
|
|
" to " + (!newOptions.cdn().empty() ? newOptions.cdn() : "off"));
|
|
try
|
|
{
|
|
gOptions.setOption(utf8("cdn"),utf8(newOptions.cdn()));
|
|
}
|
|
catch(const exception &)
|
|
{
|
|
}
|
|
}
|
|
|
|
if (newOptions.maxUser() != gOptions.maxUser())
|
|
{
|
|
const int old_maxUser = gOptions.maxUser(),
|
|
new_maxUser = newOptions.maxUser();
|
|
ILOG(gOptions.logSectionName() + "Changing server maxuser from " +
|
|
(old_maxUser > 0 ? tos(old_maxUser) : "unlimited") + " to " +
|
|
(new_maxUser > 0 ? tos(new_maxUser) : "unlimited"));
|
|
gOptions.setOption(utf8("maxuser"),utf8(tos(newOptions.maxUser())));
|
|
}
|
|
|
|
if (newOptions.maxBitrate() != gOptions.maxBitrate())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing server maxbitrate from " +
|
|
(gOptions.maxBitrate() > 0 ? tos(gOptions.maxBitrate()) + "bps" : "unlimited") + " to " +
|
|
(newOptions.maxBitrate() > 0 ? tos(newOptions.maxBitrate()) + "bps" : "unlimited"));
|
|
gOptions.setOption(utf8("maxbitrate"),utf8(tos(newOptions.maxBitrate())));
|
|
}
|
|
|
|
if (newOptions.minBitrate() != gOptions.minBitrate())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing server minbitrate from " +
|
|
(gOptions.minBitrate() > 0 ? tos(gOptions.minBitrate()) + "bps" : "unlimited") + " to " +
|
|
(newOptions.minBitrate() > 0 ? tos(newOptions.minBitrate()) + "bps" : "unlimited"));
|
|
gOptions.setOption(utf8("minbitrate"),utf8(tos(newOptions.minBitrate())));
|
|
}
|
|
|
|
bool publicChanged = false;
|
|
if (newOptions.publicServer() != gOptions.publicServer())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing state of publicserver - killing sources as applicable");
|
|
gOptions.setOption(utf8("publicserver"),newOptions.publicServer());
|
|
publicChanged = true;
|
|
}
|
|
|
|
// update the server-wide hiding and redirection options
|
|
if (newOptions.hideStats() != gOptions.hideStats())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'hidestats'");
|
|
gOptions.setOption(utf8("hidestats"),utf8(newOptions.hideStats()));
|
|
}
|
|
|
|
if (newOptions.redirectUrl() != gOptions.redirectUrl())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'redirecturl'");
|
|
gOptions.setOption(utf8("redirecturl"),utf8(newOptions.redirectUrl()));
|
|
}
|
|
|
|
if (newOptions.ripOnly() != gOptions.ripOnly())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'riponly'");
|
|
gOptions.setOption(utf8("riponly"),utf8(tos(newOptions.ripOnly())));
|
|
}
|
|
|
|
if (newOptions.blockEmptyUserAgent() != gOptions.blockEmptyUserAgent())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'blockemptyuseragent'");
|
|
gOptions.setOption(utf8("blockemptyuseragent"),utf8(tos(newOptions.blockEmptyUserAgent())));
|
|
}
|
|
|
|
if (newOptions.metricsMaxQueue() != gOptions.metricsMaxQueue())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'metricsmaxqueue'");
|
|
gOptions.setOption(utf8("metricsmaxqueue"),utf8(tos(newOptions.metricsMaxQueue())));
|
|
}
|
|
|
|
metrics::metrics_apply(newOptions);
|
|
|
|
if (newOptions.relayReconnectTime() != gOptions.relayReconnectTime())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'relayreconnecttime'");
|
|
gOptions.setOption(utf8("relayreconnecttime"),utf8(tos(newOptions.relayReconnectTime())));
|
|
}
|
|
|
|
if (newOptions.relayConnectRetries() != gOptions.relayConnectRetries())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'relayconnectretries'");
|
|
gOptions.setOption(utf8("relayconnectretries"),utf8(tos(newOptions.relayConnectRetries())));
|
|
}
|
|
|
|
if (newOptions.backupLoop() != gOptions.backupLoop())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'backuploop'");
|
|
gOptions.setOption(utf8("backuploop"),utf8(tos(newOptions.backupLoop())));
|
|
}
|
|
|
|
if (newOptions.songHistory() != gOptions.songHistory())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'songhistory'");
|
|
gOptions.setOption(utf8("songhistory"),utf8(tos(newOptions.songHistory())));
|
|
}
|
|
|
|
if (newOptions.adminFile() != gOptions.adminFile())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'adminfile'");
|
|
gOptions.setOption(utf8("adminfile"),newOptions.adminFile());
|
|
}
|
|
|
|
if (newOptions.clacks() != gOptions.clacks())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'clacks'");
|
|
gOptions.setOption(utf8("clacks"),utf8(tos(newOptions.clacks())));
|
|
}
|
|
|
|
if (newOptions.startInactive() != gOptions.startInactive())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'startinactive'");
|
|
gOptions.setOption(utf8("startinactive"),utf8(tos(newOptions.startInactive())));
|
|
}
|
|
|
|
if (newOptions.rateLimit() != gOptions.rateLimit())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'rateLimit'");
|
|
gOptions.setOption(utf8("ratelimit"),utf8(tos(newOptions.rateLimit())));
|
|
}
|
|
|
|
if (newOptions.adTestFile() != gOptions.adTestFile())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'adtestfile'");
|
|
gOptions.setOption(utf8("adtestfile"),utf8(newOptions.adTestFile()));
|
|
}
|
|
|
|
if (newOptions.adTestFile2() != gOptions.adTestFile2())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'adtestfile2'");
|
|
gOptions.setOption(utf8("adtestfile2"),utf8(newOptions.adTestFile2()));
|
|
}
|
|
|
|
if (newOptions.adTestFile3() != gOptions.adTestFile3())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'adtestfile3'");
|
|
gOptions.setOption(utf8("adtestfile3"),utf8(newOptions.adTestFile3()));
|
|
}
|
|
|
|
if (newOptions.adTestFile4() != gOptions.adTestFile4())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'adtestfile4'");
|
|
gOptions.setOption(utf8("adtestfile4"),utf8(newOptions.adTestFile4()));
|
|
}
|
|
|
|
if (newOptions.adTestFileLoop() != gOptions.adTestFileLoop())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'adtestfileloop'");
|
|
gOptions.setOption(utf8("adtestfileloop"),utf8(tos(newOptions.adTestFileLoop())));
|
|
}
|
|
|
|
if (newOptions.metaInterval() != gOptions.metaInterval())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'metainterval'");
|
|
gOptions.setOption(utf8("metainterval"),utf8(tos(newOptions.metaInterval())));
|
|
}
|
|
|
|
if (newOptions.useXFF() != gOptions.useXFF())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'usexff'");
|
|
gOptions.setOption(utf8("usexff"),utf8(tos(newOptions.useXFF())));
|
|
}
|
|
|
|
if (newOptions.forceShortSends() != gOptions.forceShortSends())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'forceshortsends'");
|
|
gOptions.setOption(utf8("forceshortsends"),utf8(tos(newOptions.forceShortSends())));
|
|
}
|
|
|
|
if (newOptions.adminNoWrap() != gOptions.adminNoWrap())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'adminnowrap'");
|
|
gOptions.setOption(utf8("adminnowrap"),utf8(tos(newOptions.adminNoWrap())));
|
|
}
|
|
|
|
if (newOptions.adminCSSFile() != gOptions.adminCSSFile())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'admincssfile'");
|
|
gOptions.setOption(utf8("admincssfile"),utf8(newOptions.adminCSSFile()));
|
|
gOptions.m_styleCustomStr.clear();
|
|
gOptions.m_styleCustomStrGZ.clear();
|
|
gOptions.m_styleCustomHeaderTime = 0;
|
|
}
|
|
|
|
if (newOptions.destIP() != gOptions.destIP())
|
|
{
|
|
utf8 destBindAddr = metrics::metrics_verifyDestIP(newOptions, false);
|
|
ILOG(gOptions.logSectionName() + "Changing 'destip'");
|
|
gOptions.setOption(utf8("destip"), destBindAddr);
|
|
|
|
// if we're updating then attempt to behave like it was a new load
|
|
g_IPAddressForClients = destBindAddr;
|
|
|
|
if (g_IPAddressForClients.empty())
|
|
{
|
|
char s[MAXHOSTNAMELEN] = {0};
|
|
if (!::gethostname(s,MAXHOSTNAMELEN - 1))
|
|
{
|
|
g_IPAddressForClients = socketOps::hostNameToAddress(s,g_portForClients);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newOptions.publicIP() != gOptions.publicIP())
|
|
{
|
|
utf8 publicAddr = stripWhitespace(newOptions.publicIP());
|
|
publicAddr = stripHTTPprefix(publicAddr);
|
|
ILOG(gOptions.logSectionName() + "Changing 'publicip'");
|
|
gOptions.setOption(utf8("publicip"),publicAddr);
|
|
}
|
|
|
|
if (newOptions.autoDumpTime() != gOptions.autoDumpTime())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'autodumptime'");
|
|
gOptions.setOption(utf8("autodumptime"),utf8(tos(newOptions.autoDumpTime())));
|
|
}
|
|
|
|
if (newOptions.nameLookups() != gOptions.nameLookups())
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Changing 'namelookups'");
|
|
gOptions.setOption(utf8("namelookups"),utf8(tos(newOptions.nameLookups())));
|
|
}
|
|
|
|
// update the YP details (can do on fly without any actual updates)
|
|
bool ypChanged = false;
|
|
bool https = ((gOptions.ypAddr() == DEFAULT_YP_ADDRESS) && uniFile::fileExists(gOptions.m_certPath));
|
|
utf8 oldYP = (https ? "https://" : "http://") + gOptions.ypAddr() + ":" + tos(gOptions.ypPort()) + gOptions.ypPath();
|
|
if (newOptions.ypAddr() != gOptions.ypAddr())
|
|
{
|
|
ypChanged = true;
|
|
gOptions.setOption(utf8("ypaddr"),utf8(newOptions.ypAddr()));
|
|
}
|
|
if (newOptions.ypPort() != gOptions.ypPort())
|
|
{
|
|
ypChanged = true;
|
|
gOptions.setOption(utf8("ypport"),utf8(tos(newOptions.ypPort())));
|
|
}
|
|
if (newOptions.ypPath() != gOptions.ypPath())
|
|
{
|
|
ypChanged = true;
|
|
gOptions.setOption(utf8("yppath"),utf8(newOptions.ypPath()));
|
|
}
|
|
|
|
if (ypChanged)
|
|
{
|
|
bool https = ((newOptions.ypAddr() == DEFAULT_YP_ADDRESS) && uniFile::fileExists(gOptions.m_certPath));
|
|
utf8 newYP = (https ? "https://" : "http://") + newOptions.ypAddr() + ":" + tos(newOptions.ypPort()) + newOptions.ypPath();
|
|
ILOG(gOptions.logSectionName() + "Changing YP details from " + oldYP + " to " + newYP);
|
|
}
|
|
|
|
gOptions.setOption(utf8("yp2debug"),utf8(newOptions.yp2Debug()?"1":"0"));
|
|
gOptions.setOption(utf8("shoutcastsourcedebug"),utf8(newOptions.shoutcastSourceDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("uvox2sourcedebug"),utf8(newOptions.uvox2SourceDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("httpsourcedebug"),utf8(newOptions.HTTPSourceDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("shoutcast1clientdebug"),utf8(newOptions.shoutcast1ClientDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("shoutcast2clientdebug"),utf8(newOptions.shoutcast2ClientDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("httpclientdebug"),utf8(newOptions.HTTPClientDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("flvclientdebug"),utf8(newOptions.flvClientDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("m4aclientdebug"),utf8(newOptions.m4aClientDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("relayshoutcastdebug"),utf8(newOptions.relayShoutcastDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("relayuvoxdebug"),utf8(newOptions.relayUvoxDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("relaydebug"),utf8(newOptions.relayDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("streamdatadebug"),utf8(newOptions.streamDataDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("httpstyledebug"),utf8(newOptions.httpStyleDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("statsdebug"),utf8(newOptions.statsDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("microserverdebug"),utf8(newOptions.microServerDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("threadrunnerdebug"),utf8(newOptions.threadRunnerDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("admetricsdebug"),utf8(newOptions.adMetricsDebug()?"1":"0"));
|
|
gOptions.setOption(utf8("authdebug"),utf8(newOptions.authDebug()?"1":"0"));
|
|
|
|
ILOG(gOptions.logSectionName() + "Processed global configuration settings.");
|
|
|
|
// test for the source password having changed this will be
|
|
// applied in general though per stream changes are likely
|
|
// to have been set in the earlier checks before we got here
|
|
if (newOptions.password() != gOptions.password())
|
|
{
|
|
gOptions.setOption(utf8("password"),newOptions.password());
|
|
streamData *sd = streamData::accessStream(DEFAULT_SOURCE_STREAM);
|
|
if (sd)
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Killing all stream sources due to change of relay options.");
|
|
sd->killAllSources();
|
|
m_reloadRefresh = true;
|
|
sd->releaseStream();
|
|
}
|
|
}
|
|
|
|
config::streams_t new_streams, old_streams;
|
|
newOptions.getStreamConfigs(new_streams, false);
|
|
gOptions.getStreamConfigs(old_streams, false);
|
|
|
|
config::streams_t::const_iterator iNSC = new_streams.begin(), iOSC = old_streams.begin();
|
|
|
|
// if no configurations found then we can just remove everything
|
|
if (new_streams.empty())
|
|
{
|
|
// kick the source and clients as required on a removal
|
|
for (; iOSC != old_streams.end(); ++iOSC)
|
|
{
|
|
size_t streamID = (*iOSC).second.m_streamID;
|
|
streamData *sd = streamData::accessStream(streamID);
|
|
if (sd)
|
|
{
|
|
sd->killSource(streamID, sd);
|
|
m_reloadRefresh = true;
|
|
}
|
|
gOptions.removeStreamConfig((*iOSC).second);
|
|
}
|
|
}
|
|
|
|
// otherwise if the same or more then we can update / add as required
|
|
else if (new_streams.size() >= old_streams.size())
|
|
{
|
|
// if no configs specified and we're starting to add new stream configs
|
|
// then we really need to kick any existing sources otherwise it'll stay
|
|
// with the current details which will prevent a YP connection with extra
|
|
// checks in build 21+ to make sure it only works if there was a change.
|
|
if ((new_streams.size() != old_streams.size()) &&
|
|
(old_streams.size() == DEFAULT_SOURCE_STREAM))
|
|
{
|
|
// kick the source and clients as required on a complete addition
|
|
// (wouldn't have a valid config via authhash, etc so is sensible)
|
|
streamData *sd = streamData::accessStream(DEFAULT_SOURCE_STREAM);
|
|
if (sd)
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Forcing stream source disconnect due to addition of stream config(s).");
|
|
sd->killAllSources();
|
|
m_reloadRefresh = true;
|
|
sd->releaseStream();
|
|
}
|
|
}
|
|
|
|
for (; iNSC != new_streams.end(); ++iNSC)
|
|
{
|
|
config::streams_t::const_iterator iOSC2 = old_streams.find((*iNSC).first);
|
|
if (iOSC2 != old_streams.end())
|
|
{
|
|
// update required
|
|
__uint64 updated = gOptions.updateStreamConfig(newOptions, (*iNSC).second);
|
|
size_t streamID = (*iNSC).second.m_streamID;
|
|
streamData *sd = streamData::accessStream(streamID);
|
|
if (sd)
|
|
{
|
|
// otherwise do an in-place update as long as it is applicable
|
|
bool isRelay = sd->isRelayStream(streamID);
|
|
if (publicChanged || force ||
|
|
(!force && (updated & RELAY_URL || updated & SOURCE_PWD ||
|
|
updated & PUBLIC_SRV || (updated & ALLOW_RELAY && isRelay) ||
|
|
(updated & ALLOW_PUBLIC_RELAY && isRelay) || updated & CIPHER_KEY ||
|
|
updated & INTRO_FILE || updated & BACKUP_FILE ||
|
|
updated & BACKUP_URL || updated & MOVED_URL)))
|
|
{
|
|
sd->killSource(streamID);
|
|
m_reloadRefresh = true;
|
|
|
|
if (!(*iNSC).second.m_relayUrl.url().empty() &&
|
|
!sd->isSourceConnected(streamID) &&
|
|
gOptions.stream_movedUrl(streamID).empty())
|
|
{
|
|
relays.push_back((*iNSC).second);
|
|
}
|
|
}
|
|
// otherwise do an in-place update as long as it is applicable
|
|
else
|
|
{
|
|
sd->streamUpdate(streamID, (*iNSC).second.m_authHash, (*iNSC).second.m_maxStreamUser,
|
|
(*iNSC).second.m_maxStreamBitrate, (*iNSC).second.m_minStreamBitrate);
|
|
}
|
|
|
|
// refresh the played history size as needed
|
|
if (updated & SONG_HIST)
|
|
{
|
|
sd->updateSongHistorySize();
|
|
}
|
|
|
|
if (updated & ARTWORK_FILE)
|
|
{
|
|
if (gOptions.read_stream_artworkFile(streamID))
|
|
{
|
|
gOptions.m_artworkBody[streamID] = loadLocalFile(fileUtil::getFullFilePath(gOptions.stream_artworkFile(streamID)), LOGNAME, 523872/*32 x (16384 - 6 - 6 - 1)*/);
|
|
}
|
|
else
|
|
{
|
|
gOptions.m_artworkBody[streamID].clear();
|
|
sd->clearCachedArtwork(0);
|
|
}
|
|
}
|
|
sd->releaseStream();
|
|
}
|
|
// otherwise if no proper update or a relay was added / is known and not connected then bump it
|
|
else
|
|
{
|
|
// force flag a source relay kick if this is called
|
|
bool noEntry = false;
|
|
if ((streamData::isRelayActive(streamID, noEntry) & 12))
|
|
{
|
|
// if there is a relay attempt and no new one
|
|
// then we need to signal it to abort trying
|
|
if ((*iNSC).second.m_relayUrl.url().empty())
|
|
{
|
|
streamData::setRelayActiveFlags (streamID, noEntry, 2);
|
|
}
|
|
// otherwise we need update the relay url
|
|
// which the attempt is trying to join to
|
|
// which is done by the relay handling.
|
|
}
|
|
|
|
if (((*iOSC2).second.m_relayUrl.url() != (*iNSC).second.m_relayUrl.url() ||
|
|
(updated & MOVED_URL)) && !(*iNSC).second.m_relayUrl.url().empty())
|
|
{
|
|
relays.push_back((*iNSC).second);
|
|
m_reloadRefresh = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// addition required
|
|
// no need to do anything else as there shouldn't be anything connected on this at the time
|
|
gOptions.addStreamConfig(newOptions, (*iNSC).second);
|
|
m_reloadRefresh = true;
|
|
|
|
// only attempt to start a relay url if added and a relay url exists
|
|
if (!(*iNSC).second.m_relayUrl.url().empty())
|
|
{
|
|
relays.push_back((*iNSC).second);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// otherwise if there are fewer stream configurations then we can update / remove as required
|
|
else
|
|
{
|
|
for (; iOSC != old_streams.end(); ++iOSC)
|
|
{
|
|
config::streams_t::const_iterator iOSC2 = new_streams.find((*iOSC).first);
|
|
if (iOSC2 != new_streams.end())
|
|
{
|
|
// update required
|
|
__uint64 updated = gOptions.updateStreamConfig(newOptions, (*iOSC2).second);
|
|
size_t streamID = (*iOSC2).second.m_streamID;
|
|
streamData *sd = streamData::accessStream(streamID);
|
|
if (sd)
|
|
{
|
|
// check what has been updated and kick the source as applicable
|
|
bool isRelay = sd->isRelayStream(streamID);
|
|
if (publicChanged || force ||
|
|
(!force && (updated & RELAY_URL || updated & SOURCE_PWD ||
|
|
updated & PUBLIC_SRV || (updated & ALLOW_RELAY && isRelay) ||
|
|
(updated & ALLOW_PUBLIC_RELAY && isRelay) || updated & CIPHER_KEY ||
|
|
updated & INTRO_FILE || updated & BACKUP_FILE ||
|
|
updated & BACKUP_URL || updated & MOVED_URL)))
|
|
{
|
|
sd->killSource(streamID);
|
|
m_reloadRefresh = true;
|
|
|
|
if (!(*iOSC2).second.m_relayUrl.url().empty() &&
|
|
!sd->isSourceConnected(streamID) &&
|
|
gOptions.stream_movedUrl(streamID).empty())
|
|
{
|
|
relays.push_back((*iOSC2).second);
|
|
}
|
|
}
|
|
// otherwise do an in-place update as long as it is applicable
|
|
else
|
|
{
|
|
sd->streamUpdate(streamID, (*iOSC2).second.m_authHash, (*iOSC2).second.m_maxStreamUser,
|
|
(*iOSC2).second.m_maxStreamBitrate, (*iOSC2).second.m_minStreamBitrate);
|
|
}
|
|
|
|
// refresh the played history size as needed
|
|
if (updated & SONG_HIST)
|
|
{
|
|
sd->updateSongHistorySize();
|
|
}
|
|
|
|
if (updated & ARTWORK_FILE)
|
|
{
|
|
if (gOptions.read_stream_artworkFile(streamID))
|
|
{
|
|
gOptions.m_artworkBody[streamID] = loadLocalFile(fileUtil::getFullFilePath(gOptions.stream_artworkFile(streamID)), LOGNAME, 523872/*32 x (16384 - 6 - 6 - 1)*/);
|
|
}
|
|
else
|
|
{
|
|
gOptions.m_artworkBody[streamID].clear();
|
|
sd->clearCachedArtwork(0);
|
|
}
|
|
}
|
|
sd->releaseStream();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// kick the source and clients as required on a removal
|
|
size_t streamID = (*iOSC).second.m_streamID;
|
|
streamData *sd = streamData::accessStream(streamID);
|
|
if (sd)
|
|
{
|
|
sd->killSource(streamID, sd);
|
|
m_reloadRefresh = true;
|
|
}
|
|
// removal required
|
|
gOptions.removeStreamConfig((*iOSC).second);
|
|
}
|
|
}
|
|
}
|
|
|
|
// test for the require stream configs having changed
|
|
// only need to kick streams not known if enabled
|
|
if (newOptions.requireStreamConfigs() != gOptions.requireStreamConfigs())
|
|
{
|
|
gOptions.setOption(utf8("requirestreamconfigs"),utf8(newOptions.requireStreamConfigs()?"1":"0"));
|
|
if (newOptions.requireStreamConfigs())
|
|
{
|
|
size_t inc = 0;
|
|
size_t sid;
|
|
do
|
|
{
|
|
sid = streamData::enumStreams(inc);
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams, false);
|
|
|
|
config::streams_t::const_iterator ics = streams.find(sid);
|
|
if (ics == streams.end())
|
|
{
|
|
streamData *sd = streamData::accessStream(sid);
|
|
if (sd)
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Killing source for stream #" + tos(sid) + " as it is not in the known stream config(s).", LOG_NAME, sid);
|
|
sd->killSource(sid, sd);
|
|
m_reloadRefresh = true;
|
|
}
|
|
}
|
|
++inc;
|
|
}
|
|
while (sid);
|
|
}
|
|
}
|
|
|
|
// finally we refresh the passwords so they're correct after any changes
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams, false);
|
|
gOptions.setupPasswords(streams);
|
|
|
|
// if a force was done then we really need to restart any relays
|
|
if (force)
|
|
{
|
|
// schedule relays
|
|
vector<config::streamConfig> relayList(gOptions.getRelayList());
|
|
if (!relayList.empty())
|
|
{
|
|
for_each(relayList.begin(),relayList.end(),restartRelay);
|
|
}
|
|
}
|
|
// otherwise only attempt to restart any which were added, changed, etc
|
|
else
|
|
{
|
|
if (!relays.empty())
|
|
{
|
|
for_each(relays.begin(),relays.end(),restartRelay);
|
|
}
|
|
}
|
|
|
|
ILOG(gOptions.logSectionName() + "Completed stream config reload from `" + fileUtil::getFullFilePath(gOptions.confFile()));
|
|
return m_reloadRefresh;
|
|
}
|
|
|
|
void reloadBanLists()
|
|
{
|
|
// load up ban file
|
|
int loaded = g_banList.load(gOptions.banFile(),0),
|
|
count = loaded;
|
|
|
|
// per-stream options
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
// load up ban file
|
|
if (gOptions.read_stream_banFile((*i).first))
|
|
{
|
|
++count;
|
|
if (g_banList.load(gOptions.stream_banFile((*i).first),(*i).first))
|
|
{
|
|
++loaded;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!count)
|
|
{
|
|
ILOG("[BAN] No banned lists reloaded.");
|
|
}
|
|
else
|
|
{
|
|
if (count == loaded)
|
|
{
|
|
ILOG("[BAN] Reloaded all banned list(s).");
|
|
}
|
|
else
|
|
{
|
|
ILOG("[BAN] Partially reloaded banned list(s) - check error messages above.");
|
|
}
|
|
}
|
|
}
|
|
|
|
void reloadRipLists()
|
|
{
|
|
// load up rip file
|
|
int loaded = g_ripList.load(gOptions.ripFile(),0),
|
|
count = loaded;
|
|
|
|
// per-stream options
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
// load up rip file
|
|
if (gOptions.read_stream_ripFile((*i).first))
|
|
{
|
|
++count;
|
|
if (g_ripList.load(gOptions.stream_ripFile((*i).first),(*i).first))
|
|
{
|
|
++loaded;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!count)
|
|
{
|
|
ILOG("[RIP] No reserved lists reloaded.");
|
|
}
|
|
else
|
|
{
|
|
if (count == loaded)
|
|
{
|
|
ILOG("[RIP] Reloaded all reserved list(s).");
|
|
}
|
|
else
|
|
{
|
|
ILOG("[RIP] Partially reloaded reserved list(s) - check error messages above.");
|
|
}
|
|
}
|
|
}
|
|
|
|
void reloadAdminAccessList()
|
|
{
|
|
// load up admin access file
|
|
g_adminList.load(gOptions.adminFile());
|
|
ILOG("[ADMINCGI] Reloaded admin access list.");
|
|
}
|
|
|
|
void reloadAgentLists()
|
|
{
|
|
// load up agent file
|
|
int loaded = g_agentList.load(gOptions.agentFile(),0),
|
|
count = loaded;
|
|
|
|
// per-stream options
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
// load up agent file
|
|
if (gOptions.read_stream_agentFile((*i).first))
|
|
{
|
|
++count;
|
|
if (g_agentList.load(gOptions.stream_agentFile((*i).first),(*i).first))
|
|
{
|
|
++loaded;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!count)
|
|
{
|
|
ILOG("[AGENT] No user agent lists reloaded.");
|
|
}
|
|
else
|
|
{
|
|
if (count == loaded)
|
|
{
|
|
ILOG("[AGENT] Reloaded all user agent list(s).");
|
|
}
|
|
else
|
|
{
|
|
ILOG("[AGENT] Partially reloaded user agent list(s) - check error messages above.");
|
|
}
|
|
}
|
|
}
|
|
|
|
void printUpdateMessage()
|
|
{
|
|
// display update message where applicable
|
|
updater::verInfo ver;
|
|
if (updater::getNewVersion(ver))
|
|
{
|
|
ULOG(string(YP2_LOGNAME) + "A new DNAS version is now available: " + ver.ver);
|
|
ULOG(string(YP2_LOGNAME) + "The suggested download for your setup is: " + ver.url);
|
|
ULOG(string(YP2_LOGNAME) + "See " + ver.log + " for more information about this update and alternative download links");
|
|
}
|
|
}
|
|
|
|
utf8 warningImage(const bool right = true)
|
|
{
|
|
return utf8(right ? " " : "") + "<img border=\"0\" src=\"images/warn.png\" style=\"float:" +
|
|
utf8(right ? "right" : "left") + "\" "
|
|
"alt=\"" + (right ? "Possible Stream Ripper" : "Please Register Your Authhash") +
|
|
"\" title=\"" + (right ? "Possible Stream Ripper" : "Please Register Your Authhash") + "\">";
|
|
}
|
|
|
|
utf8 baseImage(const utf8& image, const utf8& title, const bool left = false, const bool prefix = true)
|
|
{
|
|
return (prefix ? " " : (utf8)"") + "<img border=\"0\" src=\"images/" +
|
|
(!image.empty() ? (image + ".png") : "favicon.ico") + "\" " +
|
|
(left ? "style=\"float:left\" " : "") + "alt=\"" + title + "\" title=\"" + title + "\">";
|
|
}
|
|
|
|
#define xffImage() baseImage("xff", "XFF", true)
|
|
#define mplayerImage() baseImage("mplayer", "MPlayer Client")
|
|
#define psImage() baseImage("ps", "PlayStation Client")
|
|
#define radioToolboxImage() baseImage("rtb", "Radio Toolbox Monitor / Player")
|
|
#define v1Image() baseImage("v1", "v1 Client")
|
|
#define v2Image() baseImage("v2", "v2 Client")
|
|
#define relayImage() baseImage("relay", "Relay Connection", false, false)
|
|
#define html5Image() baseImage("html5", "HTTP / HTML5 Client")
|
|
#define flashImage() baseImage("flash", "Flash Client")
|
|
#define icecastImage() baseImage("icecast", "Icecast Client / Relay")
|
|
#define vlcImage() baseImage("vlc", "VLC Client")
|
|
#define waImage() baseImage("wa", "Winamp Client")
|
|
#define scImage() baseImage("", "Shoutcast Client / Relay")
|
|
#define iOSImage() baseImage("", "iOS Client")
|
|
#define curlImage() baseImage("curl", "cURL / libcurl Based Client")
|
|
#define radionomyImage() baseImage("radionomy", "Radionomy Stats Collector")
|
|
#define fb2kImage() baseImage("fb2k", "Foobar2000 Client")
|
|
#define rokuImage() baseImage("roku", "Roku Streaming Player")
|
|
#define WiiMCImage() baseImage("v1", "Wii Media Centre Player")
|
|
#define synologyImage() baseImage("synology", "Audio Station (Synology) Player")
|
|
#define appleImage() baseImage("apple", "Apple Device / OS / Client")
|
|
#define iTunesImage() baseImage("itunes", "iTunes Client")
|
|
#define wmpImage() baseImage("wmp", "Windows Media Player Client")
|
|
#define chromeImage() baseImage("chrome", "Chrome Web Browser")
|
|
#define safariImage() baseImage("safari", "Safari Web Browser")
|
|
#define ieImage() baseImage("ie", "Internet Explorer Web Browser")
|
|
#define firefoxImage() baseImage("firefox", "Firefox Web Browser")
|
|
|
|
utf8 advertImage(const streamData::streamID_t sid, const int group, const size_t count)
|
|
{
|
|
if (streamData::knownAdvertGroup(sid, group))
|
|
{
|
|
if (count > 0)
|
|
{
|
|
return baseImage("adplayed", (tos(count) + " Advert Breaks Have Been Played\nAdvert Group: " + tos(group)), false, false);
|
|
}
|
|
return baseImage("adavail", ("Waiting For First Advert Break To Play\nAdvert Group: " + tos(group)), false, false);
|
|
}
|
|
if (count > 0)
|
|
{
|
|
return baseImage("", (tos(count) + " Advert Breaks Have Been Played\nNo Applicable "
|
|
"Adverts Currently Available\nAdvert Group: " + tos(group)), false, false);
|
|
}
|
|
|
|
return baseImage("noadavail", ("No Applicable Adverts Available\nAdvert Group: " + tos(group)), false, false);
|
|
}
|
|
|
|
utf8 getClientImage(const streamData::source_t type)
|
|
{
|
|
return ((type & streamData::RADIONOMY) ? radionomyImage() :
|
|
((type & streamData::FLV) ? flashImage() :
|
|
((type & streamData::CURL_TOOL) ? curlImage() :
|
|
((type & streamData::HTTP) ? html5Image() :
|
|
//((type & streamData::M4A) ? m4aImage() :
|
|
((type & streamData::SHOUTCAST2) ? ((type & streamData::RELAY) ? scImage() : waImage()) :
|
|
((type & streamData::ICECAST) ? icecastImage() :
|
|
((type & streamData::VLC) ? vlcImage() :
|
|
((type & streamData::WINAMP) ? waImage() :
|
|
((type & streamData::APPLE) ? appleImage() :
|
|
((type & streamData::ITUNES) ? iTunesImage() :
|
|
((type & streamData::WMP) ? wmpImage() :
|
|
((type & streamData::ROKU) ? rokuImage() :
|
|
((type & streamData::WIIMC) ? WiiMCImage() :
|
|
((type & streamData::SYNOLOGY) ? synologyImage() :
|
|
((type & streamData::CHROME) ? chromeImage() :
|
|
((type & streamData::SAFARI) ? safariImage() :
|
|
((type & streamData::IE) ? ieImage() :
|
|
((type & streamData::FIREFOX) ? firefoxImage() :
|
|
((type & streamData::MPLAYER) ? mplayerImage() :
|
|
((type & streamData::PS) ? psImage() :
|
|
((type & streamData::RADIO_TOOLBOX) ? radioToolboxImage() :
|
|
((type & streamData::HTML5) ? html5Image() :
|
|
((type & streamData::WARNING) ? warningImage() :
|
|
((type & streamData::SC_IRADIO) ? iOSImage() :
|
|
((type & streamData::FB2K) ? fb2kImage() : v1Image()
|
|
))))))))))))))))))))))))) +
|
|
((type & streamData::RELAY) ? relayImage() : "");
|
|
}
|
|
|
|
/*
|
|
Handles all HTTP requests to admin.cgi
|
|
*/
|
|
|
|
protocol_admincgi::protocol_admincgi(const socketOps::tSOCKET s, const streamData::streamID_t sid, const bool no_sid,
|
|
const bool zero_sid, const utf8 &clientLogString,
|
|
const uniString::utf8 &password, const uniString::utf8 &referer,
|
|
const uniString::utf8 &hostIP, const uniString::utf8 &userAgent,
|
|
const protocol_HTTPStyle::HTTPRequestInfo &httpRequestInfo) throw(std::exception)
|
|
: runnable(s), m_noSID(no_sid), m_zeroSID(zero_sid),
|
|
m_saveLogFile(0), m_clientLogString(clientLogString),
|
|
m_httpRequestInfo(httpRequestInfo), m_password(password),
|
|
m_referer(referer), m_hostIP(hostIP), m_userAgent(userAgent),
|
|
m_sid(sid), m_state(&protocol_admincgi::state_ConfirmPassword),
|
|
m_nextState(0), m_outBuffer(0), m_outBufferSize(0),
|
|
m_tailLogFile(false), lastChar(-1), inMsg(false),
|
|
first(false), m_logFile(0)
|
|
{
|
|
memset(&m_stream, 0, sizeof(m_stream));
|
|
|
|
// check for updates weekly from the last update
|
|
// so we're taking into account manual checks...
|
|
if ((m_lastActivityTime - last_update_check) > 604800)
|
|
{
|
|
checkVersion(m_lastActivityTime);
|
|
}
|
|
|
|
DEBUG_LOG(utf8(LOGNAME) + __FUNCTION__);
|
|
}
|
|
|
|
protocol_admincgi::~protocol_admincgi() throw()
|
|
{
|
|
DEBUG_LOG(utf8(LOGNAME) + __FUNCTION__);
|
|
|
|
socketOps::forgetTCPSocket(m_socket);
|
|
if (m_logFile)
|
|
{
|
|
::fclose(m_logFile);
|
|
m_logFile = 0;
|
|
}
|
|
}
|
|
|
|
void protocol_admincgi::timeSlice() throw(exception)
|
|
{
|
|
(this->*m_state)();
|
|
}
|
|
|
|
void protocol_admincgi::state_Close() throw(exception)
|
|
{
|
|
m_result.done();
|
|
}
|
|
|
|
// send buffer text
|
|
void protocol_admincgi::state_Send() throw(exception)
|
|
{
|
|
if (sendDataBuffer(DEFAULT_CLIENT_STREAM_ID, m_outBuffer, m_outBufferSize, m_clientLogString))
|
|
{
|
|
m_state = m_nextState;
|
|
}
|
|
}
|
|
|
|
void protocol_admincgi::sendMessageAndClose(const utf8 &msg) throw()
|
|
{
|
|
m_outMsg = msg;
|
|
m_outBuffer = m_outMsg.c_str();
|
|
bandWidth::updateAmount(bandWidth::PRIVATE_WEB, (m_outBufferSize = (int)m_outMsg.size()));
|
|
m_state = &protocol_admincgi::state_Send;
|
|
m_nextState = &protocol_admincgi::state_Close;
|
|
m_result.write();
|
|
m_result.run();
|
|
}
|
|
|
|
/// update the metatdata in the shoutcast ring buffer
|
|
void protocol_admincgi::state_UpdateMetadata() throw(exception)
|
|
{
|
|
DEBUG_LOG(utf8(LOGNAME) + __FUNCTION__);
|
|
|
|
streamData::streamInfo info;
|
|
streamData::extraInfo extra;
|
|
streamData::getStreamInfo(m_sid, info, extra);
|
|
|
|
utf8 titleMsg, urlMsg, djMsg, nextMsg;
|
|
|
|
if (!m_updinfoSong.empty())
|
|
{
|
|
// use this as a way to try to ensure we've got a utf-8
|
|
// encoded title to improve legacy source title support
|
|
if (!m_updinfoSong.isValid())
|
|
{
|
|
m_updinfoSong = asciiToUtf8(m_updinfoSong.toANSI(true));
|
|
}
|
|
|
|
if (streamData::validateTitle(m_updinfoSong))
|
|
{
|
|
titleMsg = ("[ADMINCGI sid=" + tos(m_sid) + "] Title updated [" + m_updinfoSong + "]");
|
|
}
|
|
else
|
|
{
|
|
WLOG("Title update rejected - value not allowed: " + m_updinfoSong, LOG_NAME, m_sid);
|
|
m_updinfoSong = info.m_currentSong;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!info.m_currentSong.empty())
|
|
{
|
|
titleMsg = ("[ADMINCGI sid=" + tos(m_sid) + "] Title cleared");
|
|
}
|
|
}
|
|
|
|
if (!m_updinfoURL.empty())
|
|
{
|
|
urlMsg = ("[ADMINCGI sid=" + tos(m_sid) + "] Song url updated [" + m_updinfoURL + "]");
|
|
}
|
|
else
|
|
{
|
|
if (!info.m_currentURL.empty())
|
|
{
|
|
// TODO if there's a custom image then indicate using it or do not show this
|
|
urlMsg = ("[ADMINCGI sid=" + tos(m_sid) + "] Song url cleared");
|
|
}
|
|
}
|
|
|
|
if (!m_updinfoDJ.empty())
|
|
{
|
|
djMsg = ("[ADMINCGI sid=" + tos(m_sid) + "] DJ name updated [" + m_updinfoDJ + "]");
|
|
}
|
|
else
|
|
{
|
|
if (!info.m_streamUser.empty())
|
|
{
|
|
djMsg = ("[ADMINCGI sid=" + tos(m_sid) + "] DJ name cleared");
|
|
}
|
|
}
|
|
|
|
if (!m_updinfoNext.empty())
|
|
{
|
|
// use this as a way to try to ensure we've got a utf-8
|
|
// encoded title to improve legacy source title support
|
|
if (!m_updinfoNext.isValid())
|
|
{
|
|
m_updinfoNext = asciiToUtf8(m_updinfoNext.toANSI(true));
|
|
}
|
|
|
|
if (streamData::validateTitle(m_updinfoNext))
|
|
{
|
|
titleMsg = ("[ADMINCGI sid=" + tos(m_sid) + "] Next title updated [" + m_updinfoNext + "]");
|
|
}
|
|
else
|
|
{
|
|
WLOG("[ADMINCGI sid=" + tos(m_sid) + "] Next title update rejected - value not allowed: " + m_updinfoNext);
|
|
m_updinfoNext = info.m_comingSoon;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!info.m_comingSoon.empty())
|
|
{
|
|
nextMsg = ("[ADMINCGI sid=" + tos(m_sid) + "] Next title cleared");
|
|
}
|
|
}
|
|
|
|
streamData *sd = streamData::accessStream(m_sid);
|
|
if (sd)
|
|
{
|
|
utf8 sourceIdent = (!m_userAgent.empty() ? m_userAgent : utf8("Legacy / Unknown"));
|
|
sd->updateSourceIdent(sourceIdent, sd->isRelayStream(m_sid));
|
|
sd->addSc1MetadataAtCurrentPosition(LOGNAME, m_updinfoSong, m_updinfoURL, m_updinfoNext);
|
|
sd->updateStreamUser(m_updinfoDJ);
|
|
// this will call streamData::releaseStream(..)
|
|
streamData::streamClientLost(LOGNAME, sd, m_sid);
|
|
|
|
// we'll output any earlier generated messages now that we've sent
|
|
// the details to be used as otherwise they were being reported as
|
|
// ok which if there was an issue (e.g. bad sid#) was confusing
|
|
if (!titleMsg.empty())
|
|
{
|
|
ILOG(titleMsg, LOG_NAME, m_sid);
|
|
}
|
|
if (!nextMsg.empty())
|
|
{
|
|
ILOG(nextMsg, LOG_NAME, m_sid);
|
|
}
|
|
if (!urlMsg.empty())
|
|
{
|
|
ILOG(urlMsg, LOG_NAME, m_sid);
|
|
}
|
|
if (!djMsg.empty())
|
|
{
|
|
ILOG(djMsg, LOG_NAME, m_sid);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WLOG("[ADMINCGI sid=" + tos(m_sid) + "] Metadata update rejected as the stream does not exist", LOG_NAME, m_sid);
|
|
}
|
|
|
|
m_updinfoSong.clear();
|
|
m_updinfoURL.clear();
|
|
m_updinfoDJ.clear();
|
|
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
|
|
/// update the metatdata in the shoutcast ring buffer
|
|
void protocol_admincgi::state_UpdateXMLMetadata() throw(exception)
|
|
{
|
|
DEBUG_LOG(utf8(LOGNAME) + __FUNCTION__);
|
|
|
|
if (!m_updinfoSong.empty())
|
|
{
|
|
ILOG("XML title update [" + m_updinfoSong + "]", LOG_NAME, m_sid);
|
|
streamData *sd = streamData::accessStream(m_sid);
|
|
if (sd)
|
|
{
|
|
vector<__uint8> assembledData;
|
|
assembledData.insert(assembledData.end(), m_updinfoSong.begin(), m_updinfoSong.end());
|
|
utf8 sourceIdent = (!m_userAgent.empty() ? m_userAgent : utf8("Legacy / Unknown"));
|
|
sd->updateSourceIdent(sourceIdent);
|
|
sd->addUvoxMetadataAtCurrentPosition(MSG_METADATA_XML_NEW, assembledData);
|
|
// this will call streamData::releaseStream(..)
|
|
streamData::streamClientLost(LOGNAME, sd, m_sid);
|
|
}
|
|
else
|
|
{
|
|
WLOG("XML title update rejected as stream #" + tos(m_sid) + " does not exist", LOG_NAME, m_sid);
|
|
}
|
|
m_updinfoSong.clear();
|
|
m_updinfoURL.clear();
|
|
}
|
|
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
|
|
void protocol_admincgi::state_ConfirmPassword() throw(std::exception)
|
|
{
|
|
DEBUG_LOG(utf8(LOGNAME) + __FUNCTION__);
|
|
|
|
// this will see if we've been refered from the base admin.cgi and will allow through
|
|
// if the password / auth matches with the key admin account so the page is useable.
|
|
utf8::size_type pos;
|
|
if ((pos = m_referer.rfind(utf8("admin.cgi"))) != utf8::npos)
|
|
{
|
|
m_referer = m_referer.substr(pos);
|
|
}
|
|
|
|
const utf8& mode = mapGet(m_httpRequestInfo.m_QueryParameters, "mode", (utf8)"");
|
|
const bool updinfo = (mode == "updinfo");
|
|
const bool viewxml = (mode == "viewxml");
|
|
const bool viewjson = (mode == "viewjson");
|
|
const bool _register = (mode == "register");
|
|
|
|
// on the root admin summary page we can only allow referer admin through if looking at the stats
|
|
const bool adminRefer = (m_referer.find(utf8("admin.cgi")) == 0 || m_referer.find(utf8("admin.cgi?sid=0")) == 0);
|
|
int okReferer = (adminRefer && (viewxml || viewjson || _register));
|
|
|
|
// on the root admin summary page we can check if it's an authhash action and allow
|
|
// through if things match up + also allow updinfo (not ideal but maintains legacy)
|
|
if ((!okReferer && ((mode == "manualauthhash") || _register)) || updinfo)
|
|
{
|
|
if (!m_referer.empty())
|
|
{
|
|
okReferer = (m_referer.find(utf8("admin.cgi?sid=" + tos(m_sid) + "&mode=")) != utf8::npos);
|
|
}
|
|
}
|
|
|
|
DEBUG_LOG(LOGNAME "Referrer status: " + tos(okReferer));
|
|
|
|
// fixed in Build 18 to revert to the master passwords if there's nothing for the stream
|
|
utf8 streamPassword = gOptions.stream_password(m_sid);
|
|
if (!gOptions.read_stream_password(m_sid) && streamPassword.empty())
|
|
{
|
|
streamPassword = gOptions.password();
|
|
}
|
|
|
|
utf8 streamAdminPassword = gOptions.stream_adminPassword(m_sid);
|
|
if (!gOptions.read_stream_adminPassword(m_sid) && streamAdminPassword.empty())
|
|
{
|
|
streamAdminPassword = gOptions.adminPassword();
|
|
}
|
|
|
|
// this is so we can allow source password connections access to the
|
|
// mode=viewxml&page=1 (/stats) and mode=viewxml&page=4 (/played) so
|
|
// we maintain compatibility with 1.x DNAS which only had these ones
|
|
int page = 0;
|
|
bool compat_mode = false;
|
|
if (viewxml || viewjson)
|
|
{
|
|
page = mapGet(m_httpRequestInfo.m_QueryParameters, "page", (int)page);
|
|
// this will act like /stats or / played (or both
|
|
// if page=0)so as to better emulate the 1.x DNAS
|
|
if ((page == 0) || (page == 1) || (page == 4))
|
|
{
|
|
compat_mode = true;
|
|
}
|
|
}
|
|
|
|
bool proceed = (((!streamPassword.empty() && (updinfo || compat_mode) && (m_password == streamPassword)) ||
|
|
(!streamAdminPassword.empty() && (m_password == streamAdminPassword)) ||
|
|
(!gOptions.adminPassword().empty() && (m_password == gOptions.adminPassword()))));
|
|
|
|
DEBUG_LOG(LOGNAME "Password status: " + tos(proceed));
|
|
|
|
if (proceed)
|
|
{
|
|
const streamData::streamID_t this_sid = (m_zeroSID || m_noSID ? 0 : m_sid);
|
|
const utf8& p1 = mapGet(m_httpRequestInfo.m_QueryParameters, mode, (utf8)"");
|
|
DEBUG_LOG(LOGNAME "Requested mode: " + (!mode.empty() ? mode : "Not Specified"));
|
|
if (updinfo)
|
|
{
|
|
m_updinfoSong = stripWhitespace(mapGet(m_httpRequestInfo.m_QueryParameters, "song", (utf8)""));
|
|
m_updinfoURL = stripWhitespace(mapGet(m_httpRequestInfo.m_QueryParameters, "url", (utf8)""));
|
|
m_updinfoDJ = stripWhitespace(mapGet(m_httpRequestInfo.m_QueryParameters, "dj", (utf8)""));
|
|
m_updinfoNext = stripWhitespace(mapGet(m_httpRequestInfo.m_QueryParameters, "next", (utf8)""));
|
|
m_state = ((m_updinfoSong.find(utf8("<?xml ")) != utf8::npos) ? &protocol_admincgi::state_UpdateXMLMetadata : &protocol_admincgi::state_UpdateMetadata);
|
|
m_result.run();
|
|
}
|
|
else if (viewxml)
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
const bool iponly = mapGet(m_httpRequestInfo.m_QueryParameters, "iponly", (bool)false);
|
|
const bool ipcount = mapGet(m_httpRequestInfo.m_QueryParameters, "ipcount", (bool)false);
|
|
mode_viewxml(m_sid, page, iponly, ipcount);
|
|
}
|
|
}
|
|
else if (viewjson)
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
const bool iponly = mapGet(m_httpRequestInfo.m_QueryParameters, "iponly", (bool)false);
|
|
const bool ipcount = mapGet(m_httpRequestInfo.m_QueryParameters, "ipcount", (bool)false);
|
|
const utf8 &callback = mapGet(m_httpRequestInfo.m_QueryParameters, "callback", (utf8)"");
|
|
mode_viewjson(m_sid, page, iponly, ipcount, callback);
|
|
}
|
|
}
|
|
else if (_register)
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
// do a sanity check so that we're only accessing this with the true adminpassword
|
|
if (m_zeroSID)
|
|
{
|
|
mode_summary(0);
|
|
}
|
|
// not matching the master password so show the simple summary
|
|
else
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// see if connected otherwise block any access to the pages
|
|
bool connected = false;
|
|
if (p1 == "clear")
|
|
{
|
|
bool handled = false, idHandled = false;
|
|
// if we get a clear then just remove from the config and reload the page
|
|
if (gOptions.editConfigFileEntry(m_sid, gOptions.confFile(), "", "",
|
|
true, handled, idHandled, true) == false)
|
|
{
|
|
handled = false;
|
|
}
|
|
// now attempt to update internal states as appropriate if all went ok
|
|
if (handled == true)
|
|
{
|
|
gOptions.setOption(utf8("streamauthhash_" + tos(m_sid)), (utf8)"");
|
|
streamData *sd = streamData::accessStream(m_sid);
|
|
if (sd)
|
|
{
|
|
sd->streamUpdate(m_sid, (utf8)"", sd->streamMaxUser(),
|
|
sd->streamMaxBitrate(), sd->streamMinBitrate());
|
|
sd->releaseStream();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
connected = true;
|
|
|
|
// sanity checks to ensure that we're not re-adding when there is one, etc
|
|
streamData::streamInfo info;
|
|
streamData::extraInfo extra;
|
|
if (!streamData::getStreamInfo(m_sid, info, extra))
|
|
{
|
|
info.m_authHash = gOptions.stream_authHash(m_sid);
|
|
}
|
|
|
|
if (connected == true || extra.isRelay)
|
|
{
|
|
mode_register(m_sid, info);
|
|
}
|
|
}
|
|
|
|
if (connected == false)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=" + tos(this_sid), SHRINK));
|
|
}
|
|
}
|
|
}
|
|
else if (mode == "listeners")
|
|
{
|
|
mode_listeners(this_sid);
|
|
}
|
|
else if (mode == "kicksrc")
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// kick source off system
|
|
streamData::killStreamSource(m_sid);
|
|
|
|
if (!m_referer.empty())
|
|
{
|
|
utf8 check = ("admin.cgi?sid=" + tos(m_sid));
|
|
// if the referer is the server summary page then we need to go back to it
|
|
sendMessageAndClose(redirect((m_referer == check ? check : "admin.cgi?sid=0"), SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
}
|
|
else if (mode == "kickdst" && (!p1.empty()))
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mode_kickdst(m_sid, p1);
|
|
}
|
|
}
|
|
else if (mode == "viewban")
|
|
{
|
|
mode_viewban(this_sid);
|
|
}
|
|
else if (mode == "bandst" && (!p1.empty()))
|
|
{
|
|
const int mask = mapGet(m_httpRequestInfo.m_QueryParameters, "banmsk", (int)255);
|
|
mode_ban(this_sid, p1, mask);
|
|
}
|
|
else if (mode == "banip")
|
|
{
|
|
const utf8 &ip1 = mapGet(m_httpRequestInfo.m_QueryParameters, "ip1", (utf8)""),
|
|
&ip2 = mapGet(m_httpRequestInfo.m_QueryParameters, "ip2", (utf8)""),
|
|
&ip3 = mapGet(m_httpRequestInfo.m_QueryParameters, "ip3", (utf8)""),
|
|
&ip4 = mapGet(m_httpRequestInfo.m_QueryParameters, "ip4", (utf8)""),
|
|
&mask = mapGet(m_httpRequestInfo.m_QueryParameters, "banmsk", (utf8)"");
|
|
if (ip1.empty() || ip2.empty() || ip3.empty() || ip4.empty() || mask.empty())
|
|
{
|
|
if ((m_referer != utf8("admin.cgi?sid=" + tos(this_sid) + "&mode=viewban")) &&
|
|
(m_referer != utf8("admin.cgi?mode=viewban")))
|
|
{
|
|
sendMessageAndClose(MSG_STD200 + utf8(!HEAD_REQUEST ? "<html><head>"
|
|
"<title>Shoutcast Server</title></head><body>"
|
|
"Invalid resource</body></html>" : ""));
|
|
}
|
|
else
|
|
{
|
|
if (!m_referer.empty())
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=" + tos(this_sid) + "&mode=viewban", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mode_ban(this_sid, (ip1 + "." + ip2 + "." + ip3 + "." + ip4), strtol((const char *)mask.c_str(),0,10));
|
|
}
|
|
}
|
|
else if (mode == "unbandst")
|
|
{
|
|
const utf8 &ip = mapGet(m_httpRequestInfo.m_QueryParameters, "bandst", (utf8)""),
|
|
&mask = mapGet(m_httpRequestInfo.m_QueryParameters, "banmsk", (utf8)"");
|
|
if (ip.empty() || mask.empty())
|
|
{
|
|
if ((m_referer != utf8("admin.cgi?sid=" + tos(this_sid) + "&mode=viewban")) &&
|
|
(m_referer != utf8("admin.cgi?mode=viewban")))
|
|
{
|
|
sendMessageAndClose(MSG_STD200 + utf8(!HEAD_REQUEST ? "<html><head>"
|
|
"<title>Shoutcast Server</title></head><body>"
|
|
"Invalid resource</body></html>" : ""));
|
|
}
|
|
else
|
|
{
|
|
if (!m_referer.empty())
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=" + tos(this_sid) + "&mode=viewban", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mode_unban(this_sid, ip, strtol((const char *)mask.c_str(), 0, 10));
|
|
}
|
|
}
|
|
else if (mode == "viewrip")
|
|
{
|
|
mode_viewrip(this_sid);
|
|
}
|
|
else if (mode == "ripip")
|
|
{
|
|
const utf8 &ip1 = mapGet(m_httpRequestInfo.m_QueryParameters, "ip1", (utf8)""),
|
|
&ip2 = mapGet(m_httpRequestInfo.m_QueryParameters, "ip2", (utf8)""),
|
|
&ip3 = mapGet(m_httpRequestInfo.m_QueryParameters, "ip3", (utf8)""),
|
|
&ip4 = mapGet(m_httpRequestInfo.m_QueryParameters, "ip4", (utf8)"");
|
|
if (ip1.empty() || ip2.empty() || ip3.empty() || ip4.empty())
|
|
{
|
|
// see if we've got a host add attempt and handle as appropriately
|
|
if (m_hostIP.empty())
|
|
{
|
|
if ((m_referer != utf8("admin.cgi?sid=" + tos(m_sid) + "&mode=viewrip")) &&
|
|
(m_referer != utf8("admin.cgi?mode=viewrip")))
|
|
{
|
|
sendMessageAndClose(MSG_STD200 + utf8(!HEAD_REQUEST ? "<html><head>"
|
|
"<title>Shoutcast Server</title></head><body>"
|
|
"Invalid resource</body></html>" : ""));
|
|
}
|
|
else
|
|
{
|
|
if (!m_referer.empty())
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=" + tos(m_sid) + "&mode=viewrip", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const utf8 &raw = mapGet(m_httpRequestInfo.m_QueryParameters, "ripdstraw", (utf8)"");
|
|
mode_rip(this_sid, m_hostIP, raw);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const utf8 &raw = mapGet(m_httpRequestInfo.m_QueryParameters, "ripdstraw", (utf8)"");
|
|
mode_rip(this_sid, (ip1 + "." + ip2 + "." + ip3 + "." + ip4), raw);
|
|
}
|
|
}
|
|
else if (mode == "unripdst")
|
|
{
|
|
const utf8 &ip = mapGet(m_httpRequestInfo.m_QueryParameters, "ripdst", (utf8)""),
|
|
&raw = mapGet(m_httpRequestInfo.m_QueryParameters, "ripdstraw", (utf8)"");
|
|
if (!isAddress(ip))
|
|
{
|
|
if ((m_referer != utf8("admin.cgi?sid=" + tos(m_sid) + "&mode=viewrip")) &&
|
|
(m_referer != utf8("admin.cgi?mode=viewrip")))
|
|
{
|
|
sendMessageAndClose(MSG_STD200 + utf8(!HEAD_REQUEST ? "<html><head>"
|
|
"<title>Shoutcast Server</title></head><body>"
|
|
"Invalid resource</body></html>" : ""));
|
|
}
|
|
else
|
|
{
|
|
if (!m_referer.empty())
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=" + tos(m_sid) + "&mode=viewrip", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mode_unrip(this_sid, ip, raw);
|
|
}
|
|
}
|
|
else if (mode == "ripdst" && (!p1.empty()))
|
|
{
|
|
const utf8 &raw = mapGet(m_httpRequestInfo.m_QueryParameters, "ripdstraw", (utf8)"");
|
|
mode_rip(this_sid, p1, raw);
|
|
}
|
|
else if (mode == "viewagent")
|
|
{
|
|
mode_viewagent(this_sid);
|
|
}
|
|
else if (mode == "unagent")
|
|
{
|
|
const utf8 &agent = mapGet(m_httpRequestInfo.m_QueryParameters, "agent", (utf8)"");
|
|
if (agent.empty())
|
|
{
|
|
if ((m_referer != utf8("admin.cgi?sid=" + tos(m_sid) + "&mode=viewagent")) &&
|
|
(m_referer != utf8("admin.cgi?mode=viewagent")))
|
|
{
|
|
sendMessageAndClose(MSG_STD200 + utf8(!HEAD_REQUEST ? "<html><head>"
|
|
"<title>Shoutcast Server</title></head><body>"
|
|
"Invalid resource</body></html>" : ""));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=" + tos(m_sid) + "&mode=viewagent", SHRINK));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mode_unagent(this_sid, agent);
|
|
}
|
|
}
|
|
else if (mode == "agent")
|
|
{
|
|
const utf8 &agent = mapGet(m_httpRequestInfo.m_QueryParameters, "agent", (utf8)"");
|
|
mode_agent(this_sid, agent);
|
|
}
|
|
else if (mode == "resetxml")
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
stats::resetStats(m_sid);
|
|
if (!m_referer.empty())
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=" + tos(m_sid), SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
}
|
|
else if (mode == "clearcache")
|
|
{
|
|
// TODO consider clearing out the intro / backup files as ell as part of this...
|
|
|
|
// clear out all cached resource copies
|
|
gOptions.m_crossdomainStr.clear();
|
|
gOptions.m_crossdomainStrGZ.clear();
|
|
gOptions.m_shoutcastSWFStr.clear();
|
|
gOptions.m_shoutcastSWFStrGZ.clear();
|
|
gOptions.m_robotsTxtBody.clear();
|
|
gOptions.m_robotsTxtBodyGZ.clear();
|
|
|
|
gOptions.m_faviconBody.clear();
|
|
gOptions.m_faviconBodyGZ.clear();
|
|
gOptions.m_favIconTime = 0;
|
|
|
|
gOptions.m_styleCustomStr.clear();
|
|
gOptions.m_styleCustomStrGZ.clear();
|
|
gOptions.m_styleCustomHeaderTime = 0;
|
|
|
|
DeleteAllCaches();
|
|
|
|
last_update_check = 0;
|
|
|
|
ILOG(LOGNAME "Cleared resource cache(s).");
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if (mode == "bannedlist")
|
|
{
|
|
reloadBanLists();
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if (mode == "reservelist")
|
|
{
|
|
reloadRipLists();
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if (mode == "adminlist")
|
|
{
|
|
reloadAdminAccessList();
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if (mode == "useragentlist")
|
|
{
|
|
reloadAgentLists();
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if (mode == "reload")
|
|
{
|
|
const int force = mapGet(m_httpRequestInfo.m_QueryParameters, "force", (int)0);
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect((reloadConfig(force) == true ? "admin.cgi?sid=0&refresh=3" : "admin.cgi?sid=0"), SHRINK));
|
|
}
|
|
else
|
|
{
|
|
reloadConfig(force);
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if (mode == "viewlog")
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
const utf8 &server = mapGet(m_httpRequestInfo.m_QueryParameters, "server", (utf8)"");
|
|
if (!server.empty() && ((server == logId) || (server == logTailId)))
|
|
{
|
|
mode_viewlog(m_sid, (p1 == "tail"), (p1 == "save"), true);
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mode_viewlog(m_sid, (p1 == "tail"), (p1 == "save"), false);
|
|
}
|
|
}
|
|
else if (mode == "history")
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const utf8 &type = mapGet(m_httpRequestInfo.m_QueryParameters, "type", (utf8)"");
|
|
const bool json = (type == "json"), xml = (type == "xml");
|
|
if (!json && !xml)
|
|
{
|
|
mode_history(m_sid);
|
|
}
|
|
else
|
|
{
|
|
utf8 header, body;
|
|
if (json)
|
|
{
|
|
const utf8& callback = mapGet(m_httpRequestInfo.m_QueryParameters, "callback", (utf8)"");
|
|
body = protocol_HTTPStyle::getPlayedJSON(m_sid, header, callback, true);
|
|
}
|
|
else
|
|
{
|
|
body = protocol_HTTPStyle::getPlayedXML(m_sid, header, true);
|
|
}
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
}
|
|
}
|
|
else if (mode == "art")
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mode_art(m_sid, (p1 == "playing"));
|
|
}
|
|
}
|
|
else if (mode == "manualauthhash")
|
|
{
|
|
bool handled = false;
|
|
const utf8 &authhash = mapGet(m_httpRequestInfo.m_QueryParameters, "authhash", (utf8)"");
|
|
// make sure that we're only allow valid values
|
|
if (authhash.empty() || yp2::isValidAuthhash(authhash))
|
|
{
|
|
bool idHandled = false;
|
|
if (gOptions.editConfigFileEntry(m_sid, gOptions.confFile(), authhash, "",
|
|
true, handled, idHandled, true) == false)
|
|
{
|
|
handled = false;
|
|
}
|
|
|
|
// changed in b69 to force the change through even if there is a config saving issue
|
|
// as there are cases where the authhash is gone but only updating on success would
|
|
// end up in the inability to start things over again e.g. with CentroCast v3 quirks
|
|
gOptions.setOption(utf8("streamauthhash_" + tos(m_sid)), authhash);
|
|
}
|
|
|
|
streamData *sd = streamData::accessStream(m_sid);
|
|
if (sd)
|
|
{
|
|
if (sd->isSourceConnected(m_sid))
|
|
{
|
|
utf8 oldAuthhash = sd->streamAuthhash();
|
|
sd->streamUpdate(m_sid, authhash, sd->streamMaxUser(),
|
|
sd->streamMaxBitrate(), sd->streamMinBitrate());
|
|
|
|
ILOG(gOptions.logSectionName() + "Changed authhash for stream #" +
|
|
tos(m_sid) + " to " + (authhash.empty() ? "empty" : authhash) +
|
|
" [was " + (oldAuthhash.empty() ? "empty" : oldAuthhash) + "]");
|
|
}
|
|
sd->releaseStream();
|
|
}
|
|
|
|
// now attempt to update internal states as appropriate if all went ok
|
|
if (handled == true)
|
|
{
|
|
sendMessageAndClose("HTTP/1.1 200 OK\r\n"
|
|
"Content-Type:text/plain\r\n"
|
|
"Content-Length:5\r\n"
|
|
"Cache-Control:no-cache\r\n"
|
|
"Access-Control-Allow-Origin:*\r\n"
|
|
"Connection:close\r\n\r\n200\r\n");
|
|
}
|
|
else
|
|
{
|
|
utf8 message = "Error saving changes to the configuration file.<br>"
|
|
"Check that you have write access and the<br>"
|
|
"specified configuration file still exists.<br><br>"
|
|
"The requested authhash change was applied.";
|
|
utf8 header = "HTTP/1.1 667\r\n"
|
|
"Content-Type:text/plain\r\n"
|
|
"Access-Control-Allow-Origin:*\r\n"
|
|
"Connection:close\r\n";
|
|
COMPRESS(header, message);
|
|
header += "Content-Length:" + tos(message.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? message : ""));
|
|
}
|
|
}
|
|
else if (mode == "rotate")
|
|
{
|
|
const utf8 &files = mapGet(m_httpRequestInfo.m_QueryParameters, "files", (utf8)""),
|
|
&rotateType = (!(files == "log" || files == "w3c") ? "all " : (files == "log" ? "": "W3C "));
|
|
ILOG(LOGNAME "Rotating " + rotateType + "log file(s)");
|
|
|
|
if ((files == "log") || (files == ""))
|
|
{
|
|
ROTATE;
|
|
printUpdateMessage();
|
|
if (m_logFile)
|
|
{
|
|
::fclose(m_logFile);
|
|
m_logFile = 0;
|
|
}
|
|
}
|
|
|
|
// and now rotate the w3c logs (going upto the configured number of old copies to be like the log rotate)
|
|
rotatew3cFiles(files);
|
|
|
|
ILOG(LOGNAME "Rotated " + rotateType + "log file(s) [PID: " + tos(getpid()) + "]");
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if (mode == "startrelay")
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// only attempt a source relay connection if it is not signaled
|
|
// as active or pending to prevent multiple attempts being run.
|
|
bool noEntry = false, active = (streamData::isRelayActive(m_sid, noEntry) == 1);
|
|
if (!active)
|
|
{
|
|
config::streamConfig stream;
|
|
if (gOptions.getStreamConfig(stream, m_sid))
|
|
{
|
|
restartRelay(stream);
|
|
}
|
|
}
|
|
|
|
if (!m_referer.empty())
|
|
{
|
|
utf8 check = ("admin.cgi?sid=" + tos(m_sid));
|
|
// if the referer is the server summary page then we need to go back to it
|
|
sendMessageAndClose(redirect((m_referer == check ? check : "admin.cgi?sid=0") +
|
|
(!active ? "&refresh=3" : ""), SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
}
|
|
else if (mode == "startrelays")
|
|
{
|
|
bool refresh = false;
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
if (!(*i).second.m_relayUrl.url().empty() && !streamData::isSourceConnected((*i).first))
|
|
{
|
|
// only attempt a source relay connection if it is not signaled
|
|
// as active or pending to prevent multiple attempts being run.
|
|
bool noEntry = false, active = (streamData::isRelayActive((*i).first, noEntry) == 1);
|
|
if (!active)
|
|
{
|
|
ILOG(gOptions.logSectionName() + "Starting source for stream #" + tos((*i).first) + ".");
|
|
restartRelay((*i).second);
|
|
refresh = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=0" + (refresh ? "&refresh=3" : (utf8)""), SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if (mode == "kicksources")
|
|
{
|
|
bool refresh = false;
|
|
streamData::streamIDs_t streamIds = streamData::getStreamIds(true);
|
|
if (!streamIds.empty())
|
|
{
|
|
for (streamData::streamIDs_t::const_iterator i = streamIds.begin(); i != streamIds.end(); ++i)
|
|
{
|
|
// kick source off system
|
|
streamData::killStreamSource((*i));
|
|
refresh = true;
|
|
}
|
|
}
|
|
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi?sid=0" + (refresh ? "&refresh=1" : (utf8)""), SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else if(mode == "bandwidth")
|
|
{
|
|
const utf8 &type = mapGet(m_httpRequestInfo.m_QueryParameters, "type", (utf8)"");
|
|
const bool json = (type == "json"), xml = (type == "xml");
|
|
|
|
if (!json && !xml)
|
|
{
|
|
const int refresh = mapGet(m_httpRequestInfo.m_QueryParameters, "refresh", 0);
|
|
mode_bandwidth_html(refresh);
|
|
}
|
|
else
|
|
{
|
|
if (json)
|
|
{
|
|
const utf8 &callback = mapGet(m_httpRequestInfo.m_QueryParameters, "callback", (utf8)"");
|
|
mode_bandwidth_json(callback);
|
|
}
|
|
else
|
|
{
|
|
mode_bandwidth_xml();
|
|
}
|
|
}
|
|
}
|
|
else if (mode == "ypstatus")
|
|
{
|
|
const utf8 &type = mapGet(m_httpRequestInfo.m_QueryParameters, "type", (utf8)"");
|
|
if ((type == "json"))
|
|
{
|
|
const utf8 &callback = mapGet(m_httpRequestInfo.m_QueryParameters, "callback", (utf8)"");
|
|
mode_ypstatus_json(callback);
|
|
}
|
|
else
|
|
{
|
|
mode_ypstatus_xml();
|
|
}
|
|
}
|
|
else if (mode == "sources")
|
|
{
|
|
mode_sources(m_hostIP);
|
|
}
|
|
else if (mode == "adgroups")
|
|
{
|
|
mode_adgroups();
|
|
}
|
|
else if (mode == "debug")
|
|
{
|
|
const utf8 &option = mapGet(m_httpRequestInfo.m_QueryParameters, "option", (utf8)"");
|
|
if (!option.empty() && !adminRefer)
|
|
{
|
|
// prevent direct access to being able to edit this
|
|
// and force a redirection to the non-param version
|
|
sendMessageAndClose(redirect("admin.cgi?mode=debug", SHRINK));
|
|
return;
|
|
}
|
|
|
|
const utf8 &on_off = mapGet(m_httpRequestInfo.m_QueryParameters, "on", (utf8)"");
|
|
mode_debug(option, (on_off == "true"), adminRefer);
|
|
}
|
|
else if (mode == "help")
|
|
{
|
|
mode_help();
|
|
}
|
|
else if (mode == "config")
|
|
{
|
|
mode_config();
|
|
}
|
|
#if 0
|
|
else if (mode == "logs")
|
|
{
|
|
mode_logs(result);
|
|
}
|
|
#endif
|
|
else if (mode == "version")
|
|
{
|
|
// only allow from the admin page to avoid possible spamming sttempts
|
|
if (adminRefer)
|
|
{
|
|
checkVersion(m_lastActivityTime);
|
|
sendMessageAndClose(redirect("admin.cgi?sid=0&refresh=-3", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (m_noSID)
|
|
{
|
|
// do a sanity check so that we're only accessing this with the true adminpassword
|
|
if (m_zeroSID)
|
|
{
|
|
mode_summary(mapGet(m_httpRequestInfo.m_QueryParameters, "refresh", 0));
|
|
}
|
|
// not matching the master password so show the simple summary
|
|
else
|
|
{
|
|
sendMessageAndClose(redirect("index.html", SHRINK));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const int refresh = mapGet(m_httpRequestInfo.m_QueryParameters, "refresh", 0);
|
|
mode_none(m_sid, refresh);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_AUTHFAILURE401 + utf8(!HEAD_REQUEST ?
|
|
"<html><head>Unauthorized<title>Shoutcast "
|
|
"Administrator</title></head></html>" : ""));
|
|
}
|
|
}
|
|
|
|
void protocol_admincgi::state_SendFileHeader() throw(std::exception)
|
|
{
|
|
if ((!m_saveLogFile && SHRINK) &&
|
|
compressDataStart(m_logFileBodyPrefix, &m_stream, (Bytef*)"sc_serv.log\0", false))
|
|
{
|
|
m_logFileHeader += "Content-Encoding:gzip\r\n";
|
|
}
|
|
m_logFileHeader += "\r\n";
|
|
|
|
m_outMsg = m_logFileHeader + (!m_saveLogFile ? tohex(m_logFileBodyPrefix.size()) + "\r\n" + m_logFileBodyPrefix + "\r\n" : "");
|
|
m_outBuffer = m_outMsg.c_str();
|
|
bandWidth::updateAmount(bandWidth::PRIVATE_WEB, (m_outBufferSize = (int)m_outMsg.size()));
|
|
m_state = &protocol_admincgi::state_Send;
|
|
m_nextState = &protocol_admincgi::state_SendFileContents;
|
|
m_result.write();
|
|
|
|
if (m_tailLogFile)
|
|
{
|
|
m_result.read(fileno(m_logFile));
|
|
}
|
|
}
|
|
|
|
void protocol_admincgi::state_SendFileFooter() throw(std::exception)
|
|
{
|
|
if (SHRINK)
|
|
{
|
|
compressDataCont(m_logFileBodyFooter, &m_stream);
|
|
}
|
|
m_logFileBodyFooter = tohex(m_logFileBodyFooter.size()) + "\r\n" + m_logFileBodyFooter + "\r\n";
|
|
m_outBuffer = m_logFileBodyFooter.c_str();
|
|
bandWidth::updateAmount(bandWidth::PRIVATE_WEB, (m_outBufferSize = (int)m_logFileBodyFooter.size()));
|
|
m_state = &protocol_admincgi::state_Send;
|
|
m_nextState = &protocol_admincgi::state_SendFileEnd;
|
|
m_result.write();
|
|
}
|
|
|
|
void protocol_admincgi::state_SendFileEnd() throw(std::exception)
|
|
{
|
|
strncpy((char*)&(m_logFileBuffer[0]), "0\r\n\r\n", 1024);
|
|
m_outBuffer = &(m_logFileBuffer[0]);
|
|
m_outBufferSize = 5;
|
|
m_nextState = &protocol_admincgi::state_Close;
|
|
bandWidth::updateAmount(bandWidth::PRIVATE_WEB, m_outBufferSize);
|
|
compressDataEnd(&m_stream);
|
|
|
|
m_state = &protocol_admincgi::state_Send;
|
|
m_nextState = &protocol_admincgi::state_Close;
|
|
m_result.write();
|
|
}
|
|
|
|
// this will only be receiving an already converted
|
|
// string so no need to do the commented part again
|
|
utf8 protocol_admincgi::escapeText(const std::vector<uniString::utf8::value_type> &s) throw()
|
|
{
|
|
utf8 result;
|
|
result.resize(0);
|
|
bool inHead = false;
|
|
int headCount = 0;
|
|
const size_t amt = s.size();
|
|
|
|
for (size_t x = 0; x < amt; ++x)
|
|
{
|
|
if ((s[x] == '*') && (((x + 1) < amt) && (s[x + 1] == '*')))
|
|
{
|
|
inHead = true;
|
|
}
|
|
else if (s[x] == '\n')
|
|
{
|
|
if (!inHead)
|
|
{
|
|
if ((((x + 1) < amt) && isdigit(s[x + 1])))
|
|
{
|
|
if (inMsg)
|
|
{
|
|
result += "</b>";
|
|
}
|
|
result += "\n";
|
|
inMsg = false;
|
|
}
|
|
else
|
|
{
|
|
if (((x + 1) == amt))
|
|
{
|
|
if (inMsg)
|
|
{
|
|
result += "</b>";
|
|
}
|
|
result += "\n";
|
|
inMsg = false;
|
|
}
|
|
else
|
|
{
|
|
result += "\n\t\t\t";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
++headCount;
|
|
}
|
|
}
|
|
else if ((s[x] == 'D') &&
|
|
(((x > 0) && (s[x - 1] == '\t')) ||
|
|
(!x && (lastChar == '\t'))))
|
|
{
|
|
result += "<b class=\"d\">D";
|
|
inMsg = true;
|
|
}
|
|
else if ((s[x] == 'E') &&
|
|
(((x > 0) && (s[x - 1] == '\t')) ||
|
|
(!x && (lastChar == '\t'))))
|
|
{
|
|
result += "<b class=\"e\">E";
|
|
inMsg = true;
|
|
}
|
|
else if ((s[x] == 'W') &&
|
|
(((x > 0) && (s[x - 1] == '\t')) ||
|
|
(!x && (lastChar == '\t'))))
|
|
{
|
|
result += "<b class=\"w\">W";
|
|
inMsg = true;
|
|
}
|
|
else if ((s[x] == 'U') &&
|
|
(((x > 0) && (s[x-1] == '\t')) ||
|
|
(!x && (lastChar == '\t'))))
|
|
{
|
|
result += "<b class=\"u\">U";
|
|
inMsg = true;
|
|
}
|
|
else if ((s[x] == 'I') &&
|
|
(((x > 0) && (s[x-1] == '\t')) ||
|
|
(!x && (lastChar == '\t'))))
|
|
{
|
|
if (!inHead)
|
|
{
|
|
result += "<b class=\"i\">I";
|
|
inMsg = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!inHead)
|
|
{
|
|
if (s[x] == '<')
|
|
{
|
|
result += "<";
|
|
}
|
|
else if (s[x] == '>')
|
|
{
|
|
result += ">";
|
|
}
|
|
else if (s[x] == '&')
|
|
{
|
|
result += "&";
|
|
}
|
|
else if (s[x] == '\'')
|
|
{
|
|
result += "'";
|
|
}
|
|
else if (s[x] == '"')
|
|
{
|
|
result += """;
|
|
}
|
|
else
|
|
{
|
|
result += s[x];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (inHead && (headCount > 3) && (s[x] == '\t'))
|
|
{
|
|
inHead = false;
|
|
headCount = 0;
|
|
x += 5;
|
|
}
|
|
}
|
|
}
|
|
if (!s[x])
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
lastChar = s[amt - 1];
|
|
return result;
|
|
}
|
|
|
|
void protocol_admincgi::state_SendFileContents() throw(std::exception)
|
|
{
|
|
m_logFileBuffer.clear();
|
|
m_logFileBuffer.resize(SEND_SIZE);
|
|
const size_t amt = fread(&(m_logFileBuffer[0]), 1, (SEND_SIZE - 1), m_logFile);
|
|
if (amt > 0)
|
|
{
|
|
static utf8 out;
|
|
out.clear();
|
|
|
|
if (!m_saveLogFile)
|
|
{
|
|
m_logFileBuffer.resize(amt);
|
|
out = escapeText(m_logFileBuffer);
|
|
}
|
|
else
|
|
{
|
|
out = utf8(&(m_logFileBuffer[0]), amt);
|
|
}
|
|
|
|
if (!out.empty())
|
|
{
|
|
if (m_saveLogFile || (SHRINK))
|
|
{
|
|
if (m_saveLogFile == 1)
|
|
{
|
|
if (compressDataStart(out, &m_stream, (Bytef*)m_logFileName.c_str()))
|
|
{
|
|
m_saveLogFile = 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
compressDataCont(out, &m_stream);
|
|
}
|
|
}
|
|
out = tohex(out.size()) + "\r\n" + out + "\r\n";
|
|
m_outBuffer = out.c_str();
|
|
bandWidth::updateAmount(bandWidth::PRIVATE_WEB, (m_outBufferSize = (int)out.size()));
|
|
m_nextState = &protocol_admincgi::state_SendFileContents;
|
|
}
|
|
else
|
|
{
|
|
m_nextState = (m_saveLogFile ? &protocol_admincgi::state_SendFileEnd : &protocol_admincgi::state_SendFileFooter);
|
|
}
|
|
|
|
m_state = &protocol_admincgi::state_Send;
|
|
m_result.write();
|
|
|
|
if (m_tailLogFile)
|
|
{
|
|
m_result.timeout(1);
|
|
m_result.read(fileno(m_logFile));
|
|
}
|
|
}
|
|
else if (ferror(m_logFile) || !m_logFile)
|
|
{
|
|
m_state = &protocol_admincgi::state_Close;
|
|
m_result.run();
|
|
}
|
|
else if (feof(m_logFile) && (!m_tailLogFile))
|
|
{
|
|
if (m_saveLogFile)
|
|
{
|
|
static utf8 out;
|
|
out.clear();
|
|
compressDataFinish(out, &m_stream);
|
|
out = tohex(out.size()) + "\r\n" + out + "\r\n";
|
|
m_outBuffer = out.c_str();
|
|
bandWidth::updateAmount(bandWidth::PRIVATE_WEB, (m_outBufferSize = (int)out.size()));
|
|
|
|
m_state = &protocol_admincgi::state_Send;
|
|
m_nextState = &protocol_admincgi::state_SendFileEnd;
|
|
}
|
|
else
|
|
{
|
|
m_state = &protocol_admincgi::state_SendFileFooter;
|
|
}
|
|
|
|
m_result.write();
|
|
m_result.run();
|
|
}
|
|
else
|
|
{
|
|
m_result.timeout(10);
|
|
}
|
|
}
|
|
|
|
// display log
|
|
void protocol_admincgi::mode_viewlog(const streamData::streamID_t sid, const bool tail, const bool save, const bool server) throw()
|
|
{
|
|
if (!save)
|
|
{
|
|
utf8 headerTitle = utf8(utf8("Server Log") + (tail ? " (Tailing)":""));
|
|
m_logFileHeader = MSG_NO_CLOSE_200;
|
|
|
|
m_logFileBodyPrefix += (server ? getServerAdminHeader(headerTitle) :
|
|
getStreamAdminHeader(sid, headerTitle)) + getUptimeScript() +
|
|
(tail ? "<div style=\"padding:1em;\"><b>Showing log output from the current log "
|
|
"position and onwards. Click <a href=\"admin.cgi?mode=viewlog&server=" +
|
|
randomId(logTailId) + "\">here</a> to view the complete log output or <a "
|
|
"href=\"admin.cgi?mode=viewlog&server=" + logId + "&viewlog=save\">here</a> "
|
|
"to save the complete log output. When tailing the log output, there may be "
|
|
"a delay before any new log output will appear and it may also stop updating if "
|
|
"there is no log activity depending on configured timeouts. Reload the page if this "
|
|
"should happens.</b></div>" : "") + getIEFlexFix() +
|
|
"<pre id=\"log\" style=\"tab-size:16;-moz-tab-size:16;-o-tab-size:16;\"><font class=\"t\">";
|
|
|
|
// doing to just make sure that the end of the output should be correct
|
|
m_logFileBodyFooter = "</font>\n</pre>" + getfooterStr();
|
|
m_tailLogFile = tail;
|
|
|
|
if (server || !(gOptions.read_stream_adminPassword(sid) && !gOptions.stream_adminPassword(sid).empty()))
|
|
{
|
|
const utf8 &file = mapGet(m_httpRequestInfo.m_QueryParameters, "file", gOptions.realLogFile());
|
|
|
|
m_logFile = uniFile::fopen(file, "r");
|
|
if (m_logFile)
|
|
{
|
|
m_logFileName = fileUtil::stripPath(file);
|
|
m_logFileName.push_back('\0');
|
|
|
|
if (m_tailLogFile)
|
|
{
|
|
::fseek(m_logFile, 0, SEEK_END);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
utf8 body = m_logFileBodyPrefix + "Viewing Not Allowed With Current Permissions</pre>" + m_logFileBodyFooter;
|
|
COMPRESS(m_logFileHeader, body);
|
|
m_logFileHeader += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(m_logFileHeader + (!HEAD_REQUEST ? body : ""));
|
|
return;
|
|
}
|
|
|
|
if (!m_logFile)
|
|
{
|
|
utf8 body = m_logFileBodyPrefix + "Log File Not Found (" +
|
|
stripWhitespace(errMessage()) + ")</pre>" +
|
|
m_logFileBodyFooter;
|
|
COMPRESS(m_logFileHeader, body);
|
|
m_logFileHeader += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(m_logFileHeader + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
else
|
|
{
|
|
m_logFileHeader += "Transfer-Encoding:chunked\r\n";
|
|
m_state = &protocol_admincgi::state_SendFileHeader;
|
|
m_result.run();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (server || !(gOptions.read_stream_adminPassword(sid) && !gOptions.stream_adminPassword(sid).empty()))
|
|
{
|
|
const utf8 &file = mapGet(m_httpRequestInfo.m_QueryParameters, "file", gOptions.realLogFile());
|
|
|
|
m_logFile = uniFile::fopen(file, "r");
|
|
if (m_logFile)
|
|
{
|
|
m_logFileName = fileUtil::stripPath(file);
|
|
m_logFileHeader = "HTTP/1.1 200 OK\r\n"
|
|
"Content-Type:application/x-gzip-compressed\r\n"
|
|
"Content-Disposition:attachment;filename=\"" +
|
|
m_logFileName + ".gz\"\r\n";
|
|
m_saveLogFile = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_HTTP403);
|
|
return;
|
|
}
|
|
|
|
if (!m_logFile)
|
|
{
|
|
sendMessageAndClose(MSG_HTTP404);
|
|
}
|
|
else
|
|
{
|
|
m_logFileHeader += "Transfer-Encoding:chunked\r\n";
|
|
m_state = &protocol_admincgi::state_SendFileHeader;
|
|
m_result.run();
|
|
}
|
|
}
|
|
}
|
|
|
|
// shown played history (as is also shown on the public pages if enabled)
|
|
void protocol_admincgi::mode_history(const streamData::streamID_t sid) throw()
|
|
{
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
body = getStreamAdminHeader(sid, "Stream History") +
|
|
"<table width=\"100%\" border=\"0\" cellpadding=\"0\" "
|
|
"cellspacing=\"0\"><tr valign=\"top\"><td>" +
|
|
protocol_HTTPStyle::getPlayedBody(sid) + "</table>" +
|
|
getUptimeScript() + getIEFlexFix() + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
// remove an IP from the rip list
|
|
void protocol_admincgi::mode_unrip(const streamData::streamID_t sid, const utf8 &ripAddr, const utf8 &rawIpAddr) throw()
|
|
{
|
|
utf8 msg;
|
|
try
|
|
{
|
|
size_t stream_ID = ((gOptions.read_stream_ripFile(sid) && !gOptions.stream_ripFile(sid).empty()) ? sid : 0);
|
|
if (isAddress(ripAddr))
|
|
{
|
|
bool ret = g_ripList.remove(ripAddr,stream_ID,false), usingRaw = false;
|
|
if (!ret && !rawIpAddr.empty())
|
|
{
|
|
ret = g_ripList.remove(rawIpAddr,stream_ID,false);
|
|
if (ret) usingRaw = true;
|
|
}
|
|
if (ret)
|
|
{
|
|
ILOG("[RIP] Removed `" + (!usingRaw ? ripAddr : rawIpAddr) + "' from " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " rip list");
|
|
|
|
if (gOptions.saveRipListOnExit())
|
|
{
|
|
if (stream_ID && gOptions.read_stream_ripFile(stream_ID) && !gOptions.stream_ripFile(stream_ID).empty())
|
|
{
|
|
g_ripList.save(gOptions.stream_ripFile(stream_ID),stream_ID);
|
|
}
|
|
else
|
|
{
|
|
g_ripList.save(gOptions.ripFile(),0);
|
|
}
|
|
}
|
|
|
|
stats::updateRipClients(stream_ID, (!usingRaw ? ripAddr : rawIpAddr), false);
|
|
}
|
|
else
|
|
{
|
|
ILOG("[RIP] Unable to remove `" + ripAddr + "' from " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " rip list");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ILOG("[RIP] `" + ripAddr + "' is not a valid value. Skipping removing from the " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " rip list");
|
|
}
|
|
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer == "admin.cgi?sid=" + tos(sid) ? "" : "&mode=viewrip"), SHRINK) : MSG_200);
|
|
}
|
|
catch(const exception &ex)
|
|
{
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer != utf8("admin.cgi?sid=" + tos(sid) + "&mode=viewrip") ? "" : "&mode=viewrip"), SHRINK) : MSG_200);
|
|
ELOG(ex.what());
|
|
}
|
|
|
|
sendMessageAndClose(msg);
|
|
}
|
|
|
|
// add an IP / hostname to the rip list
|
|
void protocol_admincgi::mode_rip(const streamData::streamID_t sid, const utf8 &ripAddr, const utf8 &rawIpAddr) throw()
|
|
{
|
|
utf8 msg;
|
|
try
|
|
{
|
|
const size_t stream_ID = ((gOptions.read_stream_ripFile(sid) && !gOptions.stream_ripFile(sid).empty()) ? sid : 0);
|
|
if (isAddress(ripAddr))
|
|
{
|
|
if (!g_ripList.find(ripAddr,stream_ID))
|
|
{
|
|
bool added = g_ripList.add(ripAddr,stream_ID, true), usingRaw = false;
|
|
if (!added && !rawIpAddr.empty())
|
|
{
|
|
if (g_ripList.add(rawIpAddr,stream_ID, false))
|
|
{
|
|
usingRaw = true;
|
|
}
|
|
}
|
|
|
|
ILOG("[RIP] Added `" + (!usingRaw ? ripAddr : rawIpAddr) + "' to " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " rip list");
|
|
|
|
if (gOptions.saveRipListOnExit())
|
|
{
|
|
if (stream_ID && gOptions.read_stream_ripFile(stream_ID) && !gOptions.stream_ripFile(stream_ID).empty())
|
|
{
|
|
g_ripList.save(gOptions.stream_ripFile(stream_ID),stream_ID);
|
|
}
|
|
else
|
|
{
|
|
g_ripList.save(gOptions.ripFile(),0);
|
|
}
|
|
}
|
|
stats::updateRipClients(stream_ID, (!usingRaw ? ripAddr : rawIpAddr), true);
|
|
}
|
|
else
|
|
{
|
|
ILOG("[RIP] `" + ripAddr + "' already in the " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " rip list");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ILOG("[RIP] `" + ripAddr + "' is not a valid value. Skipping adding to the " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " rip list");
|
|
}
|
|
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer == "admin.cgi?sid=" + tos(sid) ? "" : "&mode=viewrip"), SHRINK) : MSG_200);
|
|
}
|
|
catch(const exception &ex)
|
|
{
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer != utf8("admin.cgi?sid=" + tos(sid) + "&mode=viewrip") ? "" : "&mode=viewrip"), SHRINK) : MSG_200);
|
|
ELOG(ex.what());
|
|
}
|
|
|
|
sendMessageAndClose(msg);
|
|
}
|
|
|
|
// show rip list
|
|
void protocol_admincgi::mode_viewrip(const streamData::streamID_t sid) throw()
|
|
{
|
|
vector<ripList::rip_t> rip_list;
|
|
g_ripList.get(rip_list,((gOptions.read_stream_ripFile(sid) && !gOptions.stream_ripFile(sid).empty()) ? sid : 0));
|
|
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
headerTitle = (!sid ? "Server Reserved List" : "Stream Reserved List"),
|
|
body = (!sid ? getServerAdminHeader(headerTitle) : getStreamAdminHeader(sid, headerTitle)) +
|
|
"<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\"><td>";
|
|
|
|
if (rip_list.empty())
|
|
{
|
|
body += "<b> No Reserved Entries</b><br>";
|
|
}
|
|
else
|
|
{
|
|
body += "<b> Reserved Entry List:</b><ol>";
|
|
for (vector<ripList::rip_t>::const_iterator i = rip_list.begin(); i != rip_list.end(); ++i)
|
|
{
|
|
body += "<li><b>" + aolxml::escapeXML((*i).m_numericIP) + "</b>" +
|
|
(!(*i).m_hostIP.empty() ? " (" + aolxml::escapeXML((*i).m_hostIP) + ")" : "") +
|
|
" - <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=unripdst&ripdst=" +
|
|
urlUtils::escapeURI_RFC3986((*i).m_numericIP) + "\">remove</a>";
|
|
}
|
|
body += "</ol>";
|
|
}
|
|
|
|
body +=
|
|
"</td><td style=\"padding:0 1em 0 1em;\"><br>"
|
|
"<table class=\"ent\" border=\"0\" cellspacing=\"0\" cellpadding=\"3\" align=\"right\">"
|
|
"<tr class=\"ent\">"
|
|
"<td class=\"inp\" align=\"center\">Reserve Connection Slot by IP</td>"
|
|
"</tr>"
|
|
"<form method=\"url\" action=\"admin.cgi\">"
|
|
"<tr>"
|
|
"<td><table cellspacing=\"0\" cellpadding=\"3\" border=\"0\">"
|
|
"<tr>"
|
|
"<td align=\"center\">Enter the IP address:<br><i>(example: 127.0.0.1)</i></td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td align=\"center\">"
|
|
"<input name=\"mode\" value=\"ripip\" type=\"hidden\">"
|
|
"<input name=\"sid\" value=\"" + tos(sid) + "\" type=\"hidden\">"
|
|
"<input name=\"ip1\" size=\"3\" maxlength=\"3\">."
|
|
"<input name=\"ip2\" size=\"3\" maxlength=\"3\">."
|
|
"<input name=\"ip3\" size=\"3\" maxlength=\"3\">."
|
|
"<input name=\"ip4\" size=\"3\" maxlength=\"3\">"
|
|
"</td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td colspan=\"2\" align=\"center\"><input class=\"submit\" type=\"submit\" value=\"Reserve IP\">"
|
|
" <input class=\"submit\" value=\"Clear\" type=\"Reset\"></td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"<td style=\"padding: 0 1em 0 0;\"><br>"
|
|
"<table class=\"ent\" border=\"0\" cellspacing=\"0\" cellpadding=\"3\" align=\"left\">"
|
|
"<tr class=\"ent\">"
|
|
"<td class=\"inp\" align=\"center\">Reserve Connection Slot by Host</td>"
|
|
"</tr>"
|
|
"<form method=\"url\" action=\"admin.cgi\">"
|
|
"<tr>"
|
|
"<td><table cellspacing=\"0\" cellpadding=\"3\" border=\"0\">"
|
|
"<tr>"
|
|
"<td align=\"center\">Enter the hostname:<br><i>(example: my.example.com)</i></td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td align=\"center\">"
|
|
"<input name=\"mode\" value=\"ripip\" type=\"hidden\">"
|
|
"<input name=\"sid\" value=\"" + tos(sid) + "\" type=\"hidden\">"
|
|
"<input name=\"ripdstraw\" size=\"30\" maxlength=\"256\">"
|
|
"</td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td colspan=\"2\" align=\"center\"><input class=\"submit\" type=\"submit\" value=\"Reserve Host\">"
|
|
" <input class=\"submit\" value=\"Clear\" type=\"Reset\"></td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"</tr>"
|
|
"</form>"
|
|
"</table>" +
|
|
getUptimeScript() +
|
|
getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
// show ban lists
|
|
void protocol_admincgi::mode_viewban(const streamData::streamID_t sid) throw()
|
|
{
|
|
vector<banList::ban_t> ban_list;
|
|
g_banList.get(ban_list,((gOptions.read_stream_banFile(sid) && !gOptions.stream_banFile(sid).empty()) ? sid : 0));
|
|
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
headerTitle = (!sid ? "Server Ban List" : "Stream Ban List"),
|
|
body = (!sid ? getServerAdminHeader(headerTitle) : getStreamAdminHeader(sid, headerTitle)) +
|
|
"<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\"><td>";
|
|
|
|
if (ban_list.empty())
|
|
{
|
|
body += "<b> No Banned Entries</b><br>";
|
|
}
|
|
else
|
|
{
|
|
body += "<b> Ban Entry List:</b><ol>";
|
|
for (vector<banList::ban_t>::const_iterator i = ban_list.begin(); i != ban_list.end(); ++i)
|
|
{
|
|
body += "<li><b>" + aolxml::escapeXML((*i).m_numericIP) + "</b>" +
|
|
(!(*i).m_comment.empty() ? " : <b>" + aolxml::escapeXML((*i).m_comment) + "</b>" : "") +
|
|
" - " + ((*i).m_mask == 255 ? "Single IP" : "Subnet") + " ban - "
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=unbandst&bandst=" +
|
|
urlUtils::escapeURI_RFC3986((*i).m_numericIP) + "&banmsk=" + tos((*i).m_mask) + "\">remove</a>";
|
|
}
|
|
body += "</ol>";
|
|
}
|
|
|
|
body +=
|
|
"</td><td style=\"padding:0 1em 0 1em;\"><br>"
|
|
"<table class=\"ent\" border=\"0\" cellspacing=\"0\" cellpadding=\"3\" align=\"right\">"
|
|
"<tr class=\"ent\">"
|
|
"<td class=\"inp\" align=\"center\">Ban a Single IP</td>"
|
|
"</tr>"
|
|
"<form method=\"url\" action=\"admin.cgi\">"
|
|
"<tr>"
|
|
"<td><table cellspacing=\"0\" cellpadding=\"3\" border=\"0\">"
|
|
"<tr>"
|
|
"<td align=\"center\">Enter the IP address:<br><i>(example: 127.0.0.1)</i></td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td align=\"center\">"
|
|
"<input name=\"mode\" value=\"banip\" type=\"hidden\">"
|
|
"<input name=\"sid\" value=\"" + tos(sid) + "\" type=\"hidden\">"
|
|
"<input name=\"ip1\" size=\"3\" maxlength=\"3\">."
|
|
"<input name=\"ip2\" size=\"3\" maxlength=\"3\">."
|
|
"<input name=\"ip3\" size=\"3\" maxlength=\"3\">."
|
|
"<input name=\"ip4\" size=\"3\" maxlength=\"3\">"
|
|
"<input type=\"hidden\" name=\"banmsk\" value=\"255\"></td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td colspan=\"2\" align=\"center\"><input class=\"submit\" type=\"submit\" value=\"Ban Single IP\">"
|
|
" <input class=\"submit\" value=\"Clear\" type=\"Reset\"></td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"</tr>"
|
|
"</form>"
|
|
"</table>"
|
|
"</td>"
|
|
"<td style=\"padding: 0 1em 0 0;\"><br>"
|
|
"<table class=\"ent\" border=\"0\" cellspacing=\"0\" cellpadding=\"3\" align=\"left\">"
|
|
"<tr class=\"ent\">"
|
|
"<td class=\"inp\" align=\"center\">Ban an Entire Subnet</td>"
|
|
"</tr>"
|
|
"<form method=\"url\" action=\"admin.cgi\">"
|
|
"<tr>"
|
|
"<td><table cellspacing=\"0\" cellpadding=\"3\" border=\"0\">"
|
|
"<tr>"
|
|
"<td align=\"center\">Enter the Subnet address:<br><i>(example: 255.255.255)</i></td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td align=\"center\">"
|
|
"<input name=\"mode\" value=\"banip\" type=\"hidden\">"
|
|
"<input name=\"sid\" value=\"" + tos(sid) + "\" type=\"hidden\">"
|
|
"<input name=\"ip1\" size=\"1\" maxlength=\"3\">."
|
|
"<input name=\"ip2\" size=\"1\" maxlength=\"3\">."
|
|
"<input name=\"ip3\" size=\"1\" maxlength=\"3\">.0-255"
|
|
"<input name=\"ip4\" value=\"0\" type=\"hidden\">"
|
|
"<input type=\"hidden\" name=\"banmsk\" value=\"0\"></td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td colspan=\"2\" align=\"center\"><input class=\"submit\" type=\"submit\" value=\"Ban Whole Subnet\">"
|
|
" <input class=\"submit\" value=\"Clear\" type=\"Reset\"></td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"</tr>"
|
|
"</form>"
|
|
"</table>" +
|
|
getUptimeScript() +
|
|
getIEFlexFix() +
|
|
getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
// remove an IP and mask from the ban list
|
|
void protocol_admincgi::mode_unban(const streamData::streamID_t sid, const utf8 &banAddr, const int banMask) throw()
|
|
{
|
|
utf8 msg;
|
|
try
|
|
{
|
|
const size_t stream_ID = ((gOptions.read_stream_banFile(sid) && !gOptions.stream_banFile(sid).empty()) ? sid : 0);
|
|
if (isAddress(banAddr))
|
|
{
|
|
if (g_banList.remove(banAddr,banMask,stream_ID,false))
|
|
{
|
|
ILOG("[BAN] Removed `" + banAddr + "/" + tos(banMask) + "' from " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " ban list");
|
|
|
|
if (gOptions.saveBanListOnExit())
|
|
{
|
|
if (stream_ID && gOptions.read_stream_banFile(stream_ID) && !gOptions.stream_banFile(stream_ID).empty())
|
|
{
|
|
g_banList.save(gOptions.stream_banFile(stream_ID),stream_ID);
|
|
}
|
|
else
|
|
{
|
|
g_banList.save(gOptions.banFile(),0);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ILOG("[BAN] Unable to remove `" + banAddr + "' from " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " ban list");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ILOG("[BAN] `" + banAddr + "' is not a valid value. Skipping removing from the " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " ban list");
|
|
}
|
|
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer == "admin.cgi?sid=" + tos(sid) ? "" : "&mode=viewban"), SHRINK) : MSG_200);
|
|
}
|
|
catch(const exception &ex)
|
|
{
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer != utf8("admin.cgi?sid=" + tos(sid) + "&mode=viewban") ? "" : "&mode=viewban"), SHRINK) : MSG_200);
|
|
ELOG(ex.what());
|
|
}
|
|
|
|
sendMessageAndClose(msg);
|
|
}
|
|
|
|
// add an IP and mask to the ban list
|
|
void protocol_admincgi::mode_ban(const streamData::streamID_t sid, const utf8 &banAddrs, const int banMask) throw()
|
|
{
|
|
utf8 msg;
|
|
try
|
|
{
|
|
const size_t stream_ID = ((gOptions.read_stream_banFile(sid) && !gOptions.stream_banFile(sid).empty()) ? sid : 0);
|
|
if (!g_banList.find(banAddrs,banMask,stream_ID))
|
|
{
|
|
g_banList.add(banAddrs,banMask,"",stream_ID);
|
|
ILOG("[BAN] Added `" + banAddrs + "/" + tos(banMask) + "' to " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " ban list");
|
|
|
|
if (gOptions.saveBanListOnExit())
|
|
{
|
|
if (stream_ID && gOptions.read_stream_banFile(stream_ID) && !gOptions.stream_banFile(stream_ID).empty())
|
|
{
|
|
g_banList.save(gOptions.stream_banFile(stream_ID),stream_ID);
|
|
}
|
|
else
|
|
{
|
|
g_banList.save(gOptions.banFile(),0);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ILOG("[BAN] `" + banAddrs + "' already in the " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " ban list");
|
|
}
|
|
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer == "admin.cgi?sid=" + tos(sid) ? "" : "&mode=viewban"), SHRINK) : MSG_200);
|
|
}
|
|
catch(const exception &ex)
|
|
{
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer != utf8("admin.cgi?sid=" + tos(sid) + "&mode=viewban") ? "" : "&mode=viewban"), SHRINK) : MSG_200);
|
|
ELOG(ex.what());
|
|
}
|
|
|
|
// additionally when a ban happens then we attempt to kick the client(s) at the same time
|
|
const utf8 &p1 = mapGet(m_httpRequestInfo.m_QueryParameters, "kickdst", (utf8)"");
|
|
if (!p1.empty())
|
|
{
|
|
// split out multiple clients to kick (split by a ,)
|
|
std::vector<uniString::utf8> addrs = tokenizer(p1, ',');
|
|
for (vector<uniString::utf8>::const_iterator i = addrs.begin(); i != addrs.end(); ++i)
|
|
{
|
|
if ((*i).find(utf8(".")) == utf8::npos)
|
|
{
|
|
stats::kickClient(sid, atoi((*i).hideAsString().c_str()));
|
|
}
|
|
else
|
|
{
|
|
stats::kickClient(sid, (*i));
|
|
}
|
|
}
|
|
}
|
|
|
|
sendMessageAndClose(msg);
|
|
}
|
|
|
|
// remove an agent from the agent list
|
|
void protocol_admincgi::mode_unagent(const streamData::streamID_t sid, const utf8 &agent) throw()
|
|
{
|
|
utf8 msg;
|
|
try
|
|
{
|
|
const size_t stream_ID = ((gOptions.read_stream_agentFile(sid) && !gOptions.stream_agentFile(sid).empty()) ? sid : 0);
|
|
bool ret = g_agentList.remove(agent,stream_ID,false);
|
|
if (!ret && !agent.empty())
|
|
{
|
|
ret = g_agentList.remove(agent,stream_ID,false);
|
|
}
|
|
if (ret)
|
|
{
|
|
ILOG("[AGENT] Removed `" + agent + "' from " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " user agent list");
|
|
|
|
if (gOptions.saveAgentListOnExit())
|
|
{
|
|
if (stream_ID && gOptions.read_stream_agentFile(stream_ID) && !gOptions.stream_agentFile(stream_ID).empty())
|
|
{
|
|
g_agentList.save(gOptions.stream_agentFile(stream_ID),stream_ID);
|
|
}
|
|
else
|
|
{
|
|
g_agentList.save(gOptions.agentFile(),0);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ILOG("[AGENT] Unable to remove `" + agent + "' from " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " user agent list");
|
|
}
|
|
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer == "admin.cgi?sid=" + tos(sid) ? "" : "&mode=viewagent"), SHRINK) : MSG_200);
|
|
}
|
|
catch(const exception &ex)
|
|
{
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer != utf8("admin.cgi?sid=" + tos(sid) + "&mode=viewagent") ? "" : "&mode=viewagent"), SHRINK) : MSG_200);
|
|
ELOG(ex.what());
|
|
}
|
|
|
|
sendMessageAndClose(msg);
|
|
}
|
|
|
|
// add an IP / hostname to the user agent list
|
|
void protocol_admincgi::mode_agent(const streamData::streamID_t sid, const utf8 &agent) throw()
|
|
{
|
|
utf8 msg;
|
|
try
|
|
{
|
|
const size_t stream_ID = ((gOptions.read_stream_agentFile(sid) && !gOptions.stream_agentFile(sid).empty()) ? sid : 0);
|
|
if (!g_agentList.find(agent,stream_ID))
|
|
{
|
|
if (g_agentList.add(agent, stream_ID, true))
|
|
{
|
|
ILOG("[AGENT] Added `" + agent + "' to " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " user agent list");
|
|
}
|
|
|
|
stats::kickClientList_t kick_data;
|
|
stats::getClientDataForKicking(stream_ID, kick_data);
|
|
for (stats::kickClientList_t::const_iterator i = kick_data.begin(); i != kick_data.end(); ++i)
|
|
{
|
|
if (!(*i)->m_kicked && ((*i)->m_userAgent == agent))
|
|
{
|
|
stats::kickClient(sid, (*i)->m_unique);
|
|
}
|
|
delete (*i);
|
|
}
|
|
|
|
if (gOptions.saveAgentListOnExit())
|
|
{
|
|
if (stream_ID && gOptions.read_stream_agentFile(stream_ID) && !gOptions.stream_agentFile(stream_ID).empty())
|
|
{
|
|
g_agentList.save(gOptions.stream_agentFile(stream_ID),stream_ID);
|
|
}
|
|
else
|
|
{
|
|
g_agentList.save(gOptions.agentFile(),0);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ILOG("[AGENT] `" + agent + "' already in the " + (!stream_ID ? "global" : "sid=" + tos(stream_ID)) + " user agent list");
|
|
}
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer == "admin.cgi?sid=" + tos(sid) ? "" : "&mode=viewagent"), SHRINK) : MSG_200);
|
|
}
|
|
catch(const exception &ex)
|
|
{
|
|
msg = (!m_referer.empty() ? redirect("admin.cgi?sid=" + tos(sid) + (m_referer != utf8("admin.cgi?sid=" + tos(sid) + "&mode=viewagent") ? "" : "&mode=viewagent"), SHRINK) : MSG_200);
|
|
ELOG(ex.what());
|
|
}
|
|
|
|
sendMessageAndClose(msg);
|
|
}
|
|
|
|
// show agent list
|
|
void protocol_admincgi::mode_viewagent(const streamData::streamID_t sid) throw()
|
|
{
|
|
vector<agentList::agent_t> agent_list;
|
|
g_agentList.get(agent_list,((gOptions.read_stream_agentFile(sid) && !gOptions.stream_agentFile(sid).empty()) ? sid : 0));
|
|
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
headerTitle = (!sid ? "Server Blocked User Agent List" :
|
|
"Stream Blocked User Agent List"),
|
|
body = (!sid ? getServerAdminHeader(headerTitle, 0, "", 2) : getStreamAdminHeader(sid, headerTitle, 0, true)) +
|
|
"<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\"><td>";
|
|
|
|
if (agent_list.empty())
|
|
{
|
|
body += "<b> No Blocked User Agents</b><br>";
|
|
}
|
|
else
|
|
{
|
|
body += "<b> Blocked User Agent List:</b><ol>";
|
|
for (vector<agentList::agent_t>::const_iterator i = agent_list.begin(); i != agent_list.end(); ++i)
|
|
{
|
|
const streamData::source_t clientType = ((streamData::source_t)streamData::UNKNOWN);
|
|
body += "<li>" + getClientImage(streamData::getClientType(clientType, stringUtil::toLower((*i).m_agent))) +
|
|
" <b>" + aolxml::escapeXML((*i).m_agent) + "</b> - <a href=\"admin.cgi?sid=" + tos(sid) +
|
|
"&mode=unagent&agent=" + urlUtils::escapeURI_RFC3986((*i).m_agent) + "\">remove</a>";
|
|
}
|
|
body += "</ol>";
|
|
}
|
|
|
|
body +=
|
|
"</td>"
|
|
"<td style=\"padding: 0 1em 0 0;\"><br>"
|
|
"<table class=\"ent\" border=\"0\" cellspacing=\"0\" cellpadding=\"3\" align=\"left\">"
|
|
"<tr class=\"ent\">"
|
|
"<td class=\"inp\" align=\"center\">Block User Agent</td>"
|
|
"</tr>"
|
|
"<form method=\"url\" action=\"admin.cgi\">"
|
|
"<tr>"
|
|
"<td><table cellspacing=\"0\" cellpadding=\"3\" border=\"0\">"
|
|
"<tr>"
|
|
"<td align=\"center\">Enter the user agent:<br><i>(example: streamripper)</i></td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td align=\"center\">"
|
|
"<input name=\"mode\" value=\"agent\" type=\"hidden\">"
|
|
"<input name=\"sid\" value=\"" + tos(sid) + "\" type=\"hidden\">"
|
|
"<input name=\"agent\" size=\"30\" maxlength=\"256\">"
|
|
"</td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td colspan=\"2\" align=\"center\"><input class=\"submit\" type=\"submit\" value=\"Block User Agent\">"
|
|
" <input class=\"submit\" value=\"Clear\" type=\"Reset\"></td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</td>"
|
|
"</tr>"
|
|
"</form>"
|
|
"</table>" +
|
|
getUptimeScript() +
|
|
getIEFlexFix() +
|
|
getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
// kick client(s)
|
|
void protocol_admincgi::mode_kickdst(const streamData::streamID_t sid, const utf8 &kickAddrs) throw()
|
|
{
|
|
bool refresh = false;
|
|
if (kickAddrs == "all")
|
|
{
|
|
// check if this is set to kick all
|
|
refresh = stats::kickAllClients(sid);
|
|
}
|
|
else if (kickAddrs == "duplicates")
|
|
{
|
|
// check if this is set to kick all
|
|
// duplicates (doing oldest first).
|
|
refresh = stats::kickDuplicateClients(sid);
|
|
}
|
|
else
|
|
{
|
|
// split out multiple clients to kick (split by a ,)
|
|
std::vector<uniString::utf8> addrs = tokenizer(kickAddrs,',');
|
|
for (vector<uniString::utf8>::const_iterator i = addrs.begin(); i != addrs.end(); ++i)
|
|
{
|
|
if ((*i).find(utf8(".")) == utf8::npos)
|
|
{
|
|
stats::kickClient(sid, atoi((*i).hideAsString().c_str()));
|
|
}
|
|
else
|
|
{
|
|
stats::kickClient(sid,(*i));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!m_referer.empty())
|
|
{
|
|
const utf8 check = ("admin.cgi?sid=" + tos(sid));
|
|
// if the referer is the server summary page then we need to go back to it
|
|
sendMessageAndClose(redirect((m_referer == check ? check : "admin.cgi?sid=0") + (refresh ? "&refresh=1" : ""), SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
|
|
void protocol_admincgi::mode_art(const streamData::streamID_t sid, const int mode) throw()
|
|
{
|
|
utf8 header = "HTTP/1.1 200 OK\r\n", body;
|
|
streamData *sd = streamData::accessStream(sid);
|
|
if (sd)
|
|
{
|
|
vector<__uint8> sc21_albumart = (mode == 0 ? sd->streamAlbumArt() : sd->streamPlayingAlbumArt());
|
|
if (!sc21_albumart.empty())
|
|
{
|
|
utf8 mimeType[] = {
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/bmp",
|
|
"image/gif"
|
|
};
|
|
const size_t mime = (mode == 0 ? sd->streamAlbumArtMime() : sd->streamPlayingAlbumArtMime());
|
|
// if not in the valid range then don't report the mime type in the generated response
|
|
if (mime < 4)
|
|
{
|
|
header += "Content-Type:" + mimeType[mime] + "\r\n";
|
|
}
|
|
body += utf8(&sc21_albumart[0],sc21_albumart.size());
|
|
}
|
|
sd->releaseStream();
|
|
}
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_register(const streamData::streamID_t sid, const streamData::streamInfo &info) throw()
|
|
{
|
|
// construct a temp id for this action
|
|
utf8 tempId = "";
|
|
randomId(tempId);
|
|
|
|
bool loaded = false;
|
|
streamData *sd = streamData::accessStream(sid);
|
|
if (sd)
|
|
{
|
|
int addFailIgnore = 0, errorCode = 0;
|
|
loaded = sd->YP2_addSuccessful(addFailIgnore, errorCode);
|
|
sd->releaseStream();
|
|
}
|
|
|
|
utf8 header = "HTTP/1.1 200 OK\r\n"
|
|
"Content-Type:text/html;charset=utf-8\r\n"
|
|
"Cache-Control:no-cache\r\n",
|
|
authhash = (!info.m_authHash.empty() ? info.m_authHash.substr(0, 36) : (utf8)""),
|
|
body = getStreamAdminHeader(sid, "Stream Authhash") + "<br>"
|
|
"<form id=\"processing\" method=\"GET\" autocomplete=\"off\" onsubmit=\"return validateForm();\">"
|
|
|
|
"<div style=\"display:table;width:100%;\">"
|
|
|
|
"<div style=\"padding:0 1em;display:inline-block;float:left;max-width:15em;\">"
|
|
"<table class=\"ent\" cellpadding=\"15px\"><tr><td valign=\"top\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Information</b></div><div id=\"info\">"
|
|
"This page allows you to enter or amend the authhash to be used for this stream."
|
|
"<br><br><hr><br>Authhash information is now managed online. "
|
|
"<a target=\"blank\" href=\"https://radiomanager.shoutcast.com\"><b>Login</b></a> "
|
|
"to create an authhash or update the details of an existing authhash."
|
|
|
|
"<br><br><hr><br>The same authhash should be used for all stream instances of a station "
|
|
"(e.g. 128kbps MP3 and 64kbps AAC streams for 'Super Awesome Radio').<br><br>This also includes all DNAS "
|
|
"providing the stream(s) for a station to ensure the correct listing of all stream instances."
|
|
|
|
// TODO need to have a link to the account page...
|
|
"<br><br><hr><br>If you remove an authhash by mistake then you can either recover it from your "
|
|
"<a href=\"https://radiomanager.shoutcast.com\" target=\"_blank\"><b>Shoutcast account</b></a> or you will need "
|
|
"to contact <a href=\"mailto:support@shoutcast.com?subject=Shoutcast%20Support\"><b>support</b></a> directly for "
|
|
"assistance.</div></td></tr></table><br></div>"
|
|
|
|
// TODO need to consider customising some of this ?? or just above ??
|
|
"<div align=\"left\" id=\"page_info\" style=\"display:inline-block;max-width:25em;padding:0 1em;\">"
|
|
"<div align=\"left\" id=\"header\"></div>"
|
|
"<table id=\"details\" style=\"width:100%;\" align=\"left\" border=\"0\">"
|
|
|
|
"<tr id=\"hide\" valign=\"top\"><td colspan=\"2\">" + utf8(!authhash.empty() &&
|
|
loaded && (info.m_streamSampleRate > 0) && info.m_radionomyID.empty() ?
|
|
warningImage(false) + " <b>Please Register Your Authhash</b><br><br>" : "") +
|
|
|
|
"<div style=\"float:right;padding:0 1em;\">"
|
|
"<input style=\"white-space:normal;width:6em;\" id=\"register\" class=\"submit\" type=\"button\" "
|
|
"onclick=\"window.open('https://radiomanager.shoutcast.com','_blank','')\" value=\"" +
|
|
|
|
(!authhash.empty() ? "Manage Authhash\"></div>"
|
|
"The authhash currently configured for this stream is <b>" + authhash + "</b><br><br><br>"
|
|
"To change the authhash, enter it below and click 'Save'." :
|
|
"Create Authhash\"></div>"
|
|
"An authhash needs to be created for this stream.<br><br><br>"
|
|
"If you have just created an authhash via your "
|
|
"<a target=\"blank\" href=\"https://radiomanager.shoutcast.com\"><b>account</b></a> "
|
|
"or have an existing one for the stream, please enter it below and click 'Save'.") +
|
|
|
|
"<br><br></td></tr>"
|
|
"<tr id=\"hide\">"
|
|
"<td style=\"width:25%;\" align=\"right\">Authhash:</td>"
|
|
"<td colspan=\"2\"><input type=\"text\" style=\"width:15em;\" name=\"authhash\" id=\"authhash\" maxlength=\"36\" value=\"" + authhash + "\"></td>"
|
|
"</tr>"
|
|
"<tr>"
|
|
"<td id=\"status\" align=\"center\" colspan=\"3\"><br>"
|
|
"<input id=\"submit\" class=\"submit\" type=\"submit\" value=\"Save\"> "
|
|
"<input id=\"clear\" class=\"submit\" value=\"Clear\" type=\"button\" "
|
|
"onclick=\"if(confirm('Clear the authhash and save the change now?"
|
|
"\\n\\nChoose Cancel to just clear the field.')){window.location='admin.cgi?sid=" + tos(sid) +
|
|
"&mode=register&register=clear';}else{$('authhash').value = '';authhashChange();}\"> "
|
|
"<input class=\"submit\" value=\"Cancel\" type=\"button\" onclick=\"window.location='admin.cgi?sid=" + tos(sid) + "';\"></td>"
|
|
"</tr>"
|
|
"</table>"
|
|
"</div>"
|
|
"</div>"
|
|
"</form>"
|
|
|
|
"<script type=\"text/javascript\">"
|
|
"var original = \"" + /*(((mode == 3) && (auth_enabled != 1)) ?*/ info.m_authHash.substr(0, 36) /*: (utf8)"")*/ + "\";" EL
|
|
|
|
"var timeout, response;" EL
|
|
"function $(id){return document.getElementById(id);}" EL
|
|
"function trimString(str){return str.replace(/^\\s+|\\s+$/g,'');}" EL
|
|
+ utf8(!authhash.empty() ?
|
|
"function changeExisting(){" EL
|
|
"if($('existing').value != \"select\"){" EL
|
|
"$('authhash').value = $('existing').value;" EL
|
|
"}" EL
|
|
"authhashChange();" EL
|
|
"}" EL
|
|
: "")
|
|
+
|
|
|
|
"function getHTTP(){" EL
|
|
"if(window.XDomainRequest){" EL
|
|
"return new XDomainRequest();" EL
|
|
"}else if(window.XMLHttpRequest){" EL
|
|
"return new XMLHttpRequest();" EL
|
|
"}else{" EL
|
|
"return new ActiveXObject(\"Microsoft.XMLHTTP\");" EL
|
|
"}" EL
|
|
"}" EL
|
|
|
|
"function isValidAuthhash(str){" EL
|
|
"var regexp = /^[-a-fA-F\\d]+$/;" EL
|
|
"if(str != ''){" EL
|
|
"return regexp.test(str);" EL
|
|
"}" EL
|
|
"return true;" EL
|
|
"}" EL
|
|
|
|
"function authhashChange(){" EL
|
|
"var str = trimString($('authhash').value);" EL
|
|
"var valid = isValidAuthhash(str);" EL
|
|
"$('authhash').style.borderColor = (!(str.length == 0 || str.length > 36 && valid)?\"red\":\"\");" EL
|
|
"$('submit').disabled = (!((str.length == 0 || str.length == 36) && (original != $('authhash').value) && valid));" EL
|
|
"}" EL
|
|
|
|
"function validateForm(){" EL
|
|
"while($('hide') != null){" EL
|
|
"$('hide').style.display = \"none\";" EL
|
|
"$('hide').removeAttribute(\"id\");" EL
|
|
"}" EL
|
|
"$('status').setAttribute(\"colspan\",\"3\");" EL
|
|
"$('status').setAttribute(\"align\",\"left\");" EL
|
|
"$('status').setAttribute(\"valign\",\"middle\");" EL
|
|
"$('status').innerHTML = \"</td><td><b>Processing</b><br><br>This may take a while.</td>\";" EL
|
|
|
|
"var f = $('processing');" EL
|
|
"var params=\"\";" EL
|
|
"for(var i = 0; i < f.elements.length; i++ ){" EL
|
|
"if(f.elements[i].name != \"\"){" EL
|
|
"if(f.elements[i].name != \"private\" || (f.elements[i].name == \"private\" && f.elements[i].value == \"1\")){" EL
|
|
"params += (i != 0 ? \"&\" : \"\") + f.elements[i].name + \"=\" + encodeURIComponent(f.elements[i].value);" EL
|
|
"}" EL
|
|
"}" EL
|
|
"}" EL
|
|
"if(params==\"\"){" EL
|
|
"$('status').setAttribute(\"colspan\",\"3\");" EL
|
|
"$('status').setAttribute(\"align\",\"center\");" EL
|
|
"$('status').setAttribute(\"valign\",\"middle\");" EL
|
|
"$('status').innerHTML=\"Critical error in processing request."
|
|
"<br><br><b><a href=\\\"admin.cgi?sid=" + tos(sid) + "\\\">Click here to return to the Server Summary.</a></b>\";"
|
|
"return false;"
|
|
"}" EL
|
|
|
|
"var xmlhttp = getHTTP();" EL
|
|
"xmlhttp.open(\"GET\",\"admin.cgi?sid=" + tos(sid) + "&pass=" + gOptions.adminPassword() +
|
|
"&mode=manualauthhash&tempid=" + tempId + "&\"+params,true);" EL
|
|
"if(window.XDomainRequest){" EL
|
|
"xmlhttp.onerror=xmlhttp.onload=function(){" EL
|
|
"var code = parseInt((xmlhttp.responseText!=\"\"?xmlhttp.responseText:\"200\"));" EL
|
|
|
|
"if(code!=200){" EL
|
|
"clearInterval(timeout);" EL
|
|
"$('status').setAttribute(\"align\",\"center\");" EL
|
|
"$('status').setAttribute(\"colspan\",\"2\");" EL
|
|
"if(code==0){" EL
|
|
"$('status').innerHTML = \"</td><td>"
|
|
"<b><br>Error Code: \"+code+\".<br>Check the DNAS is running and there is a working network connection.<br>"
|
|
"<br><a href=\\\"admin.cgi?sid=" + tos(sid) + "\\\">Click here to return to the Server Summary.</a></b>"
|
|
"</td>\";" EL
|
|
"}else{" EL
|
|
"$('status').innerHTML = \"</td><td>"
|
|
"<b><br>Error Code: \"+code+\".<br>\"+xmlhttp.responseText.substring(5,xmlhttp.responseText.length)+\"<br>"
|
|
"<br><a href=\\\"admin.cgi?sid=" + tos(sid) + "\\\">Click here to return to the Server Summary.</a></b>"
|
|
"</td>\";" EL
|
|
"}" EL
|
|
"}else{" EL
|
|
"clearInterval(timeout);" EL
|
|
"$('status').setAttribute(\"align\",\"center\");" EL
|
|
"$('status').innerHTML = \"</td><td>"
|
|
"Authhash was changed and saved to the configuration file. The stream will now be updated with the change.<br>"
|
|
"<br><a href=\\\"admin.cgi?sid=" + tos(sid) + "\\\"><b>Click here to return to the stream summary.</b></a></td>\";" EL
|
|
"}" EL
|
|
|
|
"};" EL
|
|
"}else{" EL
|
|
"xmlhttp.onreadystatechange=function(){" EL
|
|
"if(xmlhttp.readyState==4){" EL
|
|
"var code = parseInt((xmlhttp.responseText!=\"\"?xmlhttp.responseText:\"200\"));" EL
|
|
"if(code!=200){" EL
|
|
"clearInterval(timeout);" EL
|
|
"$('status').setAttribute(\"align\",\"center\");" EL
|
|
"$('status').setAttribute(\"colspan\",\"2\");" EL
|
|
"if(code==0){" EL
|
|
"$('status').innerHTML = \"</td><td>"
|
|
"<b><br>Error Code: \"+code+\".<br>Check the DNAS is running and there is a working network connection.<br>"
|
|
"<br><a href=\\\"admin.cgi?sid=" + tos(sid) + "\\\">Click here to return to the Server Summary.</a></b>"
|
|
"</td>\";" EL
|
|
"}else{" EL
|
|
"$('status').innerHTML = \"</td><td>"
|
|
"<b><br>Error Code: \"+code+\".<br>\"+xmlhttp.responseText.substring(5,xmlhttp.responseText.length)+\"<br>"
|
|
"<br><a href=\\\"admin.cgi?sid=" + tos(sid) + "\\\">Click here to return to the Server Summary.</a></b>"
|
|
"</td>\";" EL
|
|
"}" EL
|
|
"}" EL
|
|
"}else{" EL
|
|
"clearInterval(timeout);" EL
|
|
"$('status').setAttribute(\"align\",\"center\");" EL
|
|
"$('status').innerHTML = \"</td><td>"
|
|
"Authhash was changed and saved to the configuration file. The stream will now be updated with the change.<br>"
|
|
"<br><a href=\\\"admin.cgi?sid=" + tos(sid) + "\\\"><b>Click here to return to the stream summary.</b></a></td>\";" EL
|
|
"}" EL
|
|
"};" EL
|
|
"}" EL
|
|
"xmlhttp.send(null);" EL
|
|
"timeout = setInterval(countDown,250);" EL
|
|
"return false;" EL
|
|
"}" EL
|
|
|
|
"var counter=0;" EL
|
|
"function countDown(){" EL
|
|
"counter++;" EL
|
|
"if(counter>=5){" EL
|
|
"counter=0;" EL
|
|
"}" EL
|
|
"if(counter<5){" EL
|
|
"$('status').setAttribute(\"colspan\",\"3\");" EL
|
|
"$('status').setAttribute(\"align\",\"left\");" EL
|
|
"$('status').setAttribute(\"valign\",\"middle\");" EL
|
|
"$('status').innerHTML = \"<b>Processing\"+Array(counter).join(\".\")+\"</b><br><br>This may take a while.\";" EL
|
|
"}" EL
|
|
"}" EL
|
|
|
|
"function runUrlGetError(){" EL
|
|
"}" EL
|
|
|
|
"function runUrlGet(urlString,callback){" EL
|
|
"var xmlhttp = getHTTP();" EL
|
|
"try{" EL
|
|
"xmlhttp.open(\"GET\",(urlString==document.location?urlString:\"http://" +
|
|
gOptions.ypAddr() + ":" + tos(gOptions.ypPort()) +
|
|
((gOptions.ypPath() != utf8("/yp2")) ? "/yp" : "") +
|
|
"/\"+urlString),true);" EL
|
|
"if(window.XDomainRequest){" EL
|
|
"xmlhttp.onload=callback;" EL
|
|
"xmlhttp.onerror=runUrlGetError;" EL
|
|
"}else{" EL
|
|
"xmlhttp.onreadystatechange=callback;" EL
|
|
"}" EL
|
|
"response=xmlhttp;" EL
|
|
"xmlhttp.send(null);" EL
|
|
"}" EL
|
|
"catch(e){" EL
|
|
"}" EL
|
|
"}" EL
|
|
|
|
"function getAuthInfo(){" EL
|
|
"if(response.readyState == null || response.readyState==4 && response.status==200){" EL
|
|
"$('header').innerHTML = response.responseText;" EL
|
|
"}" EL
|
|
"}" EL
|
|
|
|
"var registerOnWindowLoad = function(callback){" EL
|
|
"if(window.addEventListener){" EL
|
|
"window.addEventListener('load',callback,false);" EL
|
|
"}else{" EL
|
|
"window.attachEvent('onload',callback);" EL
|
|
"}" EL
|
|
"}" EL
|
|
|
|
"registerOnWindowLoad(function(){" EL
|
|
"runUrlGet(\"authinfo_" + (authhash.empty() ? "create" : "update") +
|
|
"?v=" + urlUtils::escapeURI_RFC3986(gOptions.getVersionBuildStrings()) +
|
|
"&os=" + urlUtils::escapeURI_RFC3986(SERV_OSNAME) + "\",getAuthInfo);" EL
|
|
"if($('existing')!=null){" EL
|
|
"$('existing').onkeyup=changeExisting;" EL
|
|
"$('existing').onchange=changeExisting;" EL
|
|
"}" EL
|
|
"if($('authhash')!=null){" EL
|
|
"$('authhash').onkeyup=authhashChange;" EL
|
|
"$('authhash').onchange=authhashChange;" EL
|
|
"}" EL
|
|
"authhashChange();" EL
|
|
"});" EL
|
|
"</script>" +
|
|
getUptimeScript() +
|
|
getIEFlexFix() +
|
|
getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_listeners(const streamData::streamID_t sid) throw()
|
|
{
|
|
utf8 header = MSG_NO_CLOSE_200, body;
|
|
// just to make sure we came from an appropriate page
|
|
const utf8 &server = mapGet(m_httpRequestInfo.m_QueryParameters, "server", (utf8)""),
|
|
&check = ("admin.cgi?sid=" + tos(sid));
|
|
const bool nowrap = mapGet(m_httpRequestInfo.m_QueryParameters, "nw", (bool)gOptions.adminNoWrap());
|
|
const int fh = mapGet(m_httpRequestInfo.m_QueryParameters, "fh", (int)0);
|
|
if ((sid > 0) && (m_referer.find(check) == 0) && !server.empty() && (listenerId == server))
|
|
{
|
|
stats::currentClientList_t client_data;
|
|
stats::getClientDataForStream(sid, client_data);
|
|
if (!client_data.empty())
|
|
{
|
|
streamData::streamInfo info;
|
|
streamData::extraInfo extra;
|
|
streamData::getStreamInfo(sid, info, extra);
|
|
|
|
stats::statsData_t data;
|
|
stats::getStats(sid, data);
|
|
|
|
utf8 clientsBody = "<div style=\"overflow:auto;" + (!fh ? "max-height:500px;" : (utf8)"") + "\">"
|
|
"<table class=\"ls\" style=\"border:0;text-align:center;" +
|
|
(nowrap ? "white-space:nowrap;" : "") + "\" "
|
|
"cellpadding=\"5\" cellspacing=\"0\" width=\"100%\" align=\"center\">"
|
|
"<col width=\"15%\"><col width=\"50%\"><col width=\"20%\">"
|
|
"<tr><td colspan=\"10\" class=\"inp\">Current Listeners</td></tr>"
|
|
"<tr class=\"tll\"><td>Listener Address<br>(Host Address)</td>"
|
|
"<td>User Agent</td><td>Connected<br>Duration</td>" + utf8(!info.m_radionomyID.empty() ?
|
|
"<td>" + baseImage("adavail", "Advert Status", false, false) + "</td>" : "") +
|
|
(data.connectedListeners != data.uniqueListeners ? "<td>Kick<br>Client</td>" : (utf8)"") +
|
|
"<td>Kick<br>IP</td><td>Ban<br>IP</td><td>Ban<br>Subnet</td>"
|
|
"<td>Reserve<br>Listener</td><td>Block<br>User Agent</td></tr>";
|
|
|
|
const time_t t = ::time(NULL);
|
|
size_t rowCount = 0;
|
|
// if we have non-unique clients then we need to process the list differently so as to group
|
|
// them together and then re-sort the output by client listener duration on the first match
|
|
// otherwise we revert the code back to the original un-grouped method to not waste resources
|
|
if (data.connectedListeners != data.uniqueListeners)
|
|
{
|
|
map<utf8,stats::uniqueClientData_t> unique_clients;
|
|
for (stats::currentClientList_t::const_iterator i = client_data.begin(); i != client_data.end(); ++i)
|
|
{
|
|
// if set to kicked then no need to show those (since they may re-appear if in-progress)
|
|
if (!(*i)->m_kicked)
|
|
{
|
|
stats::uniqueClientData_t client;
|
|
const bool localhost = ((*i)->m_ipAddr.find(utf8("127.")) == 0);
|
|
|
|
// look for existing instances and append the new details to the existing details
|
|
const map<utf8,stats::uniqueClientData_t>::const_iterator im = unique_clients.find((*i)->m_ipAddr);
|
|
if (im != unique_clients.end())
|
|
{
|
|
client = (*im).second;
|
|
client.m_userAgent += "</tr><tr" + ((*i)->m_ripClient || localhost ? utf8(" style=\"font-style:italic;\"") : "") + ">";
|
|
client.m_unique += ",";
|
|
++client.m_total;
|
|
}
|
|
else
|
|
{
|
|
client.m_connectTime = (*i)->m_startTime;
|
|
client.m_ipAddr = (*i)->m_ipAddr;
|
|
client.m_hostName = (*i)->m_hostName;
|
|
client.m_XFF = (*i)->m_XFF;
|
|
client.m_total = 1;
|
|
}
|
|
|
|
const int slave = ((*i)->m_clientType & streamData::SC_CDN_SLAVE);
|
|
client.m_userAgent += "<td " + (!client.m_XFF.empty() ? "title=\"XFF: " + aolxml::escapeXML(client.m_XFF) +
|
|
"\" " : "") + "style=\"" + (!nowrap ? "" : "white-space:nowrap;") + "\"" +
|
|
utf8(slave ? " class=\"thr\"" : "") + ">" + (((*i)->m_clientType & streamData::RADIONOMY) ?
|
|
"Radionomy Stats Collector" : (!nowrap ? addWBR((*i)->m_userAgent) : "<div style=\"float:left;\">" +
|
|
aolxml::escapeXML((*i)->m_userAgent)) + "</div>") + " <div style=\"float:right;\">" +
|
|
getClientImage((*i)->m_clientType) + "</div></td>";
|
|
if ((*i)->m_ripClient)
|
|
{
|
|
client.m_ripAddr = true;
|
|
}
|
|
|
|
const time_t connected = (t - (*i)->m_startTime);
|
|
utf8 timer = timeString(connected, true), timerTip;
|
|
if (timer.empty())
|
|
{
|
|
timer = "Starting...";
|
|
}
|
|
else
|
|
{
|
|
timerTip = timeString(connected);
|
|
}
|
|
|
|
client.m_userAgent += "<td" + utf8(slave ? " class=\"thr\"" : "") + " title=\"" +
|
|
timerTip + "\">" + aolxml::escapeXML(timer) + "</td>";
|
|
client.m_unique = (*i)->m_ipAddr;
|
|
if (!info.m_radionomyID.empty())
|
|
{
|
|
client.m_userAgent += "<td" + utf8(slave ? " class=\"thr\"" : "") + ">" + (((*i)->m_group > 0) ||
|
|
(*i)->m_triggers ? advertImage(sid, (*i)->m_group, (*i)->m_triggers) :
|
|
"<div title=\"Not Recognised For Adverts\">N/A</div>") + "</td>";
|
|
}
|
|
client.m_userAgent += "<td^" + utf8(slave ? " class=\"thr\"" : "") + "><a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=kickdst&kickdst=" + tos((*i)->m_unique) + "\">Kick</a>" +
|
|
(client.m_total == 1 ? "</td^>" : "</td>") + "<td" + utf8(slave ? " class=\"thr\"" : "") +
|
|
">" + ((*i)->m_userAgent.empty() || ((*i)->m_userAgent == EMPTY_AGENT) ? "N/A" :
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=agent&agent=" +
|
|
urlUtils::escapeURI_RFC3986((*i)->m_userAgent) + "\">Block</a>") + "</td>";
|
|
|
|
unique_clients[(*i)->m_ipAddr] = client;
|
|
}
|
|
|
|
delete (*i);
|
|
}
|
|
|
|
// take the map and convert to a vector so we can then re-sort the list back into longest to least duration
|
|
vector<stats::uniqueClientData_t> clients;
|
|
for (map<utf8,stats::uniqueClientData_t>::const_iterator i = unique_clients.begin(); i != unique_clients.end(); ++i)
|
|
{
|
|
clients.push_back((*i).second);
|
|
}
|
|
|
|
std::sort(clients.begin(), clients.end(), sortUniqueClientDataByTime);
|
|
|
|
// and now we dump the generated list
|
|
for (vector<stats::uniqueClientData_t>::const_iterator i = clients.begin(); i != clients.end(); ++i)
|
|
{
|
|
const utf8 host = ((*i).m_hostName != (*i).m_ipAddr ? aolxml::escapeXML((*i).m_hostName) + " (" + (*i).m_ipAddr + ")" : (*i).m_ipAddr);
|
|
const bool localhost = ((*i).m_ipAddr.find(utf8("127.")) == 0);
|
|
|
|
// if we have a multiple client block then re-process so the relevant parts can
|
|
// be listed individually with adjustment of some of the visual styles as needed
|
|
utf8 multiBlock = (*i).m_userAgent;
|
|
uniString::utf8::size_type tpos = multiBlock.find(utf8("<td^"));
|
|
if (tpos != uniString::utf8::npos)
|
|
{
|
|
while (tpos != uniString::utf8::npos)
|
|
{
|
|
multiBlock.replace(tpos, 4, utf8(((*i).m_total > 1 ? "<td" : "<td colspan=\"2\"")));
|
|
tpos = multiBlock.find(utf8("<td^"));
|
|
}
|
|
}
|
|
|
|
tpos = multiBlock.find(utf8("</td^>"));
|
|
if (tpos != uniString::utf8::npos)
|
|
{
|
|
const bool hasHostName = ((*i).m_hostName != (*i).m_ipAddr);
|
|
utf8 endBlock = "<td" + ((*i).m_total > 1 ? " rowspan=\"" + tos((*i).m_total) + "\"" : (utf8)"") + ">" +
|
|
(!localhost ? "<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=bandst&bandst=" +
|
|
urlUtils::escapeURI_RFC3986((*i).m_ipAddr) + "&banmsk=255" + "&kickdst=" +
|
|
(*i).m_unique + "\">Ban</a>" : "N/A") + "</td><td" + ((*i).m_total > 1 ? " rowspan=\"" +
|
|
tos((*i).m_total) + "\"" : (utf8)"") + ">" + (!localhost ? "<a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=bandst&bandst=" + urlUtils::escapeURI_RFC3986((*i).m_ipAddr) +
|
|
"&banmsk=0" + "&kickdst=" + (*i).m_unique + "\">Ban </a>" : "N/A") + "</td>"
|
|
"<td" + ((*i).m_total > 1 ? " rowspan=\"" + tos((*i).m_total) + "\"" : (utf8)"") + ">" +
|
|
(!localhost ? "<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=" +
|
|
((*i).m_ripAddr ? "unripdst" : "ripdst") + "&ripdst=" +
|
|
urlUtils::escapeURI_RFC3986((hasHostName ? (*i).m_hostName : (*i).m_ipAddr)) +
|
|
(hasHostName ? "&ripdstraw=" + urlUtils::escapeURI_RFC3986((*i).m_ipAddr) : "") +
|
|
"\">" + ((*i).m_ripAddr ? "Remove" : "Add") + "</a>" : "N/A") + "</td>";
|
|
|
|
multiBlock.replace(tpos, 6, utf8(((*i).m_total > 1 ?
|
|
"</td><td rowspan=\"" + tos((*i).m_total) +
|
|
"\"><a href=\"admin.cgi?sid=" + tos(sid) +
|
|
"&mode=kickdst&kickdst=" +
|
|
(*i).m_unique + "\">Kick</a></td>" +
|
|
endBlock : "</td>" + endBlock)));
|
|
}
|
|
|
|
rowCount += (*i).m_total;
|
|
clientsBody += "<tr" + ((*i).m_ripAddr || localhost ? utf8(" style=\"font-style:italic;\"") : "") + ">"
|
|
"<td" + ((*i).m_total > 1 ? " rowspan=\"" + tos((*i).m_total) + "\"" : "") + ">" +
|
|
(gOptions.useXFF() && !(*i).m_XFF.empty() ? xffImage() + " " : "") + host + "</td>" + multiBlock;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (stats::currentClientList_t::const_iterator i = client_data.begin(); i != client_data.end(); ++i)
|
|
{
|
|
// if set to kicked then no need to show those (since they may re-appear if in-progress)
|
|
if (!(*i)->m_kicked)
|
|
{
|
|
const time_t connected = (::time(NULL) - (*i)->m_startTime);
|
|
utf8 timer = timeString(connected, true), timerTip;
|
|
if (timer.empty())
|
|
{
|
|
timer = "Starting...";
|
|
}
|
|
else
|
|
{
|
|
timerTip = timeString(connected);
|
|
}
|
|
|
|
const utf8 host = ((*i)->m_hostName != (*i)->m_ipAddr ? aolxml::escapeXML((*i)->m_hostName) +
|
|
" (" + (*i)->m_ipAddr + ")" : (*i)->m_ipAddr);
|
|
const bool localhost = ((*i)->m_ipAddr.find(utf8("127.")) == 0);
|
|
const bool hasHostName = ((*i)->m_hostName != (*i)->m_ipAddr);
|
|
|
|
++rowCount;
|
|
const int slave = ((*i)->m_clientType & streamData::SC_CDN_SLAVE);
|
|
clientsBody += "<tr" + ((*i)->m_ripClient || localhost ? utf8(" style=\"font-style:italic;\"") : "") + ">"
|
|
"<td " + (gOptions.useXFF() && !(*i)->m_XFF.empty() ? "title=\"XFF: " +
|
|
aolxml::escapeXML((*i)->m_XFF) + "\">" + xffImage() + " " : ">") + host + "</td><td " +
|
|
(!(*i)->m_XFF.empty() ? "title=\"XFF: " + aolxml::escapeXML((*i)->m_XFF) + "\" " : "") +
|
|
"style=\"" + (!nowrap ? "" : "white-space:nowrap;") + "\"" + utf8(slave ? " class=\"thr\"" :
|
|
"") + ">" + (((*i)->m_clientType & streamData::RADIONOMY) ? "Radionomy Stats Collector" :
|
|
(!nowrap ? addWBR((*i)->m_userAgent) : "<div style=\"float:left;\">" +
|
|
aolxml::escapeXML((*i)->m_userAgent)) + "</div>") + " <div style=\"float:right;\">" +
|
|
getClientImage((*i)->m_clientType) + "</div>" + "</td><td" + utf8(slave ? " class=\"thr\"" : "") +
|
|
" title=\"" + timerTip + "\">" + aolxml::escapeXML(timer) + "</td>";
|
|
|
|
if (!info.m_radionomyID.empty())
|
|
{
|
|
clientsBody += "<td" + utf8(slave ? " class=\"thr\"" : "") + ">" + (((*i)->m_group > 0) ||
|
|
(*i)->m_triggers ? advertImage(sid, (*i)->m_group, (*i)->m_triggers) :
|
|
"<div title=\"Not Recognised For Adverts\">N/A</div>") + "</td>";
|
|
}
|
|
|
|
const utf8 unique = tos((*i)->m_unique);
|
|
clientsBody += "<td" + utf8(slave ? " class=\"thr\"" : "") + "><a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=kickdst&kickdst=" + unique + "\">Kick</a></td><td>" +
|
|
(!localhost ? "<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=bandst&bandst=" +
|
|
urlUtils::escapeURI_RFC3986((*i)->m_ipAddr) + "&banmsk=255" + "&kickdst=" + unique +
|
|
"\">Ban</a>" : "N/A") + "</td><td>" + (!localhost ? "<a href=\"admin.cgi?sid=" + tos(sid) +
|
|
"&mode=bandst&bandst=" + urlUtils::escapeURI_RFC3986((*i)->m_ipAddr) + "&banmsk=0"
|
|
"&kickdst=" + unique + "\">Ban </a>" : "N/A") + "</td><td>" + (!localhost ?
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=" + ((*i)->m_ripClient ?
|
|
"unripdst" : "ripdst") + "&ripdst=" + urlUtils::escapeURI_RFC3986((hasHostName ?
|
|
(*i)->m_hostName : (*i)->m_ipAddr)) + (hasHostName ? "&ripdstraw=" +
|
|
urlUtils::escapeURI_RFC3986((*i)->m_ipAddr) : "") + "\">" + ((*i)->m_ripClient ?
|
|
"Remove" : "Add") + "</a>" : "N/A") + "</td><td" + utf8(slave ? " class=\"thr\"" : "") +
|
|
">" + ((*i)->m_userAgent.empty() || ((*i)->m_userAgent == EMPTY_AGENT) ? "N/A" :
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=agent&agent=" +
|
|
urlUtils::escapeURI_RFC3986((*i)->m_userAgent) + "\">Block</a>") + "</td></tr>";
|
|
}
|
|
|
|
delete (*i);
|
|
}
|
|
}
|
|
|
|
if (rowCount > 0)
|
|
{
|
|
if (rowCount > 1)
|
|
{
|
|
clientsBody += "<tr><td style=\"border:0;\"></td><td style=\"padding:0;\"><a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=kickdst&kickdst=duplicates\" title=\"Kick all duplicate "
|
|
"listeners (based on oldest first by user-agent for the same address)\"><b>Kick Duplicates</b>"
|
|
"</a></td><td>" + timeString(data.avgUserListenTime, true) + "</td><td style=\"padding:0;\" "
|
|
"colspan=\"" + utf8(data.connectedListeners != data.uniqueListeners ? "4" : "3") +
|
|
"\"><a href=\"admin.cgi?sid=" + tos(sid) + "&mode=kickdst&kickdst=all\" "
|
|
"title=\"Kick all currently connected listeners\"><b>Kick All</b></a></td></tr>";
|
|
}
|
|
|
|
// only output the table if we actually had clients to show
|
|
body += clientsBody + "</table></div>";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
body = "<div style=\"padding:1em;text-align:center;\"><b><img "
|
|
"border=\"0\" src=\"images/warn.png\"> The current "
|
|
"listener list could not be loaded. <img border=\"0\" "
|
|
"src=\"images/warn.png\"><br><a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&nw=" + tos(nowrap) + "&fh=" + tos(fh) +
|
|
"\">Click here to reload this page</a>. If this issue "
|
|
"<br>persists, try logging out and back in again.</b></div>";
|
|
}
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
const utf8 protocol_admincgi::getClientIP(const bool streamPublic, const utf8 &publicIP) throw()
|
|
{
|
|
// test for potentially invalid IPs or ones that will cause the playlist link generation to fail
|
|
// attempting to use the server path provided by the YP if in public mode, otherwise uses 'host'
|
|
return (!streamPublic || publicIP.empty() ?
|
|
((g_IPAddressForClients.find(utf8("0.")) == 0 ||
|
|
// allow localhost / loopback connections through for admin access
|
|
((!g_IPAddressForClients.empty() && g_IPAddressForClients.find(utf8("127.")) == 0)) ||
|
|
g_IPAddressForClients.empty()) && !m_hostIP.empty() ?
|
|
m_hostIP : (!m_hostIP.empty() ? m_hostIP : g_IPAddressForClients)) : publicIP);
|
|
}
|
|
|
|
void protocol_admincgi::mode_none(const streamData::streamID_t sid, const int refreshRequired) throw()
|
|
{
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
body = getStreamAdminHeader(sid, "Stream Status & Listeners", refreshRequired);
|
|
|
|
time_t streamUptime = 0;
|
|
bool hasListeners = false, isConnected = false;
|
|
|
|
if (refreshRequired > 0)
|
|
{
|
|
body += "<br><table cellpadding=\"5\" cellspacing=\"0\" border=\"0\" width=\"100%\">"
|
|
"<tr><td align=\"center\" id=\"counter\">"
|
|
"<br>Waiting " + tos(refreshRequired) + " second" + ((refreshRequired > 1) ? "s" : (utf8)"") +
|
|
" for any configuration changes to take effect.<br>"
|
|
"If not automatically redirected or do not want to wait, "
|
|
"<a href=\"admin.cgi?sid="+tos(sid)+"\">click here.</a>"
|
|
"<br><br></td></tr></table></tr></table></td></tr></table>";
|
|
}
|
|
else
|
|
{
|
|
streamData::streamInfo info;
|
|
streamData::extraInfo extra;
|
|
streamData::getStreamInfo(sid, info, extra);
|
|
|
|
stats::statsData_t data;
|
|
stats::getStats(sid, data);
|
|
|
|
isConnected = extra.isConnected;
|
|
hasListeners = (data.connectedListeners > 0);
|
|
|
|
// this is a placeholder for the listener details which we grab asynchronously for speed since #615
|
|
body += "<div id=\"listeners\">" + (data.connectedListeners > 0 ?
|
|
"<div style=\"padding:1em;text-align:center;\"><b>Loading current "
|
|
"listener list...</b></div>" : (utf8)"") + "</div><table cellpadding=\"5\" "
|
|
"cellspacing=\"0\" border=\"0\" width=\"100%\"><tr><td class=\"tsp\" "
|
|
"align=\"center\">Current Stream Information</td></tr></table>";
|
|
|
|
utf8 detailsBody = "";
|
|
bool showing = false;
|
|
if (!(gOptions.read_stream_adminPassword(sid) && !gOptions.stream_adminPassword(sid).empty()))
|
|
{
|
|
utf8 log = gOptions.realLogFile();
|
|
utf8::size_type pos = log.rfind(fileUtil::getFilePathDelimiter());
|
|
if ((pos != utf8::npos))
|
|
{
|
|
log = log.substr(pos + 1);
|
|
}
|
|
|
|
utf8 conf = gOptions.confFile();
|
|
pos = conf.rfind(fileUtil::getFilePathDelimiter());
|
|
if ((pos != utf8::npos))
|
|
{
|
|
conf = conf.substr(pos + 1);
|
|
}
|
|
|
|
// trim down the file paths shown to make things less cluttered on hosted setups
|
|
// places the full path in the 'title' so it can still be found if required, etc
|
|
detailsBody += "Log file: <b title=\"" +
|
|
aolxml::escapeXML(fileUtil::getFullFilePath(gOptions.realLogFile())) +
|
|
"\">" + aolxml::escapeXML(fileUtil::stripPath(log)) + "</b><br>"
|
|
"Configuration file: <b title=\"" +
|
|
aolxml::escapeXML(fileUtil::getFullFilePath(gOptions.confFile())) +
|
|
"\">" + aolxml::escapeXML(fileUtil::stripPath(conf)) + "</b><br>";
|
|
|
|
showing = true;
|
|
}
|
|
|
|
utf8 introFile = gOptions.stream_introFile(sid);
|
|
if (!gOptions.read_stream_introFile(sid))
|
|
{
|
|
introFile = gOptions.introFile();
|
|
}
|
|
|
|
utf8 backupFile = gOptions.stream_backupFile(sid);
|
|
if (!gOptions.read_stream_backupFile(sid))
|
|
{
|
|
backupFile = gOptions.backupFile();
|
|
}
|
|
|
|
utf8 backupTitle = gOptions.stream_backupTitle(sid);
|
|
if (!gOptions.read_stream_backupTitle(sid))
|
|
{
|
|
backupTitle = gOptions.backupTitle();
|
|
}
|
|
|
|
streamData *sd = streamData::accessStream(sid);
|
|
detailsBody += utf8(showing ? "<br><hr><br>" : "") + "Intro file is <b title=\"" +
|
|
((!introFile.empty() || (sd && sd->getIntroFile().gotData()) ? (!introFile.empty() ?
|
|
fileUtil::getFullFilePath(introFile) : (utf8)"") : (utf8)"") + "\">" +
|
|
aolxml::escapeXML((!introFile.empty() || (sd && sd->getIntroFile().gotData()) ? (!introFile.empty() ?
|
|
fileUtil::stripPath(introFile) : "from source") : "empty")) + "</b><br>Backup file is <b title=\"" +
|
|
((!backupFile.empty() || (sd && sd->getBackupFile().gotData())) ? (!backupFile.empty() ?
|
|
fileUtil::getFullFilePath(backupFile) : (utf8)"") : (utf8)"") + "\">" +
|
|
aolxml::escapeXML((!backupFile.empty() || (sd && sd->getBackupFile().gotData())) ?
|
|
(!backupFile.empty() ? fileUtil::stripPath(backupFile) : "from source") : "empty")) + "</b><br>";
|
|
|
|
if (!backupTitle.empty() && !backupFile.empty() && !extra.isConnected)
|
|
{
|
|
detailsBody += "Backup title is: <b>" + aolxml::escapeXML(backupTitle) + "</b><br>";
|
|
}
|
|
|
|
detailsBody += "<br><hr><br>Idle timeouts are <b>" + tos(gOptions.getAutoDumpTime(sid)) + "s</b><br>";
|
|
|
|
if (extra.isConnected)
|
|
{
|
|
detailsBody += "<br><hr><br>Source connection type: <b>" +
|
|
utf8(info.m_sourceType == streamData::SHOUTCAST1 ? "v1" :
|
|
(info.m_sourceType == streamData::SHOUTCAST2 ? "v2" : "HTTP")) +
|
|
(extra.isRelay ? " relay" + (extra.isBackup ? utf8(" backup") : "") :
|
|
(extra.isBackup ? utf8(" backup") : "")) + "</b><br><div "
|
|
"style=\"max-width:15em;\">Source user agent: <b>" +
|
|
addWBR((!info.m_sourceIdent.empty() ? info.m_sourceIdent :
|
|
"Legacy / Unknown")) + "</b>""</div>";
|
|
}
|
|
|
|
if (sd)
|
|
{
|
|
detailsBody += (extra.isConnected ? "<br><hr><br>" : "<br>");
|
|
|
|
if (sd->streamAlbumArt().empty())
|
|
{
|
|
detailsBody += "Stream artwork <b>not available</b>";
|
|
}
|
|
else
|
|
{
|
|
detailsBody += "Stream artwork <b>available</b> [ <a href=\"/streamart?sid=" + tos(sid) + "\">view</a> ]</b>";
|
|
}
|
|
|
|
detailsBody += "<br>";
|
|
|
|
if (sd->streamPlayingAlbumArt().empty())
|
|
{
|
|
detailsBody += "Playing artwork <b>not available</b>";
|
|
}
|
|
else
|
|
{
|
|
detailsBody += "Playing artwork <b>available</b> [ <a href=\"/playingart?sid=" + tos(sid) + "\">view</a> ]</b>";
|
|
}
|
|
|
|
if (!info.m_currentURL.empty() && (info.m_currentURL.find(utf8("DNAS/")) == utf8::npos))
|
|
{
|
|
detailsBody += "<br><br><hr><br>Song url from source [ <a href=\"" +
|
|
utf8((info.m_currentURL.find(utf8("://")) == utf8::npos) &&
|
|
(info.m_currentURL.find(utf8("&")) != 0) ? "//" : "") +
|
|
info.m_currentURL + "\">view</a> ]<br>"
|
|
"<div style=\"max-width:15em;\"><b>Note:</b> "
|
|
"This may not be a valid url and is intended for internal use.</div>";
|
|
}
|
|
|
|
sd->releaseStream();
|
|
}
|
|
|
|
const utf8& message = streamData::getStreamMessage(sid);
|
|
if (!message.empty())
|
|
{
|
|
detailsBody += "<tr><td style=\"border:0;padding:0;\"><br></td></tr>"
|
|
"<tr><td align=\"center\" valign=\"top\" "
|
|
"style=\"display:block;max-width:15em;padding-top:1px;\"><br>"
|
|
"<div align=\"center\" class=\"infh\">"
|
|
"<b>Official Message Received</b></div>" + message + "</td></tr>";
|
|
}
|
|
|
|
body += "<div style=\"padding:0 1em;\"><br></div>"
|
|
"<table width=\"100%\" align=\"center\"><tr valign=\"top\">";
|
|
|
|
const utf8 movedUrl = gOptions.stream_movedUrl(sid);
|
|
if (movedUrl.empty())
|
|
{
|
|
body += "<td><table class=\"en\" cellpadding=\"15px\" "
|
|
"style=\"border:0;margin-left:1em;\"><tr>"
|
|
"<td valign=\"top\"><div align=\"center\" "
|
|
"class=\"infh\"><b>Stream Details</b></div>" +
|
|
detailsBody + "</td></tr></table></td>";
|
|
}
|
|
|
|
const int maxUsers = ((info.m_streamMaxUser > 0) && (info.m_streamMaxUser < gOptions.maxUser()) ? info.m_streamMaxUser : gOptions.maxUser());
|
|
if (extra.isConnected)
|
|
{
|
|
const bool isListable = streamData::isAllowedType(info.m_uvoxDataType);
|
|
utf8 listenLink = "<a href=\"listen.pls?sid=" + tos(sid) + "\"><img border=\"0\" title=\"Listen to Stream\" "
|
|
"alt=\"Listen to Stream\" style=\"vertical-align:middle\" src=\"images/listen.png\"></a>" +
|
|
(sd && !sd->radionomyID().empty() && sd->streamAdvertMode() ?
|
|
"<img border=\"0\" title=\"Active DNAS+ Stream\nMonetisation Enabled\" "
|
|
"alt=\"Active DNAS+ Stream\nMonetisation Enabled\" style=\"vertical-align:middle\" "
|
|
"src=\"images/adavail.png\"> " : (utf8)"");
|
|
|
|
body += "<td><table cellspacing=\"0\" cellpadding=\"2\" border=\"0\" style=\"padding-left:1em;\">"
|
|
"<tr valign=\"top\"><td colspan=\"2\">" + getNewVersionMessage() + "<td></tr>"
|
|
"<tr valign=\"top\"><td>Listing Status: </td><td><b>Stream is currently up " +
|
|
(info.m_streamPublic && isListable ? (extra.ypConnected != 1 ? "" : utf8("and public") + listenLink) : utf8("and private (not listed)") + listenLink) +
|
|
(info.m_streamPublic && isListable ? (extra.ypConnected != 1 ? (!yp2::isValidAuthhash(info.m_authHash) ?
|
|
string(info.m_authHash.empty() ? "but requires <a href=\"admin.cgi?sid=" + tos(sid) +
|
|
"&mode=register\">registration</a> in the Shoutcast Directory.<br>" :
|
|
"but not listed due to an invalid authhash.<br>") +
|
|
|
|
(info.m_authHash.empty() ? "Listeners are allowed and the stream will act like it is private until resolved."
|
|
"<br><br>To create an authhash you will need to <a href=\"admin.cgi?sid=" + tos(sid) +
|
|
"&mode=register\">register</a> the stream with us.<br>If you already have an existing authhash "
|
|
"then you can enter it <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">here</a>.<br><br>" :
|
|
"Listeners are allowed and the stream will act like it is private until resolved.") :
|
|
(extra.ypErrorCode == 200 ? "waiting on a Directory response." :
|
|
(extra.ypErrorCode == YP_COMMS_FAILURE ? "unable to access the Directory.<br>Listeners are allowed and the stream will act like it is private until resolved." :
|
|
(extra.ypErrorCode == YP_MAINTENANCE_CODE ? "received a Directory maintenance notification: <a target=\"_blank\" href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_DNAS_Server_2#YP_Server_Errors\">" + tos(extra.ypErrorCode) +
|
|
"</a><br>Listeners will be allowed though the stream will not be listed in the Directory." :
|
|
"received Directory error code: <a target=\"_blank\" href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_DNAS_Server_2#YP_Server_Errors\">" + tos(extra.ypErrorCode) + "</a><br>" +
|
|
(extra.ypConnected != 2 ? "" :
|
|
"during a listing update. The stream may no longer appear.") +
|
|
"<br>Check the server log and / or contact the server administrator.")))) : "") : "") + "</b></td></tr>"
|
|
"<tr valign=\"top\"><td>Stream Status: </td>"
|
|
"<td><b>Stream is up (" + streamData::getContentType(info) + " @ " +
|
|
(info.m_streamBitrate > 0 ? tos(info.m_streamBitrate) : "unknown") +
|
|
" kbps" + (info.m_vbr ? " (VBR)" : "") + ", " +
|
|
sampleRateStr(info.m_streamSampleRate) + ") with " +
|
|
tos(data.connectedListeners) + (maxUsers > 0 ? " of " +
|
|
tos(maxUsers) : "") + " listeners" + (!maxUsers ? " (unlimited)" : "") +
|
|
(data.connectedListeners != data.uniqueListeners ? (" (" +
|
|
tos(data.uniqueListeners) + " unique)") : "") + "</b></td></tr>";
|
|
|
|
if (data.peakListeners > 0)
|
|
{
|
|
body += "<tr valign=\"top\"><td>Listener Peak: </td><td><b>" +
|
|
tos(data.peakListeners) + "</b></td></tr>";
|
|
}
|
|
|
|
const utf8 avgTime = timeString(data.avgUserListenTime);
|
|
if (!avgTime.empty())
|
|
{
|
|
body += "<tr valign=\"top\"><td>Avg. Play Time: </td>"
|
|
"<td><b>" + avgTime + "</b></td></tr>";
|
|
}
|
|
|
|
body += "<tr valign=\"top\"><td>Stream Name: </td><td><b>" +
|
|
(info.m_streamPublic && extra.ypConnected ? "<a target=\"_blank\" href=\"http://directory.shoutcast.com/Search?query=" +
|
|
urlUtils::escapeURI_RFC3986(info.m_streamName) + "\">" + aolxml::escapeXML(info.m_streamName) + "</a>" :
|
|
aolxml::escapeXML(info.m_streamName)) + "</b></td></tr>" +
|
|
|
|
(info.m_streamPublic && extra.ypConnected ? "<tr valign=\"top\"><td alt=\"Shoutcast Directory ID\" "
|
|
"title=\"Shoutcast Directory ID\"><img border=\"0\" "
|
|
"src=\"images/favicon.ico\" style=\"vertical-align:bottom\">"
|
|
" ID: </td><td><b><a title=\"Shoutcast Directory ID\" href=\"http://" +
|
|
|
|
gOptions.ypAddr().hideAsString() + ":" + tos(gOptions.ypPort()) + ((gOptions.ypPath() != utf8("/yp2")) ? "/yp" : "") +
|
|
|
|
"/sbin/tunein-station.pls?id="+info.m_stationID+"\">"+info.m_stationID+"</a></b></td></tr>" : "");
|
|
|
|
if (!info.m_streamGenre[0].empty())
|
|
{
|
|
body += "<tr valign=\"top\"><td>Stream Genre(s): </td>"
|
|
"<td><b>" + (info.m_streamPublic && extra.ypConnected ?
|
|
"<a target=\"_blank\" href=\"http://directory.shoutcast.com/Genre?name=" +
|
|
urlUtils::escapeURI_RFC3986(info.m_streamGenre[0]) + "\">" +
|
|
aolxml::escapeXML(info.m_streamGenre[0]) + "</a>" :
|
|
aolxml::escapeXML(info.m_streamGenre[0])) + "</b>";
|
|
|
|
for (int i = 1; i < 5; i++)
|
|
{
|
|
if (!info.m_streamGenre[i].empty())
|
|
{
|
|
body += " , <b>" + (info.m_streamPublic && extra.ypConnected ? "<a target=\"_blank\" href=\"http://directory.shoutcast.com/Genre?name=" +
|
|
urlUtils::escapeURI_RFC3986(info.m_streamGenre[i]) + "\">" + aolxml::escapeXML(info.m_streamGenre[i]) + "</a>" :
|
|
aolxml::escapeXML(info.m_streamGenre[i])) + "</b>";
|
|
}
|
|
}
|
|
|
|
body += "</td></tr>";
|
|
}
|
|
|
|
if (!info.m_streamUser.empty())
|
|
{
|
|
body += "<tr valign=\"top\"><td>Stream DJ: </td>"
|
|
"<td><b>" + aolxml::escapeXML(info.m_streamUser) + "</b></td></tr>";
|
|
}
|
|
|
|
if (!info.m_streamURL.empty())
|
|
{
|
|
body += "<tr valign=\"top\"><td>Stream Website: </td>"
|
|
"<td><b>" + urlLink(info.m_streamURL) + "</b></td></tr>";
|
|
}
|
|
|
|
if (!info.m_currentSong.empty())
|
|
{
|
|
body += "<tr valign=\"top\"><td>Playing Now: </td>"
|
|
"<td><b><a href=\"currentsong?sid=" + tos(sid) + "\">" +
|
|
getCurrentSong(info.m_currentSong) + "</a></b></td></tr>";
|
|
|
|
// only show if we have a valid current song
|
|
if (!info.m_comingSoon.empty())
|
|
{
|
|
body += "<tr valign=\"top\"><td>Playing Next: </td>"
|
|
"<td><b><a href=\"nextsong?sid=" + tos(sid) + "\">" +
|
|
aolxml::escapeXML(info.m_comingSoon) + "</a></b></td></tr>";
|
|
}
|
|
}
|
|
|
|
// strip down the source address for display output to an appropriate output based on settings
|
|
utf8 srcAddr = niceURL(extra.isBackup ? info.m_backupURL : (extra.isRelay ? info.m_relayURL : info.m_srcAddr));
|
|
if (gOptions.nameLookups())
|
|
{
|
|
if (!extra.isBackup && !extra.isRelay)
|
|
{
|
|
u_short port = 0;
|
|
string addr, hostName;
|
|
socketOps::getpeername(m_socket, addr, port);
|
|
|
|
string src = (extra.isBackup ? info.m_backupURL : (extra.isRelay ? info.m_relayURL : info.m_srcAddr)).hideAsString();
|
|
hostName = src;
|
|
if (!socketOps::addressToHostName(addr,port,hostName))
|
|
{
|
|
srcAddr = hostName + " (" + niceURL(src) + ")";
|
|
}
|
|
}
|
|
}
|
|
|
|
body += "<tr valign=\"top\"><td>Stream Source: </td>"
|
|
"<td><b>" + (extra.isRelay || extra.isBackup ? urlLink(srcAddr) : srcAddr) + " " +
|
|
(extra.isRelay ? "(relaying" + (extra.isBackup ? utf8(" backup") : "") + ") " : (extra.isBackup ? "(backup) " : "")) + "</b>"
|
|
"[ <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=kicksrc\">" + (extra.isRelay || extra.isBackup ? "stop" : "kick") +
|
|
"</a> ]</td></tr><tr valign=\"top\"><td>Stream Uptime: </td>"
|
|
"<td id=\"up2\"><b>" + timeString((streamUptime = ::time(NULL) - streamData::getStreamUptime(sid))) + "</b></td></tr>";
|
|
|
|
if (!info.m_contentType.empty() && (info.m_uvoxDataType == MP3_DATA))
|
|
{
|
|
body += streamData::getHTML5Player(sid);
|
|
}
|
|
|
|
body += "</table>";
|
|
}
|
|
else
|
|
{
|
|
body += "<td" + (movedUrl.empty() ? (utf8)"" : " align=\"center\"") + ">"
|
|
"<table cellspacing=\"0\" cellpadding=\"2\" border=\"0\">"
|
|
"<tr valign=\"top\"><td colspan=\"2\">" + getNewVersionMessage("<br>") + "<td></tr>"
|
|
"<tr valign=\"top\"><td>Stream Status: </td><td><b>";
|
|
|
|
if (movedUrl.empty())
|
|
{
|
|
body += "Stream is currently down" + (data.connectedListeners > 0 ?
|
|
" with " + tos(data.connectedListeners) + (maxUsers > 0 ? " of " +
|
|
tos(maxUsers) : "") + " listeners" + (!maxUsers ? " (unlimited)" : "") +
|
|
(data.connectedListeners != data.uniqueListeners ? (" (" +
|
|
tos(data.uniqueListeners) + " unique)") : "") : ".") + "<br>There is no "
|
|
"source connected or no stream is configured for stream #" + tos(sid) + ".";
|
|
}
|
|
else
|
|
{
|
|
body += "Stream has been moved to " + urlLink(movedUrl) + "<br>No source connections will be allowed for this stream.";
|
|
}
|
|
body += "</b></td></tr>";
|
|
|
|
if (data.peakListeners > 0)
|
|
{
|
|
body += "<tr valign=\"top\"><td>Listener Peak: </td><td><b>" +
|
|
tos(data.peakListeners) + "</b></td></tr>";
|
|
}
|
|
|
|
utf8 avgTime = timeString(data.avgUserListenTime);
|
|
if (!avgTime.empty())
|
|
{
|
|
body += "<tr valign=\"top\"><td>Avg. Play Time: </td>"
|
|
"<td><b>" + avgTime + "</b></td></tr>";
|
|
}
|
|
|
|
// add in an option to restart a relay url...
|
|
if (!gOptions.stream_relayURL(sid).empty() && movedUrl.empty())
|
|
{
|
|
// strip down the source address for display output
|
|
utf8 srcAddr = niceURL(gOptions.stream_relayURL(sid));
|
|
|
|
// make sure we're not exposing the option to try re-connecting to a pending source relay
|
|
bool noEntry = false;
|
|
if (!(streamData::isRelayActive(sid, noEntry) == 1))
|
|
{
|
|
body += "<tr><td><br>Start Relay:</td><td><br><b>" + urlLink(srcAddr) + "</b> "
|
|
"[ <a href=\"admin.cgi?sid="+tos(sid)+"&mode=startrelay\">start relay</a> ]</td></tr>";
|
|
}
|
|
else
|
|
{
|
|
body += "<tr><td><br>Starting Relay:</td><td><br><b>Connection pending to " +
|
|
urlLink(srcAddr) + "</b> [ <a href=\"admin.cgi?sid=" + tos(sid) +
|
|
"&mode=kicksrc\">abort</a> ]</td></tr>";
|
|
}
|
|
}
|
|
|
|
body += "</table>";
|
|
}
|
|
|
|
body += "</td></tr></table>";
|
|
}
|
|
|
|
// for a refresh, we'll show a countdown so it's obvious that something is happening
|
|
if (refreshRequired)
|
|
{
|
|
body += "<script type=\"text/javascript\">"
|
|
"function $(id){return document.getElementById(id);}" EL
|
|
"var c = " + tos(abs(refreshRequired)) + ";" EL
|
|
"function countDown(){" EL
|
|
"c--;" EL
|
|
"if(c > 0){" EL
|
|
"$('counter').innerHTML = \"<br>Waiting \"+c+\" second\" + (c > 1 ? \"s\" : \"\") + \" for any configuration changes to take effect.<br>"
|
|
"If not automatically redirected or do not want to wait, <a href=\\\"admin.cgi?sid="+tos(sid)+"\\\">click here.</a><br><br>\";" EL
|
|
"}" EL
|
|
"}" EL
|
|
"setInterval(countDown,1000);" EL
|
|
"</script>";
|
|
}
|
|
|
|
body += getUptimeScript(false, isConnected, streamUptime) + getIEFlexFix() +
|
|
getHTML5Remover() + (!refreshRequired && hasListeners ?
|
|
getStreamListeners(sid, mapGet(m_httpRequestInfo.m_QueryParameters, "nw", (bool)gOptions.adminNoWrap()),
|
|
mapGet(m_httpRequestInfo.m_QueryParameters, "fh", (int)0)) : "") + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
utf8 getCDNMessage(const bool master, const bool slave)
|
|
{
|
|
if (slave && !master)
|
|
{
|
|
return "Configured as a CDN slave<br>Authhash inheritance enabled from master";
|
|
}
|
|
else if (master && !slave)
|
|
{
|
|
return "Configured as a CDN master<br>Authhash inheritance enabled for slaves";
|
|
}
|
|
return "Configured as a CDN intermediary<br>Authhash inheritance enabled both ways";
|
|
}
|
|
|
|
utf8 getBadAuthhashMessage(const streamData::streamID_t sid, const utf8 &authHash)
|
|
{
|
|
return "<div class=\"en\" style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">"
|
|
"<b>Invalid Authhash Detected</b> <input class=\"submit\" type=\"button\" "
|
|
"onclick=\"alert('An incorrect authhash is entered for this stream: " + authHash + "\\n\\n"
|
|
"Use the [ Clear ] option and re-enter a valid authhash or\\nregister your stream to be "
|
|
"listed in the Shoutcast directory.\\n\\nIf you think this is a valid authhash then please "
|
|
"contact\\nsupport including the authhash at support@shoutcast.com.')\" value=\"?\"><br><br>"
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register&register=clear\">Clear Authhash</a>"
|
|
" | <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Manage Authhash</a></div>";
|
|
}
|
|
|
|
void protocol_admincgi::mode_summary(const int refreshRequired) throw()
|
|
{
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
streams = "",
|
|
body = getServerAdminHeader("Server Summary", refreshRequired);
|
|
|
|
size_t totalListeners = 0,
|
|
totalPeakListeners = 0,
|
|
streamTotal = 0,
|
|
movedTotal = 0;
|
|
map<size_t,uniString::utf8> streamBlocks;
|
|
|
|
if (refreshRequired == 0)
|
|
{
|
|
size_t inc = 0, sid = DEFAULT_SOURCE_STREAM;
|
|
do
|
|
{
|
|
utf8 streamBody = "";
|
|
sid = streamData::enumStreams(inc);
|
|
|
|
// check if we have an active source and valid sid before attempting to add
|
|
if (sid >= DEFAULT_SOURCE_STREAM)
|
|
{
|
|
streamData::streamInfo info;
|
|
streamData::extraInfo extra;
|
|
if (streamData::getStreamInfo(sid, info, extra))
|
|
{
|
|
stats::statsData_t data;
|
|
stats::getStats(sid, data);
|
|
|
|
// increment our stream total now that we know we have one
|
|
totalListeners += data.connectedListeners;
|
|
totalPeakListeners += data.peakListeners;
|
|
|
|
utf8 streamBody2 = "<tr><td align=\"center\">";
|
|
const bool slave = isCDNSlave(sid);
|
|
const bool master = isCDNMaster(sid);
|
|
if (master || slave)
|
|
{
|
|
streamBody2 += "<b><div class=\"en\" style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">" +
|
|
getCDNMessage(master, slave) + "</div></b><br><br>";
|
|
}
|
|
|
|
const bool isListable = streamData::isAllowedType(info.m_uvoxDataType);
|
|
if (!isListable)
|
|
{
|
|
streamBody2 += "<b><div class=\"en\" style=\"padding:1em;margin-bottom:1em;display:inline-block;border-radius:0.5em;\">" +
|
|
(info.m_uvoxDataType == OGG_DATA ? "OGG Vorbis based streams are not fully supported<br>and will not" :
|
|
utf8("NSV based streams are no longer able<br>to ")) +
|
|
" be listed in the Shoutcast Directory.</div></b><br>";
|
|
}
|
|
/*else if (!info.m_streamPublic && gOptions.cdn().empty() && !slave && !master)
|
|
{
|
|
streamBody2 += "<b><div class=\"en\" style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">"
|
|
"An authhash is not required for private streams.</div></b><br><br>";
|
|
}*/
|
|
|
|
if (isListable)
|
|
{
|
|
if (info.m_authHash.empty())
|
|
{
|
|
streamBody2 += "<div class=\"en\" style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">"
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Create Authhash</a>"
|
|
" | <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Manage Authhash</a></div>";
|
|
}
|
|
else
|
|
{
|
|
// check that the authhash is a valid length
|
|
if (!yp2::isValidAuthhash(info.m_authHash))
|
|
{
|
|
streamBody2 += getBadAuthhashMessage(sid, info.m_authHash);
|
|
}
|
|
else
|
|
{
|
|
streamBody2 += "<div class=\"en\" style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">"
|
|
+ utf8((master || slave || info.m_streamPublic) && (info.m_streamSampleRate > 0) &&
|
|
info.m_radionomyID.empty() && ((extra.ypErrorCode != YP_NOT_VISIBLE) &&
|
|
(extra.ypErrorCode != YP_AUTH_ISSUE_CODE) && (extra.ypErrorCode != -1)) ?
|
|
warningImage(false) + " <b>Please Register Your Authhash</b><br><br>" : "") +
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Update Authhash</a> | "
|
|
" <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Manage Authhash</a></div>";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
streamBody2 += "<div class=\"en\" style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">"
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Manage Authhash</a></div>";
|
|
}
|
|
streamBody2 += "</td></tr>";
|
|
|
|
streamData *sd = streamData::accessStream(sid);
|
|
|
|
streamBody += "<tr><td align=\"center\" class=\"tnl\">" +
|
|
(sd && !sd->radionomyID().empty() && sd->streamAdvertMode() ?
|
|
"<img border=\"0\" title=\"Active DNAS+ Stream\nMonetisation Enabled\" "
|
|
"alt=\"Active DNAS+ Stream\nMonetisation Enabled\" style=\"vertical-align:middle\" "
|
|
"src=\"images/adavail.png\"> " : (utf8)"") +
|
|
"<a href=\"index.html?sid=" + tos(sid) + "\">Stream #" + tos(sid) + "</a> "
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "\">(Stream Login)</a>" +
|
|
" <a href=\"listen.pls?sid=" + tos(sid) + "\"><img border=\"0\" title=\"Listen to Stream\" "
|
|
"alt=\"Listen to Stream\" style=\"vertical-align:middle\" src=\"images/listen.png\"></a>" +
|
|
(sd && !sd->streamAlbumArt().empty() ? " <a href=\"/streamart?sid=" + tos(sid) + "\">"
|
|
"<img border=\"0\" title=\"View Stream Artwork\" alt=\"View Stream Artwork\" "
|
|
"style=\"vertical-align:middle;padding-left:0.5em;\" src=\"images/streamart.png\"></a>" : "") +
|
|
(sd && !sd->streamPlayingAlbumArt().empty() ? " <a href=\"/playingart?sid=" + tos(sid) + "\">"
|
|
"<img border=\"0\" title=\"View Playing Artwork\" alt=\"View Playing Artwork\" "
|
|
"style=\"vertical-align:middle;padding-left:0.5em;\" src=\"images/playingart.png\"></a>" : "") +
|
|
|
|
"</td></tr>" +
|
|
(!info.m_contentType.empty() && (info.m_uvoxDataType == MP3_DATA) ?
|
|
streamData::getHTML5Player(sid) : "") + streamBody2;
|
|
|
|
utf8 content = streamData::getContentType(info);
|
|
if (!info.m_streamUser.empty())
|
|
{
|
|
content = info.m_streamUser + " - " + content;
|
|
}
|
|
|
|
const int maxUsers = ((info.m_streamMaxUser > 0) && (info.m_streamMaxUser < gOptions.maxUser()) ? info.m_streamMaxUser : gOptions.maxUser());
|
|
const utf8 listeners = (data.connectedListeners ? (tos(data.connectedListeners) +
|
|
(data.connectedListeners != data.uniqueListeners ?
|
|
(" (" + tos(data.uniqueListeners) + " unique)") : "")) : "0") +
|
|
(maxUsers > 0 ? " of " + tos(maxUsers) : " (unlimited)");
|
|
|
|
const utf8 listenLink = "<a href=\"listen.pls?sid=" + tos(sid) + "\"><img border=\"0\" title=\"Listen to Stream\" "
|
|
"alt=\"Listen to Stream\" style=\"vertical-align:middle\" src=\"images/listen.png\"></a>";
|
|
|
|
streamBody += "<tr><td align=\"center\">" +
|
|
(info.m_streamPublic && extra.ypConnected ? "<a target=\"_blank\" href=\"http://directory.shoutcast.com/Search?query=" +
|
|
urlUtils::escapeURI_RFC3986(info.m_streamName) + "\">" + aolxml::escapeXML(info.m_streamName) + "</a>" :
|
|
aolxml::escapeXML(info.m_streamName)) + " (" + content + " @ " +
|
|
(info.m_streamBitrate > 0 ? tos(info.m_streamBitrate) : "unknown") +
|
|
" kbps" + (info.m_vbr ? " (VBR)" : "") + ", " +
|
|
sampleRateStr(info.m_streamSampleRate) + ")</td></tr>" +
|
|
|
|
(!info.m_currentSong.empty() ? "<tr><td align=\"center\" style=\"padding-bottom:0;\">Playing: <b>"
|
|
"<a href=\"currentsong?sid=" + tos(sid) + "\">" +
|
|
aolxml::escapeXML(info.m_currentSong) + "</a></b></td></tr>" +
|
|
(!info.m_comingSoon.empty() ? "<tr><td align=\"center\" style=\"padding-top:0;\">Coming: <b>"
|
|
"<a href=\"currentsong?sid=" + tos(sid) + "\">" +
|
|
aolxml::escapeXML(info.m_comingSoon) + "</a></b></td></tr>" : "") : "") +
|
|
|
|
"<tr><td><table align=\"center\"><tr valign=\"top\"><td align=\"center\">"
|
|
"<div style=\"text-align:left;\">Listeners: <b>" + listeners + "</b>" +
|
|
(data.peakListeners > 0 ? "<br>Peak: <b>" + tos(data.peakListeners) + "</b>" : "") +
|
|
(data.connectedListeners > 0 ? " [ <a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=kickdst&kickdst=all\">kick all</a> ]" : "") +
|
|
"</div></td><td> </td><td align=\"center\">Status: <b>" +
|
|
string(info.m_streamPublic && isListable ? (extra.ypConnected != 1 ? "" : string("Public</b><br>"
|
|
|
|
"<div title=\"Shoutcast Directory ID\" alt=\"Shoutcast Directory ID\">"
|
|
"<img border=\"0\" title=\"Shoutcast Directory ID\" alt=\"Shoutcast Directory ID\" style=\"vertical-align:bottom\" "
|
|
"src=\"images/favicon.ico\"> ID: <b><a title=\"Shoutcast Directory ID\" href=\"http://" +
|
|
gOptions.ypAddr().hideAsString() + ":" + tos(gOptions.ypPort()) + ((gOptions.ypPath() != utf8("/yp2")) ? "/yp" : "") +
|
|
"/sbin/tunein-station.pls?id=" + info.m_stationID.hideAsString() + "\">" + info.m_stationID.hideAsString() + "</a></b></div>")) +
|
|
|
|
(extra.ypConnected != 1 ? (!yp2::isValidAuthhash(info.m_authHash) ?
|
|
" Not Listed - " + string(info.m_authHash.empty() ? "Empty" : "Invalid") + " Authhash" :
|
|
(extra.ypErrorCode == 200 ? " Waiting on a Directory response" :
|
|
(extra.ypErrorCode == -1 ? "Unable to access the Directory.<br>Check the server log for more details.<br>The stream will behave like it is private." :
|
|
(extra.ypErrorCode == YP_MAINTENANCE_CODE ? "Directory is down for maintenance: <a target=\"_blank\" href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_DNAS_Server_2#YP_Server_Errors\">" +
|
|
tos(extra.ypErrorCode) + "</a><br>Listeners are allowed, stream will not be listed" :
|
|
(extra.ypErrorCode == YP_AUTH_ISSUE_CODE ? " Please contact support as there is an issue with the authhash" : " Directory returned error code: <a target=\"_blank\" href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_DNAS_Server_2#YP_Server_Errors\">" + tos(extra.ypErrorCode) + "</a><br>" +
|
|
(extra.ypConnected != 2 ? "" : "during a listing update. The stream may not<br>appear in the Directory due to the error. The<br> server will attempt to re-list the stream.")))))) : "") :
|
|
"Private") + "</td><td> </td>"
|
|
"<td title=\"Source User Agent: " + addWBR((!info.m_sourceIdent.empty() ?
|
|
info.m_sourceIdent : "Legacy / Unknown")) + "\">Source: <b>" +
|
|
utf8(info.m_sourceType == streamData::SHOUTCAST1 ? "v1" :
|
|
(info.m_sourceType == streamData::SHOUTCAST2 ? "v2" : "HTTP")) +
|
|
(extra.isRelay ? " relay" + (extra.isBackup ? utf8(" backup") : "") :
|
|
(extra.isBackup ? " backup" : "")) + "</b> [ <a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=kicksrc\">" + (extra.isRelay || extra.isBackup ? "stop" : "kick") +
|
|
"</a> ]</td></tr></table></td></tr>"
|
|
|
|
"<tr><td align=\"center\">"
|
|
"<div style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\" class=\"en\">"
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=viewxml\">Summary</a> | "
|
|
|
|
"<div style=\"display:inline-block;\"><a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=viewxml&page=3\">Listeners</a> [ <a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=viewxml&page=3&ipcount=1\">Counts</a> ]</div> | "
|
|
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=viewxml&page=4\">History</a> | "
|
|
"<a href=\"currentmetadata?sid=" + tos(sid) + "\">Metadata</a> | "
|
|
"<a href=\"stats?sid=" + tos(sid) + "\">Statistics</a> "
|
|
"<a href=\"7?sid=" + tos(sid) + "\">…</a></div></td></tr>";
|
|
|
|
if (sd)
|
|
{
|
|
sd->releaseStream();
|
|
}
|
|
|
|
const utf8& message = streamData::getStreamMessage(sid);
|
|
if (!message.empty())
|
|
{
|
|
streamBody += "<tr><td align=\"center\" width=\"100%\"><table cellspacing=\"0\" cellpadding=\"15px;\" class=\"ent\" width=\"85%\">"
|
|
"<tr><td valign=\"top\" align=\"center\" style=\"border:1px;display:block;\"><br>"
|
|
"<div align=\"center\" class=\"infh\" style=\"margin-left:-15px;margin-right:-15px;margin-top:-14px;\">"
|
|
"<b>Official Message Received</b></div>" + message + "</td></tr></table></td></tr>";
|
|
}
|
|
|
|
streamBlocks[sid] = streamBody;
|
|
}
|
|
}
|
|
++inc;
|
|
}
|
|
while (sid);
|
|
|
|
// now we check through for any known but inactive relays and then get them listed as well
|
|
vector<config::streamConfig> relayList(gOptions.getRelayList());
|
|
if (!relayList.empty())
|
|
{
|
|
for (vector<config::streamConfig>::const_iterator i = relayList.begin(); i != relayList.end(); ++i)
|
|
{
|
|
sid = (*i).m_streamID;
|
|
const bool exists = !(*i).m_relayUrl.url().empty();
|
|
if (exists && !streamData::isSourceConnected(sid))
|
|
{
|
|
stats::statsData_t data;
|
|
stats::getStats(sid, data);
|
|
|
|
// increment our stream total now that we know we have one
|
|
totalListeners += data.connectedListeners;
|
|
totalPeakListeners += data.peakListeners;
|
|
|
|
streamData::streamInfo info;
|
|
streamData::extraInfo extra;
|
|
streamData::getStreamInfo(sid, info, extra);
|
|
utf8 listeners, content, streamBody2,
|
|
streamBody = "<tr align=\"center\" class=\"tnl\"><td>"
|
|
"<a href=\"index.html?sid=" + tos(sid) + "\">Stream #" + tos(sid) + "</a> "
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "\">(Stream Login)</a></td></tr>";
|
|
|
|
const utf8 movedUrl = gOptions.stream_movedUrl(sid);
|
|
if (movedUrl.empty())
|
|
{
|
|
const bool slave = isCDNSlave(sid);
|
|
const bool master = isCDNMaster(sid);
|
|
if (master || slave)
|
|
{
|
|
streamBody2 = "<div class=\"en\" style=\"padding:1emx;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">" +
|
|
getCDNMessage(master, slave) + "</div><br><br>";
|
|
}
|
|
else
|
|
{
|
|
if ((*i).m_authHash.empty())
|
|
{
|
|
streamBody2 += "<div class=\"en\" style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">"
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Create Authhash</a>"
|
|
" | <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Manage Authhash</a></div>";
|
|
}
|
|
else
|
|
{
|
|
// check that the authhash is a valid length
|
|
if (!yp2::isValidAuthhash((*i).m_authHash))
|
|
{
|
|
streamBody2 += getBadAuthhashMessage(sid, info.m_authHash);
|
|
}
|
|
else
|
|
{
|
|
streamBody2 += "<div class=\"en\" style=\"padding:1em;margin-bottom:0.5em;display:inline-block;border-radius:0.5em;\">"
|
|
+ utf8((master || slave || info.m_streamPublic) && (info.m_streamSampleRate > 0) &&
|
|
info.m_radionomyID.empty() && ((extra.ypErrorCode != YP_NOT_VISIBLE) &&
|
|
(extra.ypErrorCode != YP_AUTH_ISSUE_CODE) && (extra.ypErrorCode != -1)) ?
|
|
warningImage(false) + " <b>Please Register Your Authhash</b><br><br>" : "") +
|
|
"<a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Update Authhash</a> | "
|
|
" <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=register\">Manage Authhash</a></div>";
|
|
}
|
|
}
|
|
|
|
if (!streamBody2.empty())
|
|
{
|
|
streamBody2 += "<br>";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
streamBody2 = "<b>This stream is configured as having been moved or retired.<br>"
|
|
"No source connections will be allowed for this stream.<br><br>"
|
|
"All client connections received will be redirected to:<br>" +
|
|
urlLink(movedUrl) + "</b>";
|
|
++movedTotal;
|
|
}
|
|
|
|
if (!streamBody2.empty())
|
|
{
|
|
streamBody += "<tr><td align=\"center\">" + streamBody2 + "</td></tr>";
|
|
}
|
|
|
|
// strip down the source address for display output
|
|
const utf8 srcAddr = niceURL(gOptions.stream_relayURL(sid));
|
|
if (movedUrl.empty())
|
|
{
|
|
bool noEntry = false;
|
|
const bool isListable = streamData::isAllowedType(info.m_uvoxDataType);
|
|
streamBody += "<tr><td><table align=\"center\"><tr valign=\"top\">"
|
|
"<td" + (!extra.isConnected ? " rowspan=\"2\"" : (utf8)"") +
|
|
" align=\"right\">Status: <b>" + string(info.m_streamPublic && isListable ? (extra.ypConnected != 1 ? "" : string("Public")) +
|
|
(extra.ypConnected != 1 ? (!yp2::isValidAuthhash(info.m_authHash) ?
|
|
" Not Listed - " + string(info.m_authHash.empty() ? "Empty" : "Invalid") + " Authhash" :
|
|
(extra.ypErrorCode == 200 ? " Waiting on a Directory response" :
|
|
(extra.ypErrorCode == -1 ? "Unable to access the Directory.<br>Check the error server for more details.<br>The stream will behave like it is private." :
|
|
(extra.ypErrorCode == YP_MAINTENANCE_CODE ? "Directory is down for maintenance: <a target=\"_blank\" "
|
|
"href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_DNAS_Server_2#YP_Server_Errors\">" +
|
|
tos(extra.ypErrorCode) + "</a><br>Listeners are allowed, stream will not be listed" :
|
|
(extra.ypErrorCode == YP_AUTH_ISSUE_CODE ? " Please contact support as there is an issue with the authhash" : " Directory returned "
|
|
"error code: <a target=\"_blank\" href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_DNAS_Server_2#YP_Server_Errors\">" + tos(extra.ypErrorCode) + "</a><br>" +
|
|
(extra.ypConnected != 2 ? "" : "during a listing update. The stream may not<br>appear in the Directory due to the error. The<br> server will attempt to re-list the stream.")))))) : "") :
|
|
"Private") + "</b></td><td" + (!extra.isConnected ? " rowspan=\"2\"" : (utf8)"") + "> </td>"
|
|
"<td title=\"Source User Agent: " + addWBR((!info.m_sourceIdent.empty() ? info.m_sourceIdent : "Legacy / Unknown")) + "\">"
|
|
"Source: <b>" + (!(streamData::isRelayActive(sid, noEntry) == 1) ?
|
|
"Inactive relay</b> [ <a href=\"admin.cgi?sid=" + tos(sid) +
|
|
"&mode=startrelay\">start relay</a> ]<br>Using: <b>" + urlLink(srcAddr) + "</b>" :
|
|
"Connection pending to " + urlLink(srcAddr) + "</b>"
|
|
" [ <a href=\"admin.cgi?sid=" + tos(sid) + "&mode=kicksrc\">abort</a> ]") +
|
|
|
|
// if it's an inactive stream then we also want to show any remaining connections
|
|
"</td></tr>" + (data.connectedListeners > 0 ? "<tr valign=\"top\">"
|
|
"<td align=\"center\"><div style=\"text-align:left;padding-top:0.5em;\">Listeners: <b>" +
|
|
tos(data.connectedListeners) + "</b>" + (data.peakListeners > 0 ? " | Peak: <b>" +
|
|
tos(data.peakListeners) + "</b>" : "") + " [ <a href=\"admin.cgi?sid=" + tos(sid) +
|
|
"&mode=kickdst&kickdst=all\">kick all</a> ]" + "</div></td></tr>" : "") +
|
|
"</table></td></tr>";
|
|
}
|
|
streamBlocks[sid] = streamBody;
|
|
}
|
|
}
|
|
}
|
|
|
|
// now we check through for any known but in-active sources & then get them listed as well
|
|
// new to build 70 but for fire builds makes it easier to see if a feed stream is inactive
|
|
config::streams_t stream_configs;
|
|
gOptions.getStreamConfigs(stream_configs);
|
|
if (!stream_configs.empty())
|
|
{
|
|
for (config::streams_t::const_iterator i = stream_configs.begin(); i != stream_configs.end(); ++i)
|
|
{
|
|
if (streamBlocks.find((*i).first) == streamBlocks.end())
|
|
{
|
|
stats::statsData_t data;
|
|
stats::getStats((*i).first, data);
|
|
|
|
// increment our stream total now that we know we have one
|
|
totalListeners += data.connectedListeners;
|
|
totalPeakListeners += data.peakListeners;
|
|
|
|
utf8 streamBody = "<tr><td align=\"center\" class=\"tnl\">"
|
|
"<a href=\"index.html?sid=" + tos((*i).first) + "\">Stream #" + tos((*i).first) + "</a> "
|
|
"<a href=\"admin.cgi?sid=" + tos((*i).first) + "\">(Stream Login)</a></td></tr>",
|
|
streamBody2;
|
|
|
|
const utf8 movedUrl = gOptions.stream_movedUrl((*i).first);
|
|
if (movedUrl.empty())
|
|
{
|
|
utf8 authhash = (*i).second.m_authHash;
|
|
if (authhash.empty())
|
|
{
|
|
authhash = gOptions.stream_authHash((*i).first);
|
|
}
|
|
|
|
if (authhash.empty())
|
|
{
|
|
streamBody2 += "<div class=\"en\" style=\"padding:1em;margin-bottom:1em;display:inline-block;border-radius:0.5em;\">"
|
|
"<a href=\"admin.cgi?sid=" + tos((*i).first) + "&mode=register\">Create Authhash</a>"
|
|
" | <a href=\"admin.cgi?sid=" + tos((*i).first) + "&mode=register\">Manage Authhash</a></div>";
|
|
}
|
|
else
|
|
{
|
|
// check that the authhash is a valid length
|
|
if (!yp2::isValidAuthhash(authhash))
|
|
{
|
|
streamBody2 += getBadAuthhashMessage((*i).first, authhash);
|
|
}
|
|
else
|
|
{
|
|
streamBody2 += "<div class=\"en\" style=\"padding:1em;margin-bottom:1em;display:inline-block;border-radius:0.5em;\">"
|
|
// TODO if the stream is not active then we don't know the admode
|
|
// status and so it's easier to not show the warning until
|
|
// we've got something that will allow us to check authhash.
|
|
//+ utf8(info.m_radionomyID.empty() ? warningImage(false) + " <b>Please Register Your Authhash</b><br><br>" : "") +
|
|
"<a href=\"admin.cgi?sid=" + tos((*i).first) + "&mode=register\">Update Authhash</a>"
|
|
" | <a href=\"admin.cgi?sid=" + tos((*i).first) + "&mode=register\">Manage Authhash</a></div>";
|
|
}
|
|
}
|
|
|
|
if (!streamBody2.empty())
|
|
{
|
|
streamBody2 += "<br>";
|
|
}
|
|
streamBody2 += "<b>This stream is configured but has no source connected.</b>" +
|
|
(data.connectedListeners > 0 ? "<div style=\"text-align:center;padding-top:0.5em;\">Listeners: "
|
|
"<b>" + tos(data.connectedListeners) + "</b>" + (data.peakListeners > 0 ? " | "
|
|
"Peak: <b>" + tos(data.peakListeners) + "</b>" : "") + " [ <a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=kickdst&kickdst=all\">kick all</a> ]" + "</div>" : "");
|
|
}
|
|
else
|
|
{
|
|
streamBody2 += "<b>This stream is configured as having been moved or retired.<br>"
|
|
"No source connections will be allowed for this stream.<br><br>"
|
|
"All client connections received will be redirected to:<br>" +
|
|
urlLink(movedUrl) + "</b>";
|
|
++movedTotal;
|
|
}
|
|
|
|
if (!streamBody2.empty())
|
|
{
|
|
streamBody += "<tr><td align=\"center\">" + streamBody2 + "</td></tr>";
|
|
}
|
|
|
|
streamBlocks[(*i).first] = streamBody;
|
|
}
|
|
}
|
|
}
|
|
|
|
// this will now do a final check for any listeners which are on
|
|
// an un-confiured stream but are being provided a 'backupfile'.
|
|
const streamData::streamIDs_t activeIds = stats::getActiveStreamIds();
|
|
if (!activeIds.empty())
|
|
{
|
|
for (streamData::streamIDs_t::const_iterator i = activeIds.begin(); i != activeIds.end(); ++i)
|
|
{
|
|
if (streamBlocks.find((*i)) == streamBlocks.end())
|
|
{
|
|
stats::statsData_t data;
|
|
stats::getStats((*i), data);
|
|
|
|
// increment our stream total now that we know we have one
|
|
totalListeners += data.connectedListeners;
|
|
totalPeakListeners += data.peakListeners;
|
|
|
|
utf8 streamBody = "<tr><td align=\"center\" class=\"tnl\">"
|
|
"<a href=\"index.html?sid=" + tos((*i)) + "\">Stream #" + tos((*i)) + "</a> "
|
|
"<a href=\"admin.cgi?sid=" + tos((*i)) + "\">(Stream Login)</a></td></tr>",
|
|
streamBody2;
|
|
streamBody2 += "<b>This stream is not configured and has no source connected.</b>" +
|
|
(data.connectedListeners > 0 ? "<div style=\"text-align:center;padding-top:0.5em;\">Listeners: "
|
|
"<b>" + tos(data.connectedListeners) + "</b>" + (data.peakListeners > 0 ? " | "
|
|
"Peak: <b>" + tos(data.peakListeners) + "</b>" : "") + " [ <a href=\"admin.cgi?sid=" +
|
|
tos(sid) + "&mode=kickdst&kickdst=all\">kick all</a> ]" + "</div>" : "");
|
|
|
|
if (!streamBody2.empty())
|
|
{
|
|
streamBody += "<tr><td align=\"center\">" + streamBody2 + "</td></tr>";
|
|
}
|
|
|
|
streamBlocks[(*i)] = streamBody;
|
|
}
|
|
}
|
|
}
|
|
|
|
// now build up the output since if we've factored in inactive relay connections
|
|
// then we could otherwise be showing the streams in an out of order manner
|
|
for (map<size_t,uniString::utf8>::const_iterator i = streamBlocks.begin(); i != streamBlocks.end(); ++i)
|
|
{
|
|
if (!(*i).second.empty())
|
|
{
|
|
if (streamTotal > 0)
|
|
{
|
|
streams += "<tr><td align=\"center\"><br><hr><br></td></tr>";
|
|
}
|
|
streams += (*i).second;
|
|
++streamTotal;
|
|
}
|
|
}
|
|
|
|
// output a refresh option if there were no active streams found
|
|
if (!streamTotal)
|
|
{
|
|
if (isPostSetup() && isPostSetup() <= 2)
|
|
{
|
|
streams += "<tr><td align=\"center\">"
|
|
"<br><div align=\"center\" class=\"infh\" style=\"border-bottom:0;top:-19px;\">"
|
|
"Setup Successfully Completed</div></td></tr><tr><td align=\"center\">"
|
|
"No stream sources are currently connected.<br>"
|
|
"To find newly connected sources, <a href=\"admin.cgi?sid=0\">click here.</a></td></tr>";
|
|
setPostSetup(isPostSetup() + 1);
|
|
}
|
|
else
|
|
{
|
|
streams += "<tr><td align=\"center\">No stream sources are currently connected.<br>"
|
|
"To find newly connected sources, <a href=\"admin.cgi?sid=0\">click here.</a><br><br></td></tr>";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
setPostSetup(0);
|
|
}
|
|
}
|
|
else if (refreshRequired > 0)
|
|
{
|
|
streams += "<tr><td align=\"center\" id=\"counter\">"
|
|
"Waiting " + tos(refreshRequired) + " second" + ((refreshRequired > 1) ? "s" : (utf8)"") +
|
|
" for any configuration changes to take effect.<br>"
|
|
"If not automatically redirected or do not want to wait, "
|
|
"<a href=\"admin.cgi?sid=0\">click here.</a><br><br></td></tr>";
|
|
}
|
|
else if (refreshRequired < 0)
|
|
{
|
|
streams += "<tr><td align=\"center\" id=\"counter\">"
|
|
"Waiting " + tos(abs(refreshRequired)) + " second" + ((abs(refreshRequired) > 1) ? "s" : (utf8)"") +
|
|
" whilst checking for any DNAS updates.<br>If not automatically redirected or do not want to wait, "
|
|
"<a href=\"admin.cgi?sid=0\">click here.</a><br><br></td></tr>";
|
|
}
|
|
|
|
utf8 log = gOptions.realLogFile();
|
|
utf8::size_type pos = log.rfind(fileUtil::getFilePathDelimiter());
|
|
if ((pos != utf8::npos))
|
|
{
|
|
log = log.substr(pos + 1);
|
|
}
|
|
|
|
utf8 conf = gOptions.confFile();
|
|
pos = conf.rfind(fileUtil::getFilePathDelimiter());
|
|
if ((pos != utf8::npos))
|
|
{
|
|
conf = conf.substr(pos + 1);
|
|
}
|
|
|
|
const bool requires = gOptions.requireStreamConfigs();
|
|
body += "<table cellpadding=\"5\" cellspacing=\"0\" border=\"0\" width=\"100%\">"
|
|
"<tr><td class=\"tsp\" align=\"center\">"
|
|
"Available Streams: " + tos(streamTotal - movedTotal) +
|
|
" | "
|
|
"Server Listeners: " + tos(totalListeners) +
|
|
" | "
|
|
"Peak Server Listeners: " + tos(totalPeakListeners) +
|
|
" | "
|
|
"Unique Listeners: " + tos(stats::getTotalUniqueListeners()) +
|
|
"</td></tr></table>"
|
|
|
|
"<div style=\"padding:0 1em;\">"
|
|
"<br><table width=\"100%\" align=\"center\"><tr valign=\"top\"><td>"
|
|
|
|
"<table class=\"en\" cellpadding=\"15px\" style=\"border:0;display:block;padding-right:1em;\"><tr>"
|
|
"<td valign=\"top\" style=\"max-width:20em;\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Server Management</b></div>"
|
|
"Rotate Log File(s):"
|
|
" [ <a href=\"admin.cgi?mode=rotate\">All</a> ]"
|
|
" [ <a href=\"admin.cgi?mode=rotate&files=log\">Log</a> ]"
|
|
" [ <a href=\"admin.cgi?mode=rotate&files=w3c\">W3C</a> ]"
|
|
"<br><br>"
|
|
|
|
// trim down the file paths shown to make things less cluttered on hosted setups
|
|
// places the full path in the 'title' so it can still be found if required, etc
|
|
"Log File: <b title=\"" +
|
|
aolxml::escapeXML(fileUtil::getFullFilePath(gOptions.realLogFile())) +
|
|
"\">" + aolxml::escapeXML(fileUtil::stripPath(log)) + "</b><br><br>"
|
|
"Configuration File: <b title=\"" +
|
|
aolxml::escapeXML(fileUtil::getFullFilePath(gOptions.confFile())) +
|
|
"\">" + aolxml::escapeXML(fileUtil::stripPath(conf)) + "</b><br>"
|
|
"[ <a href=\"admin.cgi?mode=config\">View</a> ]"
|
|
" [ <a href=\"admin.cgi?mode=reload\">Update</a> ]"
|
|
" [ <a href=\"admin.cgi?mode=reload&force=1\">Forced</a> ]"
|
|
|
|
"<br><br><hr><br>Source Connection Details:"
|
|
" [ <a href=\"admin.cgi?mode=sources\">View</a> ]<br>" +
|
|
|
|
(requires ? "<br><b>Note:</b> Only pre-configured streams are "
|
|
"allowed to connect / be run on this server.<br>" : "") +
|
|
|
|
"<br>Advert Group Details:"
|
|
" [ <a href=\"admin.cgi?mode=adgroups\">View</a> ]<br>"
|
|
|
|
"<br><hr><br>Stream Configuration(s):"
|
|
" [ <a href=\"admin.cgi?sid=1&mode=viewjson&page=6\">JSON</a> ]"
|
|
" [ <a href=\"admin.cgi?sid=1&mode=viewxml&page=6\">XML</a> ]<br>"
|
|
|
|
"<br>View Stream Statistics:"
|
|
" [ <a href=\"statistics?json=1\">JSON</a> ]"
|
|
" [ <a href=\"statistics\">XML</a> ]<br>"
|
|
|
|
"<br><hr><br>Manage Stream Source(s):<br>"
|
|
"[ <a href=\"admin.cgi?mode=startrelays\">Start Relays</a> ]"
|
|
" [ <a href=\"admin.cgi?mode=kicksources\">Kick / Stop All</a> ]<br>"
|
|
|
|
"<br><hr><br>Reload Banned List(s):"
|
|
" [ <a href=\"admin.cgi?mode=bannedlist\">Update</a> ]<br>"
|
|
|
|
"<br>Reload Reserved List(s):"
|
|
" [ <a href=\"admin.cgi?mode=reservelist\">Update</a> ]<br>"
|
|
|
|
"<br>Reload User Agent List(s):"
|
|
" [ <a href=\"admin.cgi?mode=useragentlist\">Update</a> ]<br>"
|
|
|
|
"<br>Reload Admin Access List:"
|
|
" [ <a href=\"admin.cgi?mode=adminlist\">Update</a> ]<br>"
|
|
|
|
"<br>Clear Resource / Page Cache:"
|
|
" [ <a href=\"admin.cgi?mode=clearcache\">Clear</a> ]<br>"
|
|
|
|
"<br><hr><br>Debugging Options:<br>"
|
|
"[ <a href=\"admin.cgi?mode=debug&option=all&on=true\">Enable All</a> ]"
|
|
" [ <a href=\"admin.cgi?mode=debug&option=all&on=false\">Disable All</a> ]"
|
|
" [ <a href=\"admin.cgi?mode=debug\">Edit</a> ]"
|
|
|
|
"<br><br><hr><br>New DNAS Release: [ <a href=\"admin.cgi?mode=version\">Check</a> ]<br>"
|
|
"Last checked: <b>" + getCheckedDuration((size_t)(m_lastActivityTime - last_update_check)) + "</b>"
|
|
"</td></tr></table></td><td>";
|
|
|
|
// display update message where applicable
|
|
updater::verInfo ver;
|
|
utf8 update_msg;
|
|
if (updater::getNewVersion(ver))
|
|
{
|
|
update_msg += "<tr><td align=\"center\" class=\"tnl\">"
|
|
"<table cellspacing=\"0\" cellpadding=\"5\"><tr><td align=\"center\" class=\"infb\">"
|
|
"<div align=\"center\" class=\"infh\"><b>New DNAS Version Available</b></div>A new version of the DNAS is now available:"
|
|
" <b>" + ver.ver + "</b><br><br>" +
|
|
(!ver.url.empty() ?
|
|
((ver.downloaded && !ver.fn.empty()) ? "The new version has been automatically downloaded to:<br><br><b>" + aolxml::escapeXML(ver.fn) + "</b><br><br>"
|
|
"Please install this update as soon as possible, thank you.<br><br>"
|
|
"Specific changes for this version can be found <a href=\"" + aolxml::escapeXML(ver.info) + "\"><b>here</b></a>."
|
|
:
|
|
"Click <a href=\"" + aolxml::escapeXML(ver.url) + "\"><b>here</b></a> to download the new version of the DNAS.<br><br>"
|
|
"Please install this update as soon as possible, thank you.<br><br>"
|
|
"Specific changes for this version can be found <a href=\"" + aolxml::escapeXML(ver.info) + "\"><b>here</b></a>.") : "") +
|
|
(!ver.message.empty() ? "<br>" + ver.message :
|
|
(!ver.log.empty() ? "<br>For more details of the changes in this version see <a target=\"_blank\" href=\"" + aolxml::escapeXML(ver.log) + "\">" +
|
|
"<b>here</b></a>." : "")) + "</td></tr></table><br></td></tr>"
|
|
"<tr><td align=\"center\"><hr><br></td></tr>";
|
|
}
|
|
|
|
body += "<table cellspacing=\"0\" cellpadding=\"5\" border=\"0\">" +
|
|
update_msg + streams + "</table></td></tr></table></div>" +
|
|
"<script type=\"text/javascript\">"
|
|
"function $(id){return document.getElementById(id);}" EL;
|
|
|
|
// for a refresh, we'll show a countdown so it's obvious that something is happening
|
|
if (refreshRequired > 0)
|
|
{
|
|
body += "var c = " + tos(refreshRequired) + ";" EL
|
|
"function counter(){" EL
|
|
"c--;" EL
|
|
"if(c>0){" EL
|
|
"$('counter').innerHTML=\"Waiting \"+c+\" second\" + (c > 1 ? \"s\" : \"\") + \" for any configuration changes to take effect.<br>"
|
|
"If not automatically redirected or do not want to wait, <a href=\\\"admin.cgi\\\">click here.</a><br><br>\";" EL
|
|
"}" EL
|
|
"}" EL
|
|
"setInterval(counter,1000);" EL;
|
|
}
|
|
else if (refreshRequired < 0)
|
|
{
|
|
body += "var c = " + tos(abs(refreshRequired)) + ";" EL
|
|
"function counter(){" EL
|
|
"c--;" EL
|
|
"if(c>0){" EL
|
|
"$('counter').innerHTML=\"Waiting \"+c+\" second\" + (c > 1 ? \"s\" : \"\") + \" whilst checking for any DNAS updates.<br>"
|
|
"If not automatically redirected or do not want to wait, <a href=\\\"admin.cgi\\\">click here.</a><br><br>\";" EL
|
|
"}" EL
|
|
"}" EL
|
|
"setInterval(counter,1000);" EL;
|
|
}
|
|
|
|
body += getUptimeScript(true) + "</script>" +
|
|
getIEFlexFix() + getHTML5Remover() + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_bandwidth_html(const int refreshRequired) throw()
|
|
{
|
|
const __uint64 all = bandWidth::getAmount(bandWidth::ALL),
|
|
sent = bandWidth::getAmount(bandWidth::ALL_SENT),
|
|
recv = bandWidth::getAmount(bandWidth::ALL_RECV);
|
|
const time_t running = (::time(NULL) - g_upTime);
|
|
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
body = getServerAdminHeader("Server Bandwidth Usage", refreshRequired, "&mode=bandwidth&refresh=" + tos(abs(refreshRequired)), 1) +
|
|
"<table cellpadding=\"5\" cellspacing=\"0\" border=\"0\" width=\"100%\">"
|
|
"<tr><td class=\"tsp\" align=\"center\">"
|
|
"Total: <b>" + formatSizeString(all) + (all > 0 ? " (~" + formatSizeString((all / running) * 86400) + " / day)" : "") + "</b>"
|
|
" | "
|
|
"Sent: <b>" + formatSizeString(sent) + (sent > 0 ? " (~" + formatSizeString((sent / running) * 86400) + " / day)" : "") + "</b>"
|
|
" | "
|
|
"Received: <b>" + formatSizeString(recv) + (recv > 0 ? " (~" + formatSizeString((recv / running) * 86400) + " / day)" : "") + "</b>"
|
|
"</td></tr></table><div><br>"
|
|
|
|
"<table align=\"center\" class=\"en\" cellspacing=\"0\" cellpadding=\"3\" style=\"border:0;\">"
|
|
|
|
"<tr class=\"infh\"><td class=\"t\"> Total Client Data Sent</td>"
|
|
"<td class=\"st\"><b>" + formatSizeString(bandWidth::getAmount(bandWidth::ALL_CLIENT_SENT)) + "</b></td></tr>"
|
|
"<tr class=\"t\"><td>v2 Client(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::CLIENT_V2_SENT)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>v1 Client(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::CLIENT_V1_SENT)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>HTTP Client(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::CLIENT_HTTP_SENT)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>FLV Client(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::CLIENT_FLV_SENT)) + "</td></tr>"
|
|
#if 0
|
|
"<tr class=\"t\"><td>M4A Client(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::CLIENT_M4A_SENT)) + "</td></tr>"
|
|
#endif
|
|
"<tr><td style=\"border:0;\"> </td></tr>"
|
|
|
|
"<tr class=\"infh\"><td class=\"t\"> Total Source Data Received</td>"
|
|
"<td class=\"st\"><b>" + formatSizeString(bandWidth::getAmount(bandWidth::ALL_SOURCE_RECV)) + "</b></td></tr>"
|
|
"<tr class=\"t\"><td>v2 Source(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::SOURCE_V2_RECV)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>v1 Source(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::SOURCE_V1_RECV)) + "</td></tr>"
|
|
"<tr><td style=\"border:0;\"> </td></tr>"
|
|
|
|
"<tr class=\"infh\"><td class=\"t\"> Total Source Data Sent</td>"
|
|
"<td class=\"st\"><b>" + formatSizeString(bandWidth::getAmount(bandWidth::ALL_SOURCE_SENT)) + "</b></td></tr>"
|
|
"<tr class=\"t\"><td>v2 Source(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::SOURCE_V2_SENT)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>v1 Source(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::SOURCE_V1_SENT)) + "</td></tr>"
|
|
"<tr><td style=\"border:0;\"> </td></tr>"
|
|
|
|
"<tr class=\"infh\"><td class=\"t\"> Total Relay Data Received</td>"
|
|
"<td class=\"st\"><b>" + formatSizeString(bandWidth::getAmount(bandWidth::ALL_RELAY_RECV)) + "</b></td></tr>"
|
|
"<tr class=\"t\"><td>Handshaking</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::RELAY_MISC_RECV)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>v2 Relay(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::RELAY_V2_RECV)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>v1 Relay(s)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::RELAY_V1_RECV)) + "</td></tr>"
|
|
"<tr><td style=\"border:0;\"> </td></tr>"
|
|
|
|
"<tr class=\"infh\"><td class=\"t\"> Total Web Page, XML and Resoures </td>"
|
|
"<td class=\"st\"><b>" + formatSizeString(bandWidth::getAmount(bandWidth::ALL_WEB)) + "</b></td></tr>"
|
|
"<tr class=\"t\"><td>Public (e.g. /stats or index.html)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::PUBLIC_WEB)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>Private (e.g. /admin.cgi pages)</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::PRIVATE_WEB)) + "</td></tr>"
|
|
"<tr><td style=\"border:0;\"> </td></tr>"
|
|
|
|
"<tr class=\"infh\"><td class=\"t\"> Total Other Data</td>"
|
|
"<td class=\"st\"><b>" + formatSizeString(bandWidth::getAmount(bandWidth::ALL_OTHER)) + "</b></td></tr>"
|
|
"<tr class=\"t\"><td>Flash Policy Server</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::FLASH_POLICY)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>v2 Relay(s) Sent</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::RELAY_V2_SENT)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>YP Sent</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::YP_SENT)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>YP Received</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::YP_RECV)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>Listener Authentication and Metrics</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::AUTH_AND_METRICS)) + "</td></tr>"
|
|
"<tr class=\"t\"><td>Advert Retrieval</td>"
|
|
"<td class=\"s\">" + formatSizeString(bandWidth::getAmount(bandWidth::ADVERTS)) + "</td></tr>"
|
|
|
|
"</table></div>" + getUptimeScript() + getIEFlexFix() + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_bandwidth_xml() throw()
|
|
{
|
|
utf8 header = "HTTP/1.1 200 OK\r\nContent-Type:text/xml\r\n",
|
|
body = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?>"
|
|
"<SHOUTCASTSERVER>"
|
|
|
|
"<TOTAL>" + tos(bandWidth::getAmount(bandWidth::ALL)) + "</TOTAL>"
|
|
"<SENT>" + tos(bandWidth::getAmount(bandWidth::ALL_SENT)) + "</SENT>"
|
|
"<RECV>" + tos(bandWidth::getAmount(bandWidth::ALL_RECV)) + "</RECV>"
|
|
"<TIME>" + tos((::time(NULL) - g_upTime)) + "</TIME>"
|
|
|
|
"<CLIENTSENT>"
|
|
"<TOTAL>" + tos(bandWidth::getAmount(bandWidth::ALL_CLIENT_SENT)) + "</TOTAL>"
|
|
"<V2>" + tos(bandWidth::getAmount(bandWidth::CLIENT_V2_SENT)) + "</V2>"
|
|
"<V1>" + tos(bandWidth::getAmount(bandWidth::CLIENT_V1_SENT)) + "</V1>"
|
|
"<HTTP>" + tos(bandWidth::getAmount(bandWidth::CLIENT_HTTP_SENT)) + "</HTTP>"
|
|
"<FLV>" + tos(bandWidth::getAmount(bandWidth::CLIENT_FLV_SENT)) + "</FLV>"
|
|
#if 0
|
|
"<M4A>" + tos(bandWidth::getAmount(bandWidth::CLIENT_M4A_SENT)) + "</M4A>"
|
|
#endif
|
|
"</CLIENTSENT>"
|
|
|
|
"<SOURCERECV>"
|
|
"<TOTAL>" + tos(bandWidth::getAmount(bandWidth::ALL_SOURCE_RECV)) + "</TOTAL>"
|
|
"<V2>" + tos(bandWidth::getAmount(bandWidth::SOURCE_V2_RECV)) + "</V2>"
|
|
"<V1>" + tos(bandWidth::getAmount(bandWidth::SOURCE_V1_RECV)) + "</V1>"
|
|
"</SOURCERECV>"
|
|
|
|
"<SOURCESENT>"
|
|
"<TOTAL>" + tos(bandWidth::getAmount(bandWidth::ALL_SOURCE_SENT)) + "</TOTAL>"
|
|
"<V2>" + tos(bandWidth::getAmount(bandWidth::SOURCE_V2_SENT)) + "</V2>"
|
|
"<V1>" + tos(bandWidth::getAmount(bandWidth::SOURCE_V1_SENT)) + "</V1>"
|
|
"</SOURCESENT>"
|
|
|
|
"<RELAYRECV>"
|
|
"<TOTAL>" + tos(bandWidth::getAmount(bandWidth::ALL_RELAY_RECV)) + "</TOTAL>"
|
|
"<MISC>" + tos(bandWidth::getAmount(bandWidth::RELAY_MISC_RECV)) + "</MISC>"
|
|
"<V2>" + tos(bandWidth::getAmount(bandWidth::RELAY_V2_RECV)) + "</V2>"
|
|
"<V1>" + tos(bandWidth::getAmount(bandWidth::RELAY_V1_RECV)) + "</V1>"
|
|
"</RELAYRECV>"
|
|
|
|
"<WEBPAGES>"
|
|
"<TOTAL>" + tos(bandWidth::getAmount(bandWidth::ALL_WEB)) + "</TOTAL>"
|
|
"<PUBLIC>" + tos(bandWidth::getAmount(bandWidth::PUBLIC_WEB)) + "</PUBLIC>"
|
|
"<PRIVATE>" + tos(bandWidth::getAmount(bandWidth::PRIVATE_WEB)) + "</PRIVATE>"
|
|
"</WEBPAGES>"
|
|
|
|
"<OTHER>"
|
|
"<TOTAL>" + tos(bandWidth::getAmount(bandWidth::ALL_OTHER)) + "</TOTAL>"
|
|
"<FLASH>" + tos(bandWidth::getAmount(bandWidth::FLASH_POLICY)) + "</FLASH>"
|
|
"<RELAYSENTV2>" + tos(bandWidth::getAmount(bandWidth::RELAY_V2_SENT)) + "</RELAYSENTV2>"
|
|
"<YPSENT>" + tos(bandWidth::getAmount(bandWidth::YP_SENT)) + "</YPSENT>"
|
|
"<YPRECV>" + tos(bandWidth::getAmount(bandWidth::YP_RECV)) + "</YPRECV>"
|
|
"<AUTH_METRICS>" + tos(bandWidth::getAmount(bandWidth::AUTH_AND_METRICS)) + "</AUTH_METRICS>"
|
|
"<ADVERTS>" + tos(bandWidth::getAmount(bandWidth::ADVERTS)) + "</ADVERTS>"
|
|
"</OTHER>"
|
|
"</SHOUTCASTSERVER>";
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_bandwidth_json(const uniString::utf8& callback) throw()
|
|
{
|
|
const bool jsonp = !callback.empty();
|
|
utf8 header = "HTTP/1.1 200 OK\r\nContent-Type:application/json;charset=utf-8\r\n",
|
|
body = (jsonp ? callback + "(" : "") +
|
|
"{"
|
|
"\"total\":" + tos(bandWidth::getAmount(bandWidth::ALL)) + ","
|
|
"\"sent\":" + tos(bandWidth::getAmount(bandWidth::ALL_SENT)) + ","
|
|
"\"recv\":" + tos(bandWidth::getAmount(bandWidth::ALL_RECV)) + ","
|
|
"\"time\":" + tos((::time(NULL) - g_upTime)) + ","
|
|
"\"clientsent\":{"
|
|
"\"total\":" + tos(bandWidth::getAmount(bandWidth::ALL_CLIENT_SENT)) + ","
|
|
"\"v2\":" + tos(bandWidth::getAmount(bandWidth::CLIENT_V2_SENT)) + ","
|
|
"\"v1\":" + tos(bandWidth::getAmount(bandWidth::CLIENT_V1_SENT)) + ","
|
|
"\"http\":" + tos(bandWidth::getAmount(bandWidth::CLIENT_HTTP_SENT)) + ","
|
|
"\"flv\":" + tos(bandWidth::getAmount(bandWidth::CLIENT_FLV_SENT)) +
|
|
#if 0
|
|
","
|
|
"\"m4a\":" + tos(bandWidth::getAmount(bandWidth::CLIENT_M4A_SENT)) +
|
|
#endif
|
|
"},\"sourcerecv\":{"
|
|
"\"total\":" + tos(bandWidth::getAmount(bandWidth::ALL_SOURCE_RECV)) + ","
|
|
"\"v2\":" + tos(bandWidth::getAmount(bandWidth::SOURCE_V2_RECV)) + ","
|
|
"\"v1\":" + tos(bandWidth::getAmount(bandWidth::SOURCE_V1_RECV)) +
|
|
"},\"sourcesent\":{"
|
|
"\"total\":" + tos(bandWidth::getAmount(bandWidth::ALL_SOURCE_SENT)) + ","
|
|
"\"v2\":" + tos(bandWidth::getAmount(bandWidth::SOURCE_V2_SENT)) + ","
|
|
"\"v1\":" + tos(bandWidth::getAmount(bandWidth::SOURCE_V2_SENT)) +
|
|
"},\"relayrecv\":{"
|
|
"\"total\":" + tos(bandWidth::getAmount(bandWidth::ALL_SOURCE_RECV)) + ","
|
|
"\"misc\":" + tos(bandWidth::getAmount(bandWidth::RELAY_MISC_RECV)) + ","
|
|
"\"v2\":" + tos(bandWidth::getAmount(bandWidth::RELAY_V2_RECV)) + ","
|
|
"\"v1\":" + tos(bandWidth::getAmount(bandWidth::RELAY_V1_RECV)) +
|
|
"},\"webpages\":{"
|
|
"\"total\":" + tos(bandWidth::getAmount(bandWidth::ALL_WEB)) + ","
|
|
"\"public\":" + tos(bandWidth::getAmount(bandWidth::PUBLIC_WEB)) + ","
|
|
"\"private\":" + tos(bandWidth::getAmount(bandWidth::PRIVATE_WEB)) +
|
|
"},\"other\":{"
|
|
"\"total\":" + tos(bandWidth::getAmount(bandWidth::ALL_OTHER)) + ","
|
|
"\"flash\":" + tos(bandWidth::getAmount(bandWidth::FLASH_POLICY)) + ","
|
|
"\"relaysentv2\":" + tos(bandWidth::getAmount(bandWidth::RELAY_V2_SENT)) + ","
|
|
"\"ypsent\":" + tos(bandWidth::getAmount(bandWidth::YP_SENT)) + "," +
|
|
"\"yprecv\":" + tos(bandWidth::getAmount(bandWidth::YP_RECV)) + "," +
|
|
"\"auth_metrics\":" + tos(bandWidth::getAmount(bandWidth::AUTH_AND_METRICS)) + "," +
|
|
"\"adverts\":" + tos(bandWidth::getAmount(bandWidth::ADVERTS)) +
|
|
"}}" + (jsonp ? utf8(")") : "");
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_ypstatus_xml() throw()
|
|
{
|
|
utf8 header = "HTTP/1.1 200 OK\r\nContent-Type:text/xml\r\n",
|
|
body = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?>"
|
|
"<SHOUTCASTSERVER>";
|
|
|
|
streamData::streamIDs_t streamIds = streamData::getStreamIds();
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
if ((*i).second.m_streamID)
|
|
{
|
|
streamIds.insert((*i).second.m_streamID);
|
|
}
|
|
}
|
|
|
|
for (streamData::streamIDs_t::const_iterator i = streamIds.begin(); i != streamIds.end(); ++i)
|
|
{
|
|
streamData::streamInfo info;
|
|
streamData::extraInfo extra;
|
|
streamData::getStreamInfo((*i), info, extra);
|
|
|
|
const utf8 movedUrl = gOptions.stream_movedUrl((*i));
|
|
if (movedUrl.empty())
|
|
{
|
|
body += "<STREAM id=\"" + tos((*i)) + "\">" +
|
|
(!extra.isConnected ? "<STATUS>NOSOURCE</STATUS>" :
|
|
(info.m_streamPublic ? (extra.ypConnected != 1 ? (!yp2::isValidAuthhash(info.m_authHash) ?
|
|
(info.m_authHash.empty() ? "<STATUS>EMPTY_AUTHHASH</STATUS>" : "<STATUS>INVALID_AUTHHASH</STATUS>") :
|
|
(extra.ypErrorCode == 200 ? "<STATUS>WAITING</STATUS>" :
|
|
(extra.ypErrorCode == YP_COMMS_FAILURE ? "<STATUS>YP_NOT_FOUND</STATUS>" :
|
|
(extra.ypErrorCode == YP_MAINTENANCE_CODE ? "<STATUS>YP_MAINTENANCE</STATUS>" :
|
|
"<STATUS>ERROR</STATUS><CODE>" + tos(extra.ypErrorCode) + "</CODE>")))) :
|
|
"<STATUS>PUBLIC</STATUS><STNID>" + info.m_stationID + "</STNID>") :
|
|
"<STATUS>PRIVATE</STATUS>")) + "</STREAM>";
|
|
}
|
|
else
|
|
{
|
|
body += "<STREAM id=\"" + tos((*i)) + "\"><STATUS>MOVED</STATUS></STREAM>";
|
|
}
|
|
}
|
|
|
|
body += "</SHOUTCASTSERVER>";
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_ypstatus_json(const uniString::utf8& callback) throw()
|
|
{
|
|
utf8 header = "HTTP/1.1 200 OK\r\nContent-Type:application/json;charset=utf-8\r\n";
|
|
|
|
streamData::streamIDs_t streamIds = streamData::getStreamIds();
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
if ((*i).second.m_streamID)
|
|
{
|
|
streamIds.insert((*i).second.m_streamID);
|
|
}
|
|
}
|
|
|
|
const bool jsonp = !callback.empty();
|
|
utf8 body = (jsonp ? callback + "(" : "") + "{" +
|
|
(!streamIds.empty() ? "\"streams\":[" : "");
|
|
|
|
bool read = false;
|
|
for (streamData::streamIDs_t::const_iterator i = streamIds.begin(); i != streamIds.end(); ++i)
|
|
{
|
|
streamData::streamInfo info;
|
|
streamData::extraInfo extra;
|
|
streamData::getStreamInfo((*i), info, extra);
|
|
|
|
const utf8 movedUrl = gOptions.stream_movedUrl((*i));
|
|
if (movedUrl.empty())
|
|
{
|
|
body += (read ? utf8(",") : "") +
|
|
"{"
|
|
"\"id\":" + tos((*i)) + "," +
|
|
(!extra.isConnected ? "\"status\":\"" + escapeJSON("nosource") + "\"" :
|
|
(info.m_streamPublic ? (extra.ypConnected != 1 ? (!yp2::isValidAuthhash(info.m_authHash) ?
|
|
(info.m_authHash.empty() ? "\"status\":\"" + escapeJSON("empty_authhash") + "\"" :
|
|
"\"status\":\"" + escapeJSON("invalid_authhash") + "\"") :
|
|
(extra.ypErrorCode == 200 ? "\"status\":\"" + escapeJSON("waiting") + "\"" :
|
|
(extra.ypErrorCode == YP_COMMS_FAILURE ? "\"status\":\"" + escapeJSON("yp_not_found") + "\"" :
|
|
(extra.ypErrorCode == YP_MAINTENANCE_CODE ? "\"status\":\"" + escapeJSON("yp_maintenance") + "\"" :
|
|
"\"status\":\"" + escapeJSON("error") + "\",\"code\":" + tos(extra.ypErrorCode))))) :
|
|
"\"status\":\"" + escapeJSON("public") + "\",\"stnid\":" + info.m_stationID) :
|
|
"\"status\":\"" + escapeJSON("private") + "\"")) + "}";
|
|
}
|
|
else
|
|
{
|
|
body += (read ? utf8(",") : "") + "{\"id\":" + tos((*i)) + ","
|
|
"\"status\":\"" + escapeJSON("moved") + "\"}";
|
|
}
|
|
read = true;
|
|
}
|
|
|
|
body += (!streamIds.empty() ? utf8("]") : "") + "}" + (jsonp ? utf8(")") : "");
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_viewxml(const streamData::streamID_t sid, int page,
|
|
const bool iponly, const bool ipcount) throw()
|
|
{
|
|
// abort as there's nothing generated for this now
|
|
if (page == 2)
|
|
{
|
|
sendMessageAndClose("HTTP/1.1 200 OK\r\nContent-Type:text/xml\r\n\r\n");
|
|
return;
|
|
}
|
|
|
|
if ((page > 6) || (page < 0))
|
|
{
|
|
page = 0;
|
|
}
|
|
|
|
utf8 header = "HTTP/1.1 200 OK\r\nContent-Type:text/xml\r\n",
|
|
body = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><SHOUTCASTSERVER>";
|
|
|
|
if (page < 2)
|
|
{
|
|
stats::statsData_t data;
|
|
body += protocol_HTTPStyle::getStatsXMLBody(sid, true, true, m_socket, data);
|
|
}
|
|
|
|
if ((page == 0) || (page == 3))
|
|
{
|
|
time_t t = ::time(NULL);
|
|
body += "<LISTENERS>";
|
|
|
|
stats::currentClientList_t client_data;
|
|
stats::getClientDataForStream(sid, client_data);
|
|
map<utf8, size_t> ip_counts;
|
|
for (stats::currentClientList_t::const_iterator i = client_data.begin(); i != client_data.end(); ++i)
|
|
{
|
|
const utf8& host = ((*i)->m_hostName != (*i)->m_ipAddr ? (*i)->m_hostName : (*i)->m_ipAddr);
|
|
|
|
if (!ipcount)
|
|
{
|
|
if (!iponly)
|
|
{
|
|
body += "<LISTENER>"
|
|
"<HOSTNAME>" + aolxml::escapeXML(host) + "</HOSTNAME>"
|
|
"<USERAGENT>" + aolxml::escapeXML((*i)->m_userAgent) + "</USERAGENT>"
|
|
"<CONNECTTIME>" + tos(t - (*i)->m_startTime) + "</CONNECTTIME>"
|
|
"<UID>" + tos((*i)->m_unique) + "</UID>"
|
|
"<TYPE>" + tos((*i)->m_clientType) + "</TYPE>"
|
|
"<REFERER>" + aolxml::escapeXML((*i)->m_referer) + "</REFERER>"
|
|
"<XFF>" + aolxml::escapeXML((*i)->m_XFF) + "</XFF>"
|
|
"<GRID>" + tos((*i)->m_group) + "</GRID>"
|
|
"<TRIGGERS>" + tos((*i)->m_triggers) + "</TRIGGERS>"
|
|
"</LISTENER>";
|
|
}
|
|
else
|
|
{
|
|
body += "<LISTENER>"
|
|
"<HOSTNAME>" + aolxml::escapeXML(host) + "</HOSTNAME>"
|
|
"</LISTENER>";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
++ip_counts[host];
|
|
}
|
|
|
|
delete (*i);
|
|
}
|
|
|
|
if (ipcount)
|
|
{
|
|
for (map<utf8, size_t>::const_iterator i = ip_counts.begin(); i != ip_counts.end(); ++i)
|
|
{
|
|
body += "<LISTENER>"
|
|
"<HOSTNAME>" + aolxml::escapeXML((*i).first) + "</HOSTNAME>"
|
|
"<TOTAL>" + tos((*i).second) + "</TOTAL>"
|
|
"</LISTENER>";
|
|
}
|
|
}
|
|
|
|
body += "</LISTENERS>";
|
|
}
|
|
|
|
if ((page == 0) || (page == 4) || (page == 5))
|
|
{
|
|
streamData::streamHistory_t songHistory;
|
|
streamData::getStreamSongHistory(sid, songHistory);
|
|
|
|
// only provide this as an option feature so as not to bloat the main xml stats unnecessarily
|
|
if (!(page == 5))
|
|
{
|
|
body += "<SONGHISTORY>";
|
|
for (streamData::streamHistory_t::const_iterator i = songHistory.begin(); i != songHistory.end(); ++i)
|
|
{
|
|
body += "<SONG><PLAYEDAT>" + tos((*i).m_when) + "</PLAYEDAT>"
|
|
"<TITLE>" + aolxml::escapeXML((*i).m_title) + "</TITLE>" +
|
|
protocol_HTTPStyle::getCurrentXMLMetadataBody(true, (*i).m_metadata) + "</SONG>";
|
|
}
|
|
body += "</SONGHISTORY>";
|
|
}
|
|
else
|
|
{
|
|
body += protocol_HTTPStyle::getCurrentXMLMetadataBody(false, (!songHistory.empty() ? songHistory[0].m_metadata : ""));
|
|
}
|
|
}
|
|
|
|
if (page == 6)
|
|
{
|
|
const int maxUsers = gOptions.maxUser();
|
|
body += "<STREAMCONFIGS>"
|
|
"<REQUIRECONFIGS>" + tos(gOptions.requireStreamConfigs()) + "</REQUIRECONFIGS>"
|
|
"<SERVERMAXLISTENERS>" + (maxUsers > 0 ? tos(maxUsers) : "UNLIMITED") + "</SERVERMAXLISTENERS>"
|
|
"<SERVERMINBITRATE>" + tos(gOptions.minBitrate()) + "</SERVERMINBITRATE>"
|
|
"<SERVERMAXBITRATE>" + tos(gOptions.maxBitrate()) + "</SERVERMAXBITRATE>"
|
|
"<TOTALCONFIGS>";
|
|
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
|
|
utf8 block = "";
|
|
size_t moved = 0;
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
const utf8 movedUrl = gOptions.stream_movedUrl((*i).second.m_streamID);
|
|
if (movedUrl.empty())
|
|
{
|
|
utf8 streamTag = ((*i).second.m_streamID > 1 ? "/stream/" + tos((*i).second.m_streamID) + "/" : "/");
|
|
if (!(*i).second.m_urlPath.empty())
|
|
{
|
|
streamTag = (*i).second.m_urlPath;
|
|
}
|
|
|
|
const utf8& authhash = (*i).second.m_authHash;
|
|
block += "<STREAMCONFIG id=\"" + tos((*i).second.m_streamID) + "\">"
|
|
"<STREAMAUTHHASH>" + aolxml::escapeXML(authhash.empty() ? "EMPTY" : !yp2::isValidAuthhash(authhash) ? "INVALID" : authhash) + "</STREAMAUTHHASH>"
|
|
"<STREAMPATH>" + aolxml::escapeXML(streamTag) + "</STREAMPATH>"
|
|
"<STREAMRELAYURL>" + aolxml::escapeXML((*i).second.m_relayUrl.url()) + "</STREAMRELAYURL>"
|
|
"<STREAMBACKUPURL>" + aolxml::escapeXML((*i).second.m_backupUrl.url()) + "</STREAMBACKUPURL>"
|
|
"<STREAMMAXLISTENERS>" + ((*i).second.m_maxStreamUser > 0 && (*i).second.m_maxStreamUser < gOptions.maxUser() ? tos((*i).second.m_maxStreamUser) : "SERVERMAXLISTENERS") + "</STREAMMAXLISTENERS>"
|
|
"<STREAMMINBITRATE>" + ((*i).second.m_minStreamBitrate > 0 ? tos((*i).second.m_minStreamBitrate) : "SERVERMINBITRATE") + "</STREAMMINBITRATE>"
|
|
"<STREAMMAXBITRATE>" + ((*i).second.m_maxStreamBitrate > 0 ? tos((*i).second.m_maxStreamBitrate) : "SERVERMAXBITRATE") + "</STREAMMAXBITRATE>"
|
|
"<STREAMPUBLIC>" + aolxml::escapeXML((*i).second.m_publicServer) + "</STREAMPUBLIC>"
|
|
"<STREAMALLOWRELAY>" + tos((*i).second.m_allowRelay) + "</STREAMALLOWRELAY>"
|
|
"<STREAMPUBLICRELAY>" + tos((*i).second.m_allowPublicRelay) + "</STREAMPUBLICRELAY>"
|
|
"</STREAMCONFIG>";
|
|
}
|
|
else
|
|
{
|
|
++moved;
|
|
}
|
|
}
|
|
|
|
body += tos((streams.size() - moved)) + "</TOTALCONFIGS>" + block + "</STREAMCONFIGS>";
|
|
}
|
|
|
|
body += "</SHOUTCASTSERVER>";
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_viewjson(const streamData::streamID_t sid, int page,
|
|
const bool iponly, const bool ipcount,
|
|
const uniString::utf8& callback) throw()
|
|
{
|
|
// abort as there's nothing generated for this now
|
|
if (page == 2)
|
|
{
|
|
sendMessageAndClose("HTTP/1.1 200 OK\r\nContent-Type:application/json;charset=utf-8\r\n\r\n");
|
|
return;
|
|
}
|
|
|
|
if (page > 6 || page < 0)
|
|
{
|
|
page = 0;
|
|
}
|
|
|
|
const bool jsonp = !callback.empty();
|
|
utf8 header = "HTTP/1.1 200 OK\r\nContent-Type:application/json;charset=utf-8\r\n",
|
|
body = (jsonp ? callback + "(" : "") + (page < 2 ? "{" : "");
|
|
|
|
if (page < 2)
|
|
{
|
|
stats::statsData_t data;
|
|
body += protocol_HTTPStyle::getStatsJSONBody(sid, true, true, m_socket, data);
|
|
}
|
|
|
|
if ((page == 0) || (page == 3))
|
|
{
|
|
time_t t = ::time(NULL);
|
|
body += (!page ? utf8(",\"listeners\":[") : "[");
|
|
|
|
stats::currentClientList_t client_data;
|
|
stats::getClientDataForStream(sid, client_data);
|
|
map<utf8, size_t> ip_counts;
|
|
for (stats::currentClientList_t::const_iterator i = client_data.begin(); i != client_data.end(); ++i)
|
|
{
|
|
const utf8& host = ((*i)->m_hostName != (*i)->m_ipAddr ? (*i)->m_hostName : (*i)->m_ipAddr);
|
|
|
|
if (!ipcount)
|
|
{
|
|
if (!iponly)
|
|
{
|
|
body += (i != client_data.begin() ? utf8(",") : "") +
|
|
"{"
|
|
"\"hostname\":\"" + escapeJSON(host) + "\","
|
|
"\"useragent\":\"" + escapeJSON((*i)->m_userAgent) + "\","
|
|
"\"connecttime\":" + tos(t - (*i)->m_startTime) + ","
|
|
"\"uid\":" + tos((*i)->m_unique) + ","
|
|
"\"type\":" + tos((*i)->m_clientType) + ","
|
|
"\"referer\":\"" + escapeJSON((*i)->m_referer) + "\","
|
|
"\"xff\":\"" + escapeJSON((*i)->m_XFF) + "\","
|
|
"\"grid\":" + tos((*i)->m_group) + ","
|
|
"\"triggers\":" + tos((*i)->m_triggers) + ""
|
|
"}";
|
|
}
|
|
else
|
|
{
|
|
body += (i != client_data.begin() ? utf8(",") : "") +
|
|
"{"
|
|
"\"hostname\":\"" + escapeJSON(host) + "\""
|
|
"}";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
++ip_counts[host];
|
|
}
|
|
|
|
delete (*i);
|
|
}
|
|
|
|
if (ipcount)
|
|
{
|
|
for (map<utf8, size_t>::const_iterator i = ip_counts.begin(); i != ip_counts.end(); ++i)
|
|
{
|
|
body += (i != ip_counts.begin() ? utf8(",") : "") +
|
|
"{"
|
|
"\"hostname\":\"" + escapeJSON((*i).first) + "\","
|
|
"\"total\":" + tos((*i).second) + ""
|
|
"}";
|
|
}
|
|
}
|
|
|
|
body += "]";
|
|
}
|
|
|
|
if ((page == 0) || (page == 4) || (page == 5))
|
|
{
|
|
streamData::streamHistory_t songHistory;
|
|
streamData::getStreamSongHistory(sid, songHistory);
|
|
|
|
// only provide this as an option feature so as not to bloat the main xml stats unnecessarily
|
|
if (!(page == 5))
|
|
{
|
|
bool first = true;
|
|
body += (!page ? utf8(",\"songs\":[") : "[");
|
|
for (streamData::streamHistory_t::const_iterator i = songHistory.begin(); i != songHistory.end(); ++i)
|
|
{
|
|
body += (!first ? utf8(",") : "") + "{\"playedat\":" +
|
|
tos((*i).m_when) + "," "\"title\":\"" +
|
|
escapeJSON((*i).m_title) + "\",\"metadata\":" +
|
|
protocol_HTTPStyle::getCurrentJSONMetadataBody((*i).m_metadata) + "}";
|
|
first = false;
|
|
}
|
|
body += "]";
|
|
}
|
|
else
|
|
{
|
|
body += protocol_HTTPStyle::getCurrentJSONMetadataBody((!songHistory.empty() ? songHistory[0].m_metadata : ""));
|
|
}
|
|
}
|
|
|
|
if (page == 6)
|
|
{
|
|
const int maxUsers = gOptions.maxUser();
|
|
body += "{"
|
|
"\"requireconfigs\":" + tos(gOptions.requireStreamConfigs()) + ","
|
|
"\"maxlisteners\":" + (maxUsers > 0 ? tos(maxUsers) : "\"unlimited\"") + ","
|
|
"\"minbitrate\":" + tos(gOptions.minBitrate()) + ","
|
|
"\"maxbitrate\":" + tos(gOptions.maxBitrate()) + ",";
|
|
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
|
|
bool read = false;
|
|
utf8 block = "";
|
|
size_t moved = 0;
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
const utf8 movedUrl = gOptions.stream_movedUrl((*i).second.m_streamID);
|
|
if (movedUrl.empty())
|
|
{
|
|
utf8 streamTag = ((*i).second.m_streamID > 1 ? "/stream/" + tos((*i).second.m_streamID) + "/" : "/");
|
|
if (!(*i).second.m_urlPath.empty())
|
|
{
|
|
streamTag = (*i).second.m_urlPath;
|
|
}
|
|
|
|
utf8 authhash = (*i).second.m_authHash;
|
|
block += (read ? utf8(",") : "") +
|
|
"{"
|
|
"\"id\":" + tos((*i).second.m_streamID) + ","
|
|
"\"authhash\":\"" + escapeJSON(authhash.empty() ? "empty" : !yp2::isValidAuthhash(authhash) ? "invalid" : authhash) + "\","
|
|
"\"path\":\"" + escapeJSON(streamTag) + "\","
|
|
"\"relayurl\":\"" + escapeJSON((*i).second.m_relayUrl.url()) + "\","
|
|
"\"backupurl\":\"" + escapeJSON((*i).second.m_backupUrl.url()) + "\","
|
|
"\"maxlisteners\":\"" + escapeJSON(((*i).second.m_maxStreamUser > 0) && ((*i).second.m_maxStreamUser < gOptions.maxUser()) ? tos((*i).second.m_maxStreamUser) : escapeJSON("maxlisteners")) + "\","
|
|
"\"minbitrate\":\"" + escapeJSON((*i).second.m_minStreamBitrate > 0 ? tos((*i).second.m_minStreamBitrate) : escapeJSON("minbitrate")) + "\","
|
|
"\"maxbitrate\":\"" + escapeJSON((*i).second.m_maxStreamBitrate > 0 ? tos((*i).second.m_maxStreamBitrate) : escapeJSON("maxbitrate")) + "\","
|
|
"\"public\":\"" + escapeJSON((*i).second.m_publicServer) + "\","
|
|
"\"allowrelay\":\"" + tos((*i).second.m_allowRelay) + "\","
|
|
"\"publicrelay\":\"" + tos((*i).second.m_allowPublicRelay) + "\""
|
|
"}";
|
|
read = true;
|
|
}
|
|
else
|
|
{
|
|
++moved;
|
|
}
|
|
}
|
|
const size_t total = (streams.size() - moved);
|
|
body += "\"total\":\"" + tos(total) + "\"" +
|
|
(total > 0 ? ",\"streams\":[" : "") + block +
|
|
(total > 0 ? utf8("]") : "") + "}";
|
|
}
|
|
|
|
body += (page < 2 ? utf8("}") : "") + (jsonp ? ")" : "");
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length:" + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_sources(const uniString::utf8& host) throw()
|
|
{
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
body = getServerAdminHeader("Server Source Connection Details") +
|
|
"<table cellpadding=\"5\" cellspacing=\"0\" border=\"0\" width=\"100%\">"
|
|
"<tr><td class=\"tsp\" align=\"center\">" +
|
|
utf8(gOptions.requireStreamConfigs() ? "Sources are only able to connect on the configured streams shown" :
|
|
"Sources are able to connect on any \"Stream ID\" (using the appropriate details)") +
|
|
"</td></tr></table>"
|
|
"<div><br>"
|
|
|
|
"<div style=\"width:19.5em;margin: 0 0 1em 1em;\" class=\"infb\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Information</b></div>"
|
|
"Here are the login details to enter in your chosen source software so it can then be used to connect to the server.<br><br>"
|
|
"<b>Note:</b> Names of options in the source software may vary from those shown.<br><br>"
|
|
|
|
"<b><font color=\"red\">*</font></b> This may not be correct and is a best guess from the current connection.<br><br><hr><br>"
|
|
"The '<b>Legacy Password</b>' is for use with legacy sources (which are sources that are not able to directly specify the "
|
|
"desired stream number).<br><br>This allows for use of any Shoutcast compatible source on any of the currently configured streams.<br><br>"
|
|
"If connecting a legacy source to stream #1, the '<b>Password</b>' value can be used.<br><br><hr><br>"
|
|
"The example JSON-P response uses callback=func which can be changed to a custom value as required for usage.<br><br><hr><br>"
|
|
|
|
"For further information on connecting sources to the server, as well as ensuring that all of the required ports have been opened, "
|
|
"see <a target=\"_blank\" href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_Server_Source_Support\"><b>this wiki page</b></a>.<br><br></div>";
|
|
|
|
config::streams_t streams;
|
|
gOptions.getStreamConfigs(streams);
|
|
|
|
utf8 ids = "";
|
|
for (config::streams_t::const_iterator i = streams.begin(); i != streams.end(); ++i)
|
|
{
|
|
ids += (i != streams.begin() ? ", ": "") + tos((*i).second.m_streamID);
|
|
|
|
// skip over moved streams as they cannot be used, though we include the
|
|
// streamid above (whether or not that'll confuse people i do not know).
|
|
const utf8 movedUrl = gOptions.stream_movedUrl((*i).second.m_streamID);
|
|
if (movedUrl.empty())
|
|
{
|
|
int maxbitrate = ((*i).second.m_maxStreamBitrate > 0 ? (*i).second.m_maxStreamBitrate : gOptions.maxBitrate()),
|
|
minbitrate = ((*i).second.m_minStreamBitrate > 0 ? (*i).second.m_minStreamBitrate : gOptions.minBitrate());
|
|
|
|
// test for being a relay which is active - if so then we need to not show
|
|
// direct source login details as they will not be able to connect anyway
|
|
bool noEntry = false, isRelay = (*i).second.m_relayUrl.isSet();
|
|
if (isRelay && streamData::isRelayActive((*i).second.m_streamID, noEntry))
|
|
{
|
|
body += "<div class=\"infb\" style=\"width:19.5em;word-wrap:break-word;margin: 0 0 1em 1em;\">"
|
|
"<div align=\"center\" class=\"infh\">"
|
|
"<b>Stream #" + tos((*i).second.m_streamID) + "</b></div>"
|
|
"This stream has been configured for use as a relay which is currently active.<br><br>"
|
|
"Any source connections attempted will be blocked whilst the relay is active.<br><br>"
|
|
"Password: <b>" + aolxml::escapeXML((*i).second.m_password) + "</b><br>"
|
|
"Source: <b>" + urlLink(niceURL((*i).second.m_relayUrl.url())) + "</b><br>" +
|
|
(!(*i).second.m_backupUrl.url().empty() ? "Backup Source: <b>" +
|
|
urlLink(niceURL((*i).second.m_backupUrl.url())) + "</b><br>" : "");
|
|
}
|
|
// otherwise show direct source login details for specific as well as generic stream
|
|
// logins (even if configured to be a relay as long as it is inactive at the time)
|
|
else
|
|
{
|
|
body += "<div class=\"infb\" style=\"width:19.5em;word-wrap:break-word;margin: 0 0 1em 1em;\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Stream #" + tos((*i).second.m_streamID) + "</b></div>" +
|
|
(isRelay ? "<b>Note:</b> Configured as a relay to use:<br><b>" +
|
|
niceURL((*i).second.m_relayUrl.url()) + "</b><br><br>" : "") +
|
|
"Stream ID: <b>" + tos((*i).second.m_streamID) + "</b><br>"
|
|
"Server Address: <b>" + (!host.empty() ? host : "<enter_server_address>") + "</b> <b>(<font color=\"red\">*</font>)</b><br>"
|
|
"Source Port: <b>" + tos(gOptions.portBase()) + "</b><br><br>" +
|
|
"Password: <b>" + aolxml::escapeXML((*i).second.m_password) + "</b><br>" +
|
|
((*i).second.m_streamID > 1 && (g_legacyPort > 0) ? "Legacy Password: <b>" +
|
|
aolxml::escapeXML((*i).second.m_password) + ":#" + tos((*i).second.m_streamID) + "</b><br><br>" : "<br>") +
|
|
"Min Bitrate Allowed: <b>" + (!minbitrate ? "Unlimited" : tos(minbitrate / 1000) + " kbps") + "</b><br>"
|
|
"Max Bitrate Allowed: <b>" + (!maxbitrate ? "Unlimited" : tos(maxbitrate / 1000) + " kbps") + "</b><br><br>"
|
|
"Protocol Mode: <b>" + ((g_legacyPort > 0) ? "v2, v1" : "v2") + "</b><br>";
|
|
}
|
|
|
|
const utf8 address = "http://" + ((!host.empty() ? host : "<enter_server_address>") + ":" + tos(gOptions.portBase())),
|
|
params = "?sid=" + tos((*i).second.m_streamID) + "&pass=" + aolxml::escapeXML((*i).second.m_password);
|
|
body += "<br><hr><br>Listener Playlist(s):<br><b>"
|
|
"<a href=\"" + address + "/listen.pls?sid=" + tos((*i).second.m_streamID) + "\">PLS</a>, "
|
|
"<a href=\"" + address + "/listen.m3u?sid=" + tos((*i).second.m_streamID) + "\">M3U</a>, "
|
|
"<a href=\"" + address + "/listen.asx?sid=" + tos((*i).second.m_streamID) + "\">ASX</a>, "
|
|
"<a href=\"" + address + "/listen.xspf?sid=" + tos((*i).second.m_streamID) + "\">XSPF</a>, "
|
|
"<a href=\"" + address + "/listen.qtl?sid=" + tos((*i).second.m_streamID) + "\">QTL</a></b>"
|
|
|
|
"<br><br>Direct Stream URL(s):<br><b>"
|
|
"<a title=\"Flash\" alt=\"Flash\" "
|
|
"href=\"" + address + getStreamPath((*i).second.m_streamID) + "?type=.flv\">"
|
|
"<img border=\"0\" style=\"vertical-align:bottom\" src=\"images/flash.png\"> FLV</a>, "
|
|
"<a title=\"HTTP / HTML5\" alt=\"HTTP / HTML5\" "
|
|
"href=\"" + address + getStreamPath((*i).second.m_streamID) + "?type=http\">"
|
|
"<img border=\"0\" style=\"vertical-align:bottom\" src=\"images/html5.png\"> HTTP</a>, "
|
|
"<a title=\"Shoutcast 1.x\" alt=\"Shoutcast 1.x\" "
|
|
"href=\"" + address + getStreamPath((*i).second.m_streamID) + "?type=sc1\">"
|
|
"<img border=\"0\" style=\"vertical-align:bottom\" src=\"images/favicon.ico\"> 1.x</a>, "
|
|
"<a title=\"Shoutcast 2.x\" alt=\"Shoutcast 2.x\" "
|
|
"href=\"" + address + getStreamPath((*i).second.m_streamID) + "?type=sc2\">"
|
|
"<img border=\"0\" style=\"vertical-align:bottom\" src=\"images/favicon.ico\"> 2.x</a></b>"
|
|
|
|
"<br><br>History:<br><b>"
|
|
"<a href=\"" + address + "/played" + params + "&type=xml\">XML</a>, "
|
|
"<a href=\"" + address + "/played" + params + "&type=json\">JSON</a>, "
|
|
"<a href=\"" + address + "/played" + params + "&type=json&callback=func\">JSON-P</a></b>"
|
|
|
|
"<br><br>Stream Statistics:<br><b>"
|
|
"<a href=\"" + address + "/stats" + params + "\">XML</a>, "
|
|
"<a href=\"" + address + "/stats" + params + "&json=1\">JSON</a>, "
|
|
"<a href=\"" + address + "/stats" + params + "&json=1&callback=func\">JSON-P</a></b><br></div>";
|
|
}
|
|
}
|
|
|
|
if (!gOptions.requireStreamConfigs())
|
|
{
|
|
const int minbitrate = gOptions.minBitrate(), maxbitrate = gOptions.maxBitrate();
|
|
body += "<div class=\"infb\" style=\"width:19.5em;word-wrap:break-word;margin: 0 0 1em 1em;\">"
|
|
"<div align=\"center\" class=\"infh\"><b>All" + (!ids.empty() ? " Other" : (utf8)"") + " Streams</b></div>"
|
|
"Stream ID: <b><any value" + (!ids.empty() ? " other than " + ids : "") + "></b><br>"
|
|
"Server Address: <b>" + aolxml::escapeXML(!host.empty() ? host : "<enter_server_address>") + "</b> <b>(<font color=\"red\">*</font>)</b><br>"
|
|
"Server Port: <b>" + tos(gOptions.portBase()) + "</b><br><br>" +
|
|
"Password: <b>" + aolxml::escapeXML(gOptions.password()) + "</b><br>" +
|
|
((g_legacyPort > 0) ? "Legacy Password: <b>" + aolxml::escapeXML(gOptions.password()) + ":#xx</b><br><br>" : "<br>") +
|
|
"Min Bitrate Allowed: <b>" + (!minbitrate ? "Unlimited" : tos(minbitrate / 1000) + " kbps") + "</b><br>"
|
|
"Max Bitrate Allowed: <b>" + (!maxbitrate ? "Unlimited" : tos(maxbitrate / 1000) + " kbps") + "</b><br><br>"
|
|
"Protocol Mode: <b>" + ((g_legacyPort > 0) ? "v2, v1" : "v2") + "</b><br>";
|
|
|
|
const utf8 address = "http://" + ((!host.empty() ? host : "<enter_server_address>") + ":" + tos(gOptions.portBase())),
|
|
params = "?sid=xx&pass=" + aolxml::escapeXML(gOptions.password());
|
|
body += "<br><hr><br>Listener Playlist(s):<br><b>"
|
|
"<a href=\"" + address + "/listen.pls?sid=xx\">PLS</a>, "
|
|
"<a href=\"" + address + "/listen.m3u?sid=xx\">M3U</a>, "
|
|
"<a href=\"" + address + "/listen.asx?sid=xx\">ASX</a>, "
|
|
"<a href=\"" + address + "/listen.xspf?sid=xx\">XSPF</a>, "
|
|
"<a href=\"" + address + "/listen.qtl?sid=xx\">QTL</a></b>"
|
|
|
|
"<br><br>History:<br><b>"
|
|
"<a href=\"" + address + "/played" + params + "&type=xml\">XML</a>, "
|
|
"<a href=\"" + address + "/played" + params + "&type=json\">JSON</a>, "
|
|
"<a href=\"" + address + "/played" + params + "&type=json&callback=func\">JSON-P</a></b>"
|
|
|
|
"<br><br>Stream Statistics:<br><b>"
|
|
"<a href=\"" + address + "/stats" + params + "\">XML</a>, "
|
|
"<a href=\"" + address + "/stats" + params + "&json=1\">JSON</a>, "
|
|
"<a href=\"" + address + "/stats" + params + "&json=1&callback=func\">JSON-P</a></b>"
|
|
"<br><br><b>Note:</b> Replace the '<b>xx</b>' in '<b>sid=xx</b>' in all of the example links "
|
|
"and in the '<b>Legacy Password</b>' with the stream number.<br></div>";
|
|
}
|
|
|
|
body += "</div>" + getUptimeScript() + getIEFlexFix() + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_adgroups() throw()
|
|
{
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
body = getServerAdminHeader("Server Advert Group Details") +
|
|
"<div style=\"padding-right:1em;\"><br>"
|
|
"<table align=\"center\" width=\"100%\"><tr valign=\"top\">"
|
|
"<td><table border=\"0\" cellspacing=\"0\" cellpadding=\"0\">"
|
|
"<tr><td valign=\"top\">"
|
|
"<div style=\"width:13em;\" class=\"infb\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Information</b></div>"
|
|
"Here you can see which active stream(s) have adverts enabled and their status of obtaining the advert details."
|
|
"<br><br>This can help to see if a listener should be receiving adverts or not by "
|
|
"checking for the listener's group id with the group ids shown for the required stream.<br><br></div>"
|
|
|
|
"</td><td valign=\"top\" width=\"100%\">"
|
|
"<table class=\"en\" style=\"border:0;text-align:center;word-wrap:break-word;\" "
|
|
"cellpadding=\"3\" cellspacing=\"0\" width=\"100%\" align=\"center\">";
|
|
|
|
const streamData::streamIDs_t streamIds = streamData::getStreamIds();
|
|
if (!streamIds.empty())
|
|
{
|
|
body += "<colgroup><col width=\"15%\"></colgroup>"
|
|
"<tr class=\"tll\"><td>Stream #</td><td>Advert Group(s)</td></tr>";
|
|
|
|
for (streamData::streamIDs_t::const_iterator i = streamIds.begin(); i != streamIds.end(); ++i)
|
|
{
|
|
streamData *sd = streamData::accessStream((*i));
|
|
if (sd)
|
|
{
|
|
if (sd->isSourceConnected((*i)))
|
|
{
|
|
body += "<tr><td><b>" + tos((*i)) + "</b></td>"
|
|
"<td style=\"max-width:0;text-align:left;\">" +
|
|
sd->getAdvertGroup() + "</td></tr>";
|
|
}
|
|
sd->releaseStream();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
body += "<tr><td style=\"border:0\">There are no active streams.</b></td></tr>";
|
|
}
|
|
body += "</table></td></tr></table></td></tr></table></div>" +
|
|
getUptimeScript() + getIEFlexFix() + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_debug(const uniString::utf8& option, const int on_off, const bool adminRefer) throw()
|
|
{
|
|
struct debug_options {
|
|
utf8 option;
|
|
utf8 desc;
|
|
utf8 msg;
|
|
bool value;
|
|
};
|
|
debug_options debug[] = {
|
|
{"Listener Connections", "", ""},
|
|
{"shoutcast1clientdebug", "Shoutcast 1.x Listener Connections", "Used to diagnose connection issues with listener connections using the legacy Shoutcast 1.x protocol"
|
|
"<br>(e.g. HTTP connections indicating they support icy-metadata).", gOptions.shoutcast1ClientDebug()},
|
|
{"shoutcast2clientdebug", "Shoutcast 2.x Listener Connections", "Used to diagnose connection issues with listener connections using the Shoutcast 2.x protocol.", gOptions.shoutcast2ClientDebug()},
|
|
{"httpclientdebug", "HTTP Listener Connections", "Used to diagnose connection issues with listener connections using the HTTP protocol.", gOptions.HTTPClientDebug()},
|
|
{"flvclientdebug", "FLV Listener Connections", "Used to diagnose connection issues with listener connections using the FLV encapsulation.", gOptions.flvClientDebug()},
|
|
#if 0
|
|
{"m4aclientdebug", "M4A Listener Connections", "Used to diagnose connection issues with listener connections using the M4A encapsulation.", gOptions.m4aClientDebug()},
|
|
#endif
|
|
{"Direct Source Connections", "", ""},
|
|
{"shoutcastsourcedebug", "Shoutcast 1.x Source Connections", "Used to diagnose connection issues with source connections using the legacy Shoutcast 1.x protocol.", gOptions.shoutcastSourceDebug()},
|
|
{"uvox2sourcedebug", "Shoutcast 2.x Source Connections", "Used to diagnose connection issues with source connections using the Shoutcast 2.x protocol.", gOptions.uvox2SourceDebug()},
|
|
{"httpsourcedebug", "HTTP Source Connections", "Used to diagnose connection issues with source connections using HTTP PUT protocol (e.g. Icecast sources).", gOptions.HTTPSourceDebug()},
|
|
{"Relay Source Connections", "", ""},
|
|
{"relayshoutcastdebug", "Shoutcast 1.x Relay Connections", "Used to diagnose connection issues with relay connections using the legacy Shoutcast 1.x protocol.", gOptions.relayShoutcastDebug()},
|
|
{"relayuvoxdebug", "Shoutcast 2.x Relay Connections", "Used to diagnose connection issues with relay connections using the Shoutcast 2.x protocol.", gOptions.relayUvoxDebug()},
|
|
{"relaydebug", "Common Relay Handling", "Used to diagnose issues with general relay handling when a relay connection begins (non-protcool specific).", gOptions.relayDebug()},
|
|
{"HTTP Connections", "", ""},
|
|
{"httpstyledebug", "HTTP Requests", "Used to inspect HTTP requests made to the server (e.g. listener or statistic requests).", gOptions.httpStyleDebug()},
|
|
{"webclientdebug", "HTTP Connections", "Used to inspect and diagnose issues with HTTP connections issued by the server.", gOptions.webClientDebug()},
|
|
{"Shoutcast Services", "", ""},
|
|
{"admetricsdebug", "Advert & Metrics Handling", "Used to inspect and diagnose issues with the advert and metric activity for client connections.", gOptions.adMetricsDebug()},
|
|
{"yp2debug", "YP / Directory Connections", "Used to diagnose failures to connect to the Directory servers or to be listed.", gOptions.yp2Debug()},
|
|
#if defined(_DEBUG) || defined(DEBUG)
|
|
// this is not enabled as we don't really want to promote the existence of this
|
|
{"authdebug", "Listener Auth Handling", "Used to diagnose issues with the client auth handling when clients connect to the stream.", gOptions.authDebug()},
|
|
#endif
|
|
{"Miscellaneous", "", ""},
|
|
{"streamdatadebug", "Common Stream Handling", "Used to diagnose issues with general streaming code (non-protcool specific).", gOptions.streamDataDebug()},
|
|
{"statsdebug", "Client Statistics", "Used to inspect client statistics tracked by the server.", gOptions.statsDebug()},
|
|
{"microserverdebug", "Common Server Activity", "Used to diagnose and track common server activity.", gOptions.microServerDebug()},
|
|
{"threadrunnerdebug", "Thread Manager", "Used to diagnose and track the processing of threads by the thread manager.", gOptions.threadRunnerDebug()},
|
|
};
|
|
|
|
if (option.empty())
|
|
{
|
|
utf8 header = "HTTP/1.1 200 OK\r\n"
|
|
"Cache-Control:no-cache\r\n"
|
|
"Content-Type:text/html;charset=utf-8\r\n",
|
|
body = getServerAdminHeader("Server Debugging Options") +
|
|
"<div><br>"
|
|
"<table align=\"center\" width=\"100%\"><tr valign=\"top\">"
|
|
"<td><table border=\"0\" cellspacing=\"0\" cellpadding=\"0\">"
|
|
"<tr valign=\"top\"><td>"
|
|
"<div style=\"max-width:15em;\" class=\"infb\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Information</b></div>"
|
|
"Here you can find are all debugging options provided by the "
|
|
"DNAS server.<br><br>Changing the debugging options will update "
|
|
"the value saved in the DNAS server configuration file(s) as "
|
|
"needed.<br><br>Any changes are applied immediately.<br><br><hr><br>"
|
|
|
|
"<div align=\"left\"><b>All Options</b><br><br></div>"
|
|
"<input class=\"submit\" type=\"button\" onclick=\"toggle('all','true',1);\" name=\"all\" id=\"all\" value=\"Enable\">"
|
|
" <input class=\"submit\" type=\"button\" onclick=\"toggle('all','false',1);\" name=\"all\" id=\"all\" value=\"Disable\">"
|
|
"<br><br><hr><br>"
|
|
|
|
"<div align=\"left\"><b>Listener Options</b><br><br></div>"
|
|
"<input class=\"submit\" type=\"button\" onclick=\"toggle('listener','true',2);\" name=\"listener\" id=\"listener\" value=\"Enable\">"
|
|
" <input class=\"submit\" type=\"button\" onclick=\"toggle('listener','false',2);\" name=\"listener\" id=\"listener\" value=\"Disable\">"
|
|
"<br><br><hr><br>"
|
|
|
|
"<div align=\"left\"><b>Source Options</b><br><br></div>"
|
|
"<input class=\"submit\" type=\"button\" onclick=\"toggle('source','true',3);\" name=\"source\" id=\"sources\" value=\"Enable\">"
|
|
" <input class=\"submit\" type=\"button\" onclick=\"toggle('source','false',3);\" name=\"source\" id=\"source\" value=\"Disable\">"
|
|
"<br><br><hr><br>"
|
|
|
|
"<div align=\"left\"><b>Force Short Sends</b> <div id=\"forceshortsendsval\" "
|
|
"style=\"display:inline-block\">[" + (gOptions.forceShortSends() ? "Enabled" :
|
|
"Disabled") + "]</div><br><br>This inserts delays into sending stream(s)"
|
|
"to replicate network limiting and bandwidth issues (Default: <b>Disabled</b>).<br><br></div>"
|
|
"<input class=\"submit\" type=\"button\" onclick=\"toggle('forceshortsends','true',4);\" name=\"forceshortsends\" id=\"forceshortsends\" value=\"Enable\">"
|
|
" <input class=\"submit\" type=\"button\" onclick=\"toggle('forceshortsends','false',4);\" name=\"forceshortsends\" id=\"forceshortsends\" value=\"Disable\">"
|
|
"<br><br><hr><br>"
|
|
|
|
"<div align=\"left\"><b>Rate Limiting</b> <div "
|
|
"id=\"ratelimitval\" style=\"display:inline-block\">"
|
|
"[" + (gOptions.rateLimit() ? "Enabled" : "Disabled") +
|
|
"]</div><br><br>Controls the rate of output to even "
|
|
"it out and prefer sending larger blocks in one go instead of "
|
|
"doing smaller blocks more often (Default: <b>Enabled</b>).<br><br></div>"
|
|
"<input class=\"submit\" type=\"button\" onclick=\"toggle('ratelimit','true',5);\" name=\"ratelimit\" id=\"ratelimit\" value=\"Enable\">"
|
|
" <input class=\"submit\" type=\"button\" onclick=\"toggle('ratelimit','false',5);\" name=\"ratelimit\" id=\"ratelimit\" value=\"Disable\">"
|
|
|
|
"</div><td>";
|
|
|
|
for (size_t i = 0; i < sizeof(debug) / sizeof(debug[0]); i++)
|
|
{
|
|
if (!debug[i].desc.empty())
|
|
{
|
|
body += "<input autocomplete=\"off\" type=\"checkbox\" onclick=\"toggle('" +
|
|
debug[i].option + "',$('" + debug[i].option +
|
|
"').checked,0)\" name=\"" + debug[i].option +
|
|
"\" id=\"" + debug[i].option + "\"" +
|
|
(debug[i].value ? " checked=\"true\"" : " ") + " value=\"" +
|
|
(debug[i].value ? "1" : "0") + "\">" "<label for=\"" +
|
|
debug[i].option + "\" title=\"" + debug[i].option +
|
|
"\" style=\"padding-left: 7px;\"><b>" + debug[i].desc + "</b></label>"
|
|
"<div style=\"padding:5px 0 0 27px;\">" + debug[i].msg + "</div><br>";
|
|
}
|
|
else
|
|
{
|
|
body += (i ? "<br>" : (utf8)"") + "<b><u>" + debug[i].option + "</u>:</b><br><br>";
|
|
}
|
|
}
|
|
|
|
body += "</td></tr></table></td></tr></table></div>"
|
|
+ getUptimeScript() +
|
|
"<script type=\"text/javascript\">" EL
|
|
"function toggle(opt, on, mode) {" EL
|
|
"if(window.XMLHttpRequest){" EL
|
|
"xmlhttp=new XMLHttpRequest();" EL
|
|
"}else{" EL
|
|
"xmlhttp=new ActiveXObject(\"Microsoft.XMLHTTP\");" EL
|
|
"}" EL
|
|
"xmlhttp.open(\"GET\",\"admin.cgi?pass="+gOptions.adminPassword()+"&mode=debug&option=\"+opt+\"&on=\"+on,true);" EL
|
|
"xmlhttp.send(null);" EL
|
|
"if(mode == 1){" EL
|
|
"var labels = document.getElementsByTagName('LAB EL');" EL
|
|
"for (var i = 0; i < labels.length; i++) {" EL
|
|
"if (labels[i].htmlFor != '') {" EL
|
|
"$(labels[i].htmlFor).checked=(on=='true'?'true':'');"
|
|
"}" EL
|
|
"}" EL
|
|
"}" EL
|
|
"else if(mode == 2){" EL
|
|
"var labels = ['shoutcast1clientdebug', 'shoutcast2clientdebug', 'httpclientdebug', 'flvclientdebug'/*, 'm4aclientdebug'*/];" EL
|
|
"for (var i = 0; i < labels.length; i++) {" EL
|
|
"$(labels[i]).checked=(on=='true'?'true':'');"
|
|
"}" EL
|
|
"}" EL
|
|
"else if(mode == 3){" EL
|
|
"var labels = ['shoutcastsourcedebug', 'uvox2sourcedebug', 'httpsourcedebug', 'relayshoutcastdebug', 'relayuvoxdebug', 'relaydebug'];" EL
|
|
"for (var i = 0; i < labels.length; i++) {" EL
|
|
"$(labels[i]).checked=(on=='true'?'true':'');"
|
|
"}" EL
|
|
"}" EL
|
|
"else if(mode == 4){" EL
|
|
"$('forceshortsendsval').innerHTML=(on=='true'?'[Enabled]':'[Disabled]');" EL
|
|
"}" EL
|
|
"else if(mode == 5){" EL
|
|
"$('ratelimitval').innerHTML=(on=='true'?'[Enabled]':'[Disabled]');" EL
|
|
"}" EL
|
|
"}</script>" + getIEFlexFix() + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
else
|
|
{
|
|
if (option == "all")
|
|
{
|
|
ILOG(LOGNAME + utf8(on_off ? "Enabling" : "Disabling") + " all debugging options");
|
|
for (size_t i = 0; i < sizeof(debug) / sizeof(debug[0]); i++)
|
|
{
|
|
if (!debug[i].desc.empty())
|
|
{
|
|
// always set even if the saving fails so we've got something working
|
|
try
|
|
{
|
|
gOptions.setOption(debug[i].option, utf8(tos(on_off)));
|
|
}
|
|
catch(const exception &)
|
|
{
|
|
}
|
|
|
|
bool handled = false, idHandled = false;
|
|
// if we get a clear then just remove from the config and reload the page
|
|
if (gOptions.editConfigFileEntry(0, gOptions.confFile(), debug[i].option, tos(on_off),
|
|
true, handled, idHandled, true) == false)
|
|
{
|
|
ELOG(LOGNAME "Error saving debug option: `" + debug[i].option + "'");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (option == "listener")
|
|
{
|
|
ILOG(LOGNAME + utf8(on_off ? "Enabling" : "Disabling") + " all listener debugging options");
|
|
const utf8 options[] = {"shoutcast1clientdebug", "shoutcast2clientdebug", "httpclientdebug", "flvclientdebug"/*, "m4aclientdebug"*/};
|
|
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++)
|
|
{
|
|
// always set even if the saving fails so we've got something working
|
|
try
|
|
{
|
|
gOptions.setOption(options[i], utf8(tos(on_off)));
|
|
}
|
|
catch(const exception &)
|
|
{
|
|
}
|
|
|
|
bool handled = false, idHandled = false;
|
|
// if we get a clear then just remove from the config and reload the page
|
|
if (gOptions.editConfigFileEntry(0, gOptions.confFile(), options[i], tos(on_off),
|
|
true, handled, idHandled, true) == false)
|
|
{
|
|
ELOG(LOGNAME "Error saving debug option: `" + options[i] + "'");
|
|
}
|
|
}
|
|
}
|
|
else if (option == "source")
|
|
{
|
|
ILOG(LOGNAME + utf8(on_off ? "Enabling" : "Disabling") + " all source debugging options");
|
|
const utf8 options[] = {"shoutcastsourcedebug", "uvox2sourcedebug", "httpsourcedebug",
|
|
"relayshoutcastdebug", "relayuvoxdebug", "relaydebug"};
|
|
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++)
|
|
{
|
|
// always set even if the saving fails so we've got something working
|
|
try
|
|
{
|
|
gOptions.setOption(options[i], utf8(tos(on_off)));
|
|
}
|
|
catch(const exception &)
|
|
{
|
|
}
|
|
|
|
bool handled = false, idHandled = false;
|
|
// if we get a clear then just remove from the config and reload the page
|
|
if (gOptions.editConfigFileEntry(0, gOptions.confFile(), options[i], tos(on_off),
|
|
true, handled, idHandled, true) == false)
|
|
{
|
|
ELOG(LOGNAME "Error saving debug option: `" + options[i] + "'");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ILOG(LOGNAME + utf8(on_off ? "Enabling" : "Disabling") + " the `" + option + "' debugging options");
|
|
|
|
// always set even if the saving fails so we've got something working
|
|
gOptions.setOption(option, utf8(tos(on_off)));
|
|
|
|
bool handled = false, idHandled = false;
|
|
// if we get a clear then just remove from the config and reload the page
|
|
if (gOptions.editConfigFileEntry(0, gOptions.confFile(), option, tos(on_off),
|
|
true, handled, idHandled, true) == false)
|
|
{
|
|
ELOG(LOGNAME "Error saving debug option: `" + option + "'");
|
|
}
|
|
}
|
|
|
|
if (adminRefer)
|
|
{
|
|
sendMessageAndClose(redirect("admin.cgi", SHRINK));
|
|
}
|
|
else
|
|
{
|
|
sendMessageAndClose(MSG_200);
|
|
}
|
|
}
|
|
}
|
|
|
|
void protocol_admincgi::mode_help() throw()
|
|
{
|
|
utf8 libs = "<u>Supporting Libraries</u>:<br><br>";
|
|
std::vector<uniString::utf8> parts = tokenizer(utf8(curl_version()), ' ');
|
|
for (vector<uniString::utf8>::const_iterator i = parts.begin(); i != parts.end(); ++i)
|
|
{
|
|
utf8::size_type pos = (*i).find('/');
|
|
if (pos != utf8::npos)
|
|
{
|
|
libs += (*i).substr(0, pos) + utf8(": <b>") + (*i).substr(pos + 1) + utf8("</b>");
|
|
}
|
|
else
|
|
{
|
|
libs += "<b>" + (*i) + "</b>";
|
|
}
|
|
|
|
|
|
if ((*i).find((utf8)"libcurl") != utf8::npos)
|
|
{
|
|
libs += " (<a target=\"_blank\" href=\"http://curl.haxx.se/\"><b>site</b></a>)";
|
|
}
|
|
else if ((*i).find((utf8)"OpenSSL") != utf8::npos)
|
|
{
|
|
libs += " (<a target=\"_blank\" href=\"http://www.openssl.org/\"><b>site</b></a>)";
|
|
}
|
|
else if ((*i).find((utf8)"zlib") != utf8::npos)
|
|
{
|
|
libs += " (<a target=\"_blank\" href=\"http://www.zlib.net/\"><b>site</b></a>)";
|
|
}
|
|
libs += "<br>";
|
|
}
|
|
|
|
const int cpu_count = gOptions.getCPUCount();
|
|
const utf8 cpu = "CPU Count: <b>" + tos(cpucount()) + "</b> -> " +
|
|
(cpu_count == cpucount() ? "using " + utf8(cpu_count > 1 ? "all" : "the") +
|
|
" available CPU" + (cpu_count > 1 ? "s" : "") : tos(cpu_count) +
|
|
" specified to be used") + "<br><br>";
|
|
|
|
XML_Expat_Version expat = XML_ExpatVersionInfo();
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
body = getServerAdminHeader("Server Help & Documentation") +
|
|
|
|
"<div><br>"
|
|
|
|
"<div style=\"width:19em;margin: 0 0 1em 1em;\" class=\"infb\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Help</b></div>"
|
|
"If you are having issues, you should first try to contact your "
|
|
"hosting provider.<br><br>If running the DNAS server yourself (or if instructed "
|
|
"to do so), then you can use our <a target=\"_blank\" "
|
|
"href=\"http://forums.shoutcast.com/forumdisplay.php?f=140\"><b>forum</b></a> "
|
|
"or you can contact <a href=\"mailto:support@shoutcast.com?subject=Shoutcast%20Support\">"
|
|
"<b>Shoutcast support</b></a> directly (e.g. if the issue relates to an "
|
|
"account / authhash issue).<br><br><b>Note:</b> If using the <a target=\"_blank\" "
|
|
"href=\"http://forums.shoutcast.com/forumdisplay.php?f=140\"><b>forum</b></a>, "
|
|
"do not post any information which could be used to compromise "
|
|
"your account / authhash / etc (e.g. passwords and authhash(s)).<br><br></div>"
|
|
|
|
"<div style=\"width:19em;margin: 0 0 1em 1em;\" class=\"infb\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Documentation</b></div>"
|
|
"Supporting documentation for using the DNAS server from setup "
|
|
"to getting statistics from the server are <a target=\"_blank\" "
|
|
"href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_Broadcaster\"><b>online</b></a>.<br><br>"
|
|
"Otherwise a local copy can usually be found on the host machine at:<br><br>"
|
|
"<div style=\"word-break:break-all;\"><b>" + aolxml::escapeXML(gStartupDirectory) +
|
|
"</b></div><br><b>Note:</b> The local copy is usually correct for the version being "
|
|
"used and the <a target=\"_blank\" href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_Broadcaster\">"
|
|
"<b>online</b></a> version will be for the most recent release.<br><br></div>"
|
|
|
|
"<div style=\"width:19em;margin: 0 0 1em 1em;\" class=\"infb\">"
|
|
"<div align=\"center\" class=\"infh\"><b>About</b></div>"
|
|
"Information about the DNAS server and the supporting libraries currently used.<br><br>"
|
|
"Version: <b>" + addWBR(gOptions.getVersionBuildStrings()) + "</b><br>"
|
|
"Platform: <b>" + SERV_OSNAME "</b><br>"
|
|
"Built: <b>" __DATE__"</b><br><br>"
|
|
+ libs + // libcurl, openssl, zlib
|
|
"expat: <b>" + tos(expat.major) + "." + tos(expat.minor) + "." + tos(expat.micro) + "</b>"
|
|
" (<a target=\"_blank\" href=\"http://www.libexpat.org/\"><b>site</b></a>)"
|
|
//#ifdef _WIN32
|
|
//"<br>pthread-win32: <b>" PTW32_VERSION_STR "-mod</b>"
|
|
//" (<a target=\"_blank\" href=\"https://github.com/GerHobbelt/pthread-win32\"><b>site</b></a>)"
|
|
//#endif
|
|
"<br><br></div></div>"
|
|
|
|
"<div style=\"width:19em;margin: 0 0 1em 1em;\" class=\"infb\"><div "
|
|
"align=\"center\" class=\"infh\"><b>Diagnostics</b></div>" + cpu +
|
|
"<u>Current thread & runner usage</u>:<br><br>" +
|
|
threadedRunner::getRunnabledetails() + "<br></div></div>"
|
|
|
|
+ getUptimeScript() + getIEFlexFix() + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
void protocol_admincgi::mode_config() throw()
|
|
{
|
|
utf8 conf = gOptions.confFile();
|
|
utf8::size_type pos = conf.rfind(fileUtil::getFilePathDelimiter());
|
|
if ((pos != utf8::npos))
|
|
{
|
|
conf = conf.substr(pos + 1);
|
|
}
|
|
|
|
utf8 header = MSG_NO_CLOSE_200,
|
|
body = getServerAdminHeader("Server Configuration Settings") +
|
|
"<div><div style=\"padding:1em;\"><b>This page shows the custom configuration settings "
|
|
"that this DNAS server is currently using. (Settings which match DNAS server defaults "
|
|
"may not be shown.)<br><br><u>Note #1:</u> To change these values, you will need to edit the "
|
|
"<u title=\"" + aolxml::escapeXML(fileUtil::getFullFilePath(gOptions.confFile())) + "\">" +
|
|
(!conf.empty() ? conf : "sc_serv.conf") + "</u> file on your server. See <a target=\"_blank\" "
|
|
"href=\"http://wiki.shoutcast.com/wiki/SHOUTcast_DNAS_Server_2#Configuration_File\">here</a> "
|
|
"for more information.<br><br><u>Note #2:</u> This is not the same as the actual configuration file "
|
|
"(i.e. the structure of the configuration file(s) being used is not shown)</b></div>" +
|
|
gOptions.dumpConfigFile() + "<br></div>" + getUptimeScript() + getIEFlexFix() + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
|
|
#if 0
|
|
void protocol_admincgi::mode_logs() throw()
|
|
{
|
|
utf8 header = "HTTP/1.1 200 OK\r\n"
|
|
"Cache-Control:no-cache\r\n"
|
|
"Content-Type:text/html;charset=utf-8\r\n",
|
|
headerTitle = "Server Log Management", childPage = "";
|
|
|
|
bool bwstyle = false;
|
|
int refreshRequired = 0;
|
|
utf8 body = s_serverAdminHeading;
|
|
|
|
body += "<div style=\"padding:0 1em;\"><br>"
|
|
"<table align=\"center\" width=\"100%\"><tr valign=\"top\">"
|
|
"<td><table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"padding-left:1em;\">"
|
|
"<tr valign=\"top\"><td>"
|
|
"<div style=\"width:215px;\" class=\"infb\">"
|
|
"<div align=\"center\" class=\"infh\"><b>Information</b></div>"
|
|
"Here you can find are all debugging options provided by the DNAS server.<br><br>Changing the debugging "
|
|
"options will update the value saved in the DNAS server configuration file(s) as needed.<br><br>"
|
|
"Any changes are applied immediately.<br><br></div>"
|
|
|
|
"<td>";
|
|
|
|
utf8 logfile = gOptions.realLogFile();
|
|
|
|
body += "<div style=\"padding-bottom:5px;\">Current log file:</div>";
|
|
body += "<div class=\"en\" style=\"padding-left:27px;padding:10px;margin-bottom:1em;"
|
|
"display:inline-block;border-radius:5px;\"><b>" +
|
|
aolxml::escapeXML(fileUtil::stripPath(logfile)) + "</b>"
|
|
" <a href=\"admin.cgi?mode=viewlog&server=1\">View</a>"
|
|
" <a href=\"admin.cgi?mode=viewlog&server=1&viewlog=save\">Save</a>"
|
|
"<br>Size: <b>" + formatSizeString(uniFile::fileSize(logfile)) + "</b></div><br>";
|
|
|
|
utf8 rotated = fileUtil::stripSuffix(logfile) + "_*." + fileUtil::getSuffix(logfile);
|
|
body += "<div style=\"padding-bottom:5px;\">Older log file(s):</div>";
|
|
body += "<div>";
|
|
#ifdef _WIN32
|
|
vector<wstring> fileList = fileUtil::directoryFileList(utf8(fileUtil::getFullFilePath(rotated)).toWString(), L"", true, true);
|
|
#else
|
|
vector<string> fileList = fileUtil::directoryFileList(fileUtil::getFullFilePath(rotated), "");
|
|
#endif
|
|
if (!fileList.empty())
|
|
{
|
|
#ifdef _WIN32
|
|
for (vector<wstring>::const_iterator i = fileList.begin(); i != fileList.end(); ++i)
|
|
#else
|
|
for (vector<string>::const_iterator i = fileList.begin(); i != fileList.end(); ++i)
|
|
#endif
|
|
{
|
|
#ifdef _WIN32
|
|
utf32 u32file(*i);
|
|
utf8 u8f(u32file.toUtf8());
|
|
body += "<div class=\"en\" style=\"padding-left:27px;padding:10px;margin-bottom:1em;"
|
|
"display:inline-block;border-radius:5px;\"><b>" + fileUtil::stripPath(u8f) + "</b>"
|
|
"<div style=\"float:right;\"> <a href=\"admin.cgi?mode=viewlog&server=1&file=" +
|
|
urlUtils::escapeURI_RFC3986(u8f) + "\">View</a> "
|
|
"<a href=\"admin.cgi?mode=viewlog&server=1&viewlog=save&file=" +
|
|
urlUtils::escapeURI_RFC3986(u8f) + "\">Save</a></div><br>Size: <b>" +
|
|
formatSizeString(uniFile::fileSize(u8f)) + "</b>"
|
|
"<br>Last Modified: <b>" + getRFCDate(uniFile::fileTime(u8f)) + "</b></div><br>";
|
|
#else
|
|
body += "<div style=\"padding-left:27px;\"><b>" + fileUtil::stripPath(*i) + "</b> "
|
|
"<a href=\"admin.cgi?mode=viewlog&server=1&viewlog=save&file="+urlUtils::escapeURI_RFC3986(*i)+"\">Save</a></div>";
|
|
#endif
|
|
}
|
|
}
|
|
body += "</div>";
|
|
#if 0
|
|
utf8 archived = fileUtil::stripSuffix(logfile) + "_*.gz";
|
|
body += "<div style=\"padding:5px 0 0 27px;\"><b>Log file (archived):</b> "/* + aolxml::escapeXML(fileUtil::getFullFilePath(archived)) + */"</div><br>";
|
|
body += "<div>";
|
|
#ifdef _WIN32
|
|
fileList = fileUtil::directoryFileList(utf8(fileUtil::getFullFilePath(archived)).toWString(), L"", true, true);
|
|
#else
|
|
fileList = fileUtil::directoryFileList(fileUtil::getFullFilePath(archived), "");
|
|
#endif
|
|
if (!fileList.empty())
|
|
{
|
|
#ifdef _WIN32
|
|
for (vector<wstring>::const_iterator i = fileList.begin(); i != fileList.end(); ++i)
|
|
#else
|
|
for (vector<string>::const_iterator i = fileList.begin(); i != fileList.end(); ++i)
|
|
#endif
|
|
{
|
|
#ifdef _WIN32
|
|
utf32 u32file(*i);
|
|
utf8 u8f(u32file.toUtf8());
|
|
body += "<div style=\"padding-left:27px;\">" + fileUtil::stripPath(u8f) + "</div>";
|
|
#else
|
|
body += "<div style=\"padding-left:27px;\">" + fileUtil::stripPath(*i) + "</div>";
|
|
#endif
|
|
}
|
|
}
|
|
body += "</div>";
|
|
|
|
body += "<hr>";
|
|
|
|
utf8 w3cfile = gOptions.w3cLog();
|
|
body += "<div style=\"padding:5px 0 0 27px;\">W3C file: " + aolxml::escapeXML(fileUtil::getFullFilePath(w3cfile)) + "</div><br>";
|
|
utf8 archived = fileUtil::stripSuffix(w3cfile) + "_*.gz";
|
|
body += "<div style=\"padding:5px 0 0 27px;\">W3C file (archived): " + aolxml::escapeXML(fileUtil::getFullFilePath(archived)) + "</div><br>";
|
|
archived = fileUtil::stripSuffix(w3cfile) + "_*_w3c.gz";
|
|
body += "<div style=\"padding:5px 0 0 27px;\">W3C file (archived 2): " + aolxml::escapeXML(fileUtil::getFullFilePath(archived)) + "</div><br>";
|
|
body += "<div>";
|
|
config::streams_t streams;
|
|
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))
|
|
{
|
|
body += "<div style=\"padding:5px 0 0 27px;\">Stream W3C file: " + aolxml::escapeXML(fileUtil::getFullFilePath(gOptions.stream_w3cLog((*i).first))) + "</div><br>";
|
|
}
|
|
}
|
|
body += "</div>";
|
|
body += "<hr>";
|
|
body += "<div>";
|
|
#ifdef _WIN32
|
|
fileList = fileUtil::directoryFileList(gStartupDirectory.toWString() + L"sc_w3c*.log", L"", true, true);
|
|
#else
|
|
fileList = fileUtil::directoryFileList(gStartupDirectory.hideAsString() + "sc_w3c*.log", "");
|
|
#endif
|
|
if (!fileList.empty())
|
|
{
|
|
#ifdef _WIN32
|
|
for (vector<wstring>::const_iterator i = fileList.begin(); i != fileList.end(); ++i)
|
|
#else
|
|
for (vector<string>::const_iterator i = fileList.begin(); i != fileList.end(); ++i)
|
|
#endif
|
|
{
|
|
#ifdef _WIN32
|
|
utf32 u32file(*i);
|
|
utf8 u8f(u32file.toUtf8());
|
|
body += "<div style=\"padding-left:27px;\">" + fileUtil::stripPath(u8f) + "</div>";
|
|
#else
|
|
body += "<div style=\"padding-left:27px;\">" + fileUtil::stripPath(*i) + "</div>";
|
|
#endif
|
|
}
|
|
}
|
|
body += "</div>";
|
|
#endif
|
|
body += "</td></tr></table></td></tr></table></div>"
|
|
+ getUptimeScript() +
|
|
"<script type=\"text/javascript\">" EL
|
|
"function toggle(opt, on, all) {" EL
|
|
"if(window.XMLHttpRequest){" EL
|
|
"xmlhttp=new XMLHttpRequest();" EL
|
|
"}else{" EL
|
|
"xmlhttp=new ActiveXObject(\"Microsoft.XMLHTTP\");" EL
|
|
"}" EL
|
|
"xmlhttp.open(\"GET\",\"admin.cgi?pass="+gOptions.adminPassword()+"&mode=debug&option=\"+opt+\"&on=\"+on,true);" EL
|
|
"xmlhttp.send(null);" EL
|
|
"if(all){" EL
|
|
"var labels = document.getElementsByTagName('LAB EL');" EL
|
|
"for (var i = 0; i < labels.length; i++) {" EL
|
|
"if (labels[i].htmlFor != '') {" EL
|
|
"$(labels[i].htmlFor).checked=(on=='true'?'true':'');"
|
|
"}" EL
|
|
"}" EL
|
|
"}" EL
|
|
"}</script>" + getfooterStr();
|
|
|
|
COMPRESS(header, body);
|
|
header += "Content-Length: " + tos(body.size()) + "\r\n\r\n";
|
|
sendMessageAndClose(header + (!HEAD_REQUEST ? body : ""));
|
|
}
|
|
#endif
|