mistserver/src/session.cpp
2023-04-10 14:15:00 +02:00

430 lines
17 KiB
C++

#include <mist/defines.h>
#include <mist/stream.h>
#include <mist/util.h>
#include <mist/config.h>
#include <mist/auth.h>
#include <mist/comms.h>
#include <mist/triggers.h>
#include <signal.h>
#include <stdio.h>
// Global counters
uint64_t thisType = 0;
uint64_t now = Util::bootSecs();
uint64_t currentConnections = 0;
uint64_t lastSecond = 0;
uint64_t globalTime = 0;
uint64_t globalDown = 0;
uint64_t globalUp = 0;
uint64_t globalPktcount = 0;
uint64_t globalPktloss = 0;
uint64_t globalPktretrans = 0;
// Stores last values of each connection
std::map<size_t, uint64_t> connTime;
std::map<size_t, uint64_t> connDown;
std::map<size_t, uint64_t> connUp;
std::map<size_t, uint64_t> connPktcount;
std::map<size_t, uint64_t> connPktloss;
std::map<size_t, uint64_t> connPktretrans;
// Counts the duration a connector has been active
std::map<std::string, uint64_t> connectorCount;
std::map<std::string, uint64_t> connectorLastActive;
std::map<std::string, uint64_t> hostCount;
std::map<std::string, uint64_t> hostLastActive;
std::map<std::string, uint64_t> streamCount;
std::map<std::string, uint64_t> streamLastActive;
// Set to True when a session gets invalidated, so that we know to run a new USER_NEW trigger
bool forceTrigger = false;
void handleSignal(int signum){
if (signum == SIGUSR1){
forceTrigger = true;
}
}
const char nullAddress[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
void userOnActive(Comms::Connections &connections, size_t idx){
uint64_t lastUpdate = connections.getNow(idx);
if (lastUpdate < now - 10 && thisType != 1){return;}
++currentConnections;
std::string thisConnector = connections.getConnector(idx);
std::string thisStreamName = connections.getStream(idx);
const std::string& thisHost = connections.getHost(idx);
if (connections.getLastSecond(idx) > lastSecond){lastSecond = connections.getLastSecond(idx);}
// Save info on the latest active stream, protocol and host separately
if (thisConnector.size() && thisConnector != "HTTP"){
connectorCount[thisConnector]++;
if (connectorLastActive[thisConnector] < lastUpdate){connectorLastActive[thisConnector] = lastUpdate;}
}
if (thisStreamName.size()){
streamCount[thisStreamName]++;
if (streamLastActive[thisStreamName] < lastUpdate){streamLastActive[thisStreamName] = lastUpdate;}
}
if (memcmp(thisHost.data(), nullAddress, 16)){
hostCount[thisHost]++;
if (!hostLastActive.count(thisHost) || hostLastActive[thisHost] < lastUpdate){hostLastActive[thisHost] = lastUpdate;}
}
// Sanity checks
if (connections.getDown(idx) < connDown[idx]){
WARN_MSG("Connection downloaded bytes should be a counter, but has decreased in value");
connDown[idx] = connections.getDown(idx);
}
if (connections.getUp(idx) < connUp[idx]){
WARN_MSG("Connection uploaded bytes should be a counter, but has decreased in value");
connUp[idx] = connections.getUp(idx);
}
if (connections.getPacketCount(idx) < connPktcount[idx]){
WARN_MSG("Connection packet count should be a counter, but has decreased in value");
connPktcount[idx] = connections.getPacketCount(idx);
}
if (connections.getPacketLostCount(idx) < connPktloss[idx]){
WARN_MSG("Connection packet loss count should be a counter, but has decreased in value");
connPktloss[idx] = connections.getPacketLostCount(idx);
}
if (connections.getPacketRetransmitCount(idx) < connPktretrans[idx]){
WARN_MSG("Connection packets retransmitted should be a counter, but has decreased in value");
connPktretrans[idx] = connections.getPacketRetransmitCount(idx);
}
// Add increase in stats to global stats
globalDown += connections.getDown(idx) - connDown[idx];
globalUp += connections.getUp(idx) - connUp[idx];
globalPktcount += connections.getPacketCount(idx) - connPktcount[idx];
globalPktloss += connections.getPacketLostCount(idx) - connPktloss[idx];
globalPktretrans += connections.getPacketRetransmitCount(idx) - connPktretrans[idx];
// Set last values of this connection
connTime[idx]++;
connDown[idx] = connections.getDown(idx);
connUp[idx] = connections.getUp(idx);
connPktcount[idx] = connections.getPacketCount(idx);
connPktloss[idx] = connections.getPacketLostCount(idx);
connPktretrans[idx] = connections.getPacketRetransmitCount(idx);
}
/// \brief Remove mappings of inactive connections
void userOnDisconnect(Comms::Connections & connections, size_t idx){
connTime.erase(idx);
connDown.erase(idx);
connUp.erase(idx);
connPktcount.erase(idx);
connPktloss.erase(idx);
connPktretrans.erase(idx);
}
int main(int argc, char **argv){
Comms::Sessions sessions;
uint64_t lastSeen = Util::bootSecs();
Util::redirectLogsIfNeeded();
signal(SIGUSR1, handleSignal);
// Init config and parse arguments
Util::Config config = Util::Config("MistSession");
JSON::Value option;
char * tmpStr = 0;
option.null();
option["arg_num"] = 1;
option["arg"] = "string";
option["help"] = "Session identifier of the entire session";
config.addOption("sessionid", option);
option.null();
option["long"] = "streamname";
option["short"] = "s";
option["arg"] = "string";
option["help"] = "Stream name initial value. May also be passed as SESSION_STREAM";
tmpStr = getenv("SESSION_STREAM");
option["default"] = tmpStr?tmpStr:"";
config.addOption("streamname", option);
option.null();
option["long"] = "ip";
option["short"] = "i";
option["arg"] = "string";
option["help"] = "IP address initial value. May also be passed as SESSION_IP";
tmpStr = getenv("SESSION_IP");
option["default"] = tmpStr?tmpStr:"";
config.addOption("ip", option);
option.null();
option["long"] = "tkn";
option["short"] = "t";
option["arg"] = "string";
option["help"] = "Client-side session ID initial value. May also be passed as SESSION_TKN";
tmpStr = getenv("SESSION_TKN");
option["default"] = tmpStr?tmpStr:"";
config.addOption("tkn", option);
option.null();
option["long"] = "protocol";
option["short"] = "p";
option["arg"] = "string";
option["help"] = "Protocol initial value. May also be passed as SESSION_PROTOCOL";
tmpStr = getenv("SESSION_PROTOCOL");
option["default"] = tmpStr?tmpStr:"";
config.addOption("protocol", option);
option.null();
option["long"] = "requrl";
option["short"] = "r";
option["arg"] = "string";
option["help"] = "Request URL initial value. May also be passed as SESSION_REQURL";
tmpStr = getenv("SESSION_REQURL");
option["default"] = tmpStr?tmpStr:"";
config.addOption("requrl", option);
config.activate();
if (!(config.parseArgs(argc, argv))){
config.printHelp(std::cout);
FAIL_MSG("Cannot start a new session due to invalid arguments");
return 1;
}
const uint64_t bootTime = Util::getMicros();
// Get session ID, session mode and other variables used as payload for the USER_NEW and USER_END triggers
const std::string thisStreamName = config.getString("streamname");
const std::string thisToken = config.getString("tkn");
const std::string thisProtocol = config.getString("protocol");
const std::string thisReqUrl = config.getString("requrl");
const std::string thisSessionId = config.getString("sessionid");
std::string thisHost = Socket::getBinForms(config.getString("ip"));
if (thisHost.size() > 16){thisHost = thisHost.substr(0, 16);}
std::string ipHex;
Socket::hostBytesToStr(thisHost.c_str(), thisHost.size(), ipHex);
VERYHIGH_MSG("Starting a new session. Passed variables are stream name '%s', session token '%s', protocol '%s', requested URL '%s', IP '%s' and session id '%s'",
thisStreamName.c_str(), thisToken.c_str(), thisProtocol.c_str(), thisReqUrl.c_str(), ipHex.c_str(), thisSessionId.c_str());
// Try to lock to ensure we are the only process initialising this session
IPC::semaphore sessionLock;
char semName[NAME_BUFFER_SIZE];
snprintf(semName, NAME_BUFFER_SIZE, SEM_SESSION, thisSessionId.c_str());
sessionLock.open(semName, O_CREAT | O_RDWR, ACCESSPERMS, 1);
// If the lock fails, the previous Session process must've failed in spectacular fashion
// It's the Controller's task to clean everything up. When the lock fails, this cleanup hasn't happened yet
if (!sessionLock.tryWaitOneSecond()){
FAIL_MSG("Session '%s' already locked", thisSessionId.c_str());
return 1;
}
// Check if a page already exists for this session ID. If so, quit
{
IPC::sharedPage dataPage;
char userPageName[NAME_BUFFER_SIZE];
snprintf(userPageName, NAME_BUFFER_SIZE, COMMS_SESSIONS, thisSessionId.c_str());
dataPage.init(userPageName, 0, false, false);
if (dataPage){
INFO_MSG("Session '%s' already has a running process", thisSessionId.c_str());
sessionLock.post();
return 0;
}
}
// Claim a spot in shared memory for this session on the global statistics page
sessions.reload();
if (!sessions){
FAIL_MSG("Unable to register entry for session '%s' on the stats page", thisSessionId.c_str());
sessionLock.post();
return 1;
}
// Initialise global session data
sessions.setHost(thisHost);
sessions.setSessId(thisSessionId);
sessions.setStream(thisStreamName);
if (thisProtocol.size() && thisProtocol != "HTTP"){connectorLastActive[thisProtocol] = now;}
if (thisStreamName.size()){streamLastActive[thisStreamName] = now;}
if (memcmp(thisHost.data(), nullAddress, 16)){hostLastActive[thisHost] = now;}
// Determine session type, since triggers only get run for viewer type sessions
if (thisSessionId[0] == 'I'){
thisType = 1;
} else if (thisSessionId[0] == 'O'){
thisType = 2;
} else if (thisSessionId[0] == 'U'){
thisType = 3;
}
bool shouldSleep = false;
//Scope to ensure the connections page is deleted before other cleanup happens
{
// Open the shared memory page containing statistics for each individual connection in this session
Comms::Connections connections;
connections.reload(thisSessionId, true);
// Do a USER_NEW trigger if it is defined for this stream
if (!thisType && Triggers::shouldTrigger("USER_NEW", thisStreamName)){
std::string payload = thisStreamName + "\n" + config.getString("ip") + "\n" +
thisToken + "\n" + thisProtocol +
"\n" + thisReqUrl + "\n" + thisSessionId;
if (!Triggers::doTrigger("USER_NEW", payload, thisStreamName)){
// Mark all connections of this session as finished, since this viewer is not allowed to view this stream
Util::logExitReason("Session rejected by USER_NEW");
connections.setExit();
connections.finishAll();
}
}
//start allowing viewers
sessionLock.post();
INFO_MSG("Started new session %s in %.3f ms", thisSessionId.c_str(), (double)Util::getMicros(bootTime)/1000.0);
// Stay active until Mist exits or we no longer have an active connection
while (config.is_active && (currentConnections || now - lastSeen <= STATS_DELAY) && !connections.getExit()){
currentConnections = 0;
lastSecond = 0;
now = Util::bootSecs();
// Loop through all connection entries to get a summary of statistics
COMM_LOOP(connections, userOnActive(connections, id), userOnDisconnect(connections, id));
if (currentConnections){
globalTime++;
lastSeen = now;
}
sessions.setTime(globalTime);
sessions.setDown(globalDown);
sessions.setUp(globalUp);
sessions.setPacketCount(globalPktcount);
sessions.setPacketLostCount(globalPktloss);
sessions.setPacketRetransmitCount(globalPktretrans);
sessions.setLastSecond(lastSecond);
sessions.setNow(now);
if (currentConnections){
{
// Convert active protocols to string
std::stringstream connectorSummary;
for (std::map<std::string, uint64_t>::iterator it = connectorLastActive.begin();
it != connectorLastActive.end(); ++it){
if (now - it->second < STATS_DELAY){
connectorSummary << (connectorSummary.str().size() ? "," : "") << it->first;
}
}
sessions.setConnector(connectorSummary.str());
}
{
// Set active host to last active or 0 if there were various hosts active recently
std::string thisHost;
for (std::map<std::string, uint64_t>::iterator it = hostLastActive.begin();
it != hostLastActive.end(); ++it){
if (now - it->second < STATS_DELAY){
if (!thisHost.size()){
thisHost = it->first;
}else if (thisHost != it->first){
thisHost = nullAddress;
break;
}
}
}
if (!thisHost.size()){
thisHost = nullAddress;
}
sessions.setHost(thisHost);
}
{
// Set active stream name to last active or "" if there were multiple streams active recently
std::string thisStream = "";
for (std::map<std::string, uint64_t>::iterator it = streamLastActive.begin();
it != streamLastActive.end(); ++it){
if (now - it->second < STATS_DELAY){
if (!thisStream.size()){
thisStream = it->first;
}else if (thisStream != it->first){
thisStream = "";
break;
}
}
}
sessions.setStream(thisStream);
}
}
// Retrigger USER_NEW if a re-sync was requested
if (!thisType && forceTrigger){
forceTrigger = false;
std::string host;
Socket::hostBytesToStr(thisHost.data(), 16, host);
if (Triggers::shouldTrigger("USER_NEW", thisStreamName)){
INFO_MSG("Triggering USER_NEW for stream %s", thisStreamName.c_str());
std::string payload = thisStreamName + "\n" + host + "\n" +
thisToken + "\n" + thisProtocol +
"\n" + thisReqUrl + "\n" + thisSessionId;
if (!Triggers::doTrigger("USER_NEW", payload, thisStreamName)){
INFO_MSG("USER_NEW rejected stream %s", thisStreamName.c_str());
Util::logExitReason("Session rejected by USER_NEW");
connections.setExit();
connections.finishAll();
break;
}else{
INFO_MSG("USER_NEW accepted stream %s", thisStreamName.c_str());
}
}
}
// Remember latest activity so we know when this session ends
if (currentConnections){
}
Util::wait(1000);
}
shouldSleep = connections.getExit();
}//connections scope end
if (Util::bootSecs() - lastSeen > STATS_DELAY){
Util::logExitReason("Session inactive for %d seconds", STATS_DELAY);
}
// Trigger USER_END
if (!thisType && Triggers::shouldTrigger("USER_END", thisStreamName)){
// Convert connector, host and stream into lists and counts
std::stringstream connectorSummary;
std::stringstream connectorTimes;
for (std::map<std::string, uint64_t>::iterator it = connectorCount.begin(); it != connectorCount.end(); ++it){
connectorSummary << (connectorSummary.str().size() ? "," : "") << it->first;
connectorTimes << (connectorTimes.str().size() ? "," : "") << it->second;
}
std::stringstream hostSummary;
std::stringstream hostTimes;
for (std::map<std::string, uint64_t>::iterator it = hostCount.begin(); it != hostCount.end(); ++it){
std::string host;
Socket::hostBytesToStr(it->first.data(), 16, host);
hostSummary << (hostSummary.str().size() ? "," : "") << host;
hostTimes << (hostTimes.str().size() ? "," : "") << it->second;
}
std::stringstream streamSummary;
std::stringstream streamTimes;
for (std::map<std::string, uint64_t>::iterator it = streamCount.begin(); it != streamCount.end(); ++it){
streamSummary << (streamSummary.str().size() ? "," : "") << it->first;
streamTimes << (streamTimes.str().size() ? "," : "") << it->second;
}
std::stringstream summary;
summary << thisToken << "\n"
<< streamSummary.str() << "\n"
<< connectorSummary.str() << "\n"
<< hostSummary.str() << "\n"
<< globalTime << "\n"
<< globalUp << "\n"
<< globalDown << "\n"
<< sessions.getTags() << "\n"
<< hostTimes.str() << "\n"
<< connectorTimes.str() << "\n"
<< streamTimes.str() << "\n"
<< thisSessionId;
Triggers::doTrigger("USER_END", summary.str(), thisStreamName);
}
if (!thisType && shouldSleep){
uint64_t sleepStart = Util::bootSecs();
// Keep session invalidated for 10 minutes, or until the session stops
while (config.is_active && Util::bootSecs() - sleepStart < SESS_TIMEOUT){
Util::sleep(1000);
if (forceTrigger){break;}
}
}
INFO_MSG("Shutting down session %s: %s", thisSessionId.c_str(), Util::exitReason);
return 0;
}