diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp index eedda409..c1b2f32a 100644 --- a/src/controller/controller.cpp +++ b/src/controller/controller.cpp @@ -27,7 +27,6 @@ #include -//#include #include #include #include @@ -38,203 +37,24 @@ #include #include #include -#include #include #include -#include "controller_storage.h" -#include "controller_connectors.h" -#include "controller_streams.h" -#include "controller_capabilities.h" -#include "controller_statistics.h" -#include "server.html.h" - - #include #include - - -#define UPLINK_INTERVAL 30 +#include "controller_storage.h" +#include "controller_streams.h" +#include "controller_capabilities.h" +#include "controller_connectors.h" +#include "controller_statistics.h" +#include "controller_api.h" #ifndef COMPILED_USERNAME #define COMPILED_USERNAME "" #define COMPILED_PASSWORD "" #endif -///\brief Holds everything unique to the controller. -namespace Controller { - Util::Config conf; - - Secure::Auth keychecker; ///< Checks key authorization. - - ///\brief A class storing information about a connected user. - class ConnectedUser{ - public: - Socket::Connection C;///tm_mday << "-" << TimeInfo->tm_mon << "-" << TimeInfo->tm_year + 1900; - std::string Challenge = Secure::md5(Date.str().c_str() + conn.C.getHost()); - if (Request.isMember("authorize")){ - std::string UserID = Request["authorize"]["username"]; - if (Storage["account"].isMember(UserID)){ - if (Secure::md5(Storage["account"][UserID]["password"].asString() + Challenge) == Request["authorize"]["password"].asString()){ - Response["authorize"]["status"] = "OK"; - conn.Username = UserID; - conn.Authorized = true; - return; - } - } - if (UserID != ""){ - if (Request["authorize"]["password"].asString() != "" - && Secure::md5(Storage["account"][UserID]["password"].asString()) != Request["authorize"]["password"].asString()){ - Log("AUTH", "Failed login attempt " + UserID + " @ " + conn.C.getHost()); - } - } - conn.logins++; - } - conn.Username = ""; - conn.Authorized = false; - Response["authorize"]["status"] = "CHALL"; - Response["authorize"]["challenge"] = Challenge; - //the following is used to add the first account through the LSP - if (!Storage["account"]){ - Response["authorize"]["status"] = "NOACC"; - if (Request["authorize"]["new_username"] && Request["authorize"]["new_password"]){ - //create account - Controller::Log("CONF", "Created account " + Request["authorize"]["new_username"].asString() + " through API"); - Controller::Storage["account"][Request["authorize"]["new_username"].asString()]["password"] = Secure::md5(Request["authorize"]["new_password"].asString()); - Response["authorize"]["status"] = "ACC_MADE"; - }else{ - Response["authorize"].removeMember("challenge"); - } - } - return; - } - - ///\brief Check the submitted configuration and handle things accordingly. - ///\param in The new configuration. - ///\param out The location to store the resulting configuration. - /// - /// \api - /// `"config"` requests take the form of: - /// ~~~~~~~~~~~~~~~{.js} - /// { - /// "controller": { //controller settings - /// "interface": null, //interface to listen on. Defaults to all interfaces. - /// "port": 4242, //port to listen on. Defaults to 4242. - /// "username": null //username to drop privileges to. Defaults to root. - /// }, - /// "protocols": [ //enabled connectors / protocols - /// { - /// "connector": "HTTP" //Name of the connector to enable - /// //any required and/or optional settings may be given here as "name": "value" pairs inside this object. - /// }, - /// //above structure repeated for all enabled connectors / protocols - /// ], - /// "serverid": "", //human-readable server identifier, optional. - /// } - /// ~~~~~~~~~~~~~~~ - /// and are responded to as: - /// ~~~~~~~~~~~~~~~{.js} - /// { - /// "controller": { //controller settings - /// "interface": null, //interface to listen on. Defaults to all interfaces. - /// "port": 4242, //port to listen on. Defaults to 4242. - /// "username": null //username to drop privileges to. Defaults to root. - /// }, - /// "protocols": [ //enabled connectors / protocols - /// { - /// "connector": "HTTP" //Name of the connector to enable - /// //any required and/or optional settings may be given here as "name": "value" pairs inside this object. - /// "online": 1 //boolean value indicating if the executable is running or not - /// }, - /// //above structure repeated for all enabled connectors / protocols - /// ], - /// "serverid": "", //human-readable server identifier, as configured. - /// "time": 1398982430, //current unix time - /// "version": "2.0.2/8.0.1-23-gfeb9322/Generic_64" //currently running server version string - /// } - /// ~~~~~~~~~~~~~~~ - void CheckConfig(JSON::Value & in, JSON::Value & out){ - out = in; - if (out["basepath"].asString()[out["basepath"].asString().size() - 1] == '/'){ - out["basepath"] = out["basepath"].asString().substr(0, out["basepath"].asString().size() - 1); - } - if (out.isMember("debug")){ - if (Util::Config::printDebugLevel != out["debug"].asInt()){ - Util::Config::printDebugLevel = out["debug"].asInt(); - INFO_MSG("Debug level set to %u", Util::Config::printDebugLevel); - } - } - } - -} //Controller namespace - /// the following function is a simple check if the user wants to proceed to fix (y), ignore (n) or abort on (a) a question -char yna(std::string & user_input){ +static inline char yna(std::string & user_input){ switch (user_input[0]){ case 'y': case 'Y': return 'y'; @@ -265,6 +85,21 @@ void createAccount (std::string account){ } } +/// Status monitoring thread. +/// Will check outputs, inputs and converters every five seconds +void statusMonitor(void * np){ + while (Controller::conf.is_active){ + //this scope prevents the configMutex from being locked constantly + { + tthread::lock_guard guard(Controller::configMutex); + Controller::CheckProtocols(Controller::Storage["config"]["protocols"], Controller::capabilities); + Controller::CheckAllStreams(Controller::Storage["streams"]); + Controller::myConverter.updateStatus(); + } + Util::sleep(5000);//sleep 5 seconds + } +} + ///\brief The main entry point for the controller. int main(int argc, char ** argv){ @@ -274,15 +109,12 @@ int main(int argc, char ** argv){ if ( !stored_port["default"]){ stored_port["default"] = 4242; } - JSON::Value stored_interface = - JSON::fromString( - "{\"long\":\"interface\", \"short\":\"i\", \"arg\":\"string\", \"help\":\"Interface address to listen on, or 0.0.0.0 for all available interfaces.\"}"); + JSON::Value stored_interface = JSON::fromString("{\"long\":\"interface\", \"short\":\"i\", \"arg\":\"string\", \"help\":\"Interface address to listen on, or 0.0.0.0 for all available interfaces.\"}"); stored_interface["default"] = Controller::Storage["config"]["controller"]["interface"]; if ( !stored_interface["default"]){ stored_interface["default"] = "0.0.0.0"; } - JSON::Value stored_user = JSON::fromString( - "{\"long\":\"username\", \"short\":\"u\", \"arg\":\"string\", \"help\":\"Username to transfer privileges to, default is root.\"}"); + JSON::Value stored_user = JSON::fromString("{\"long\":\"username\", \"short\":\"u\", \"arg\":\"string\", \"help\":\"Username to transfer privileges to, default is root.\"}"); stored_user["default"] = Controller::Storage["config"]["controller"]["username"]; if ( !stored_user["default"]){ stored_user["default"] = "root"; @@ -291,27 +123,10 @@ int main(int argc, char ** argv){ Controller::conf.addOption("listen_port", stored_port); Controller::conf.addOption("listen_interface", stored_interface); Controller::conf.addOption("username", stored_user); - Controller::conf.addOption("daemonize", - JSON::fromString( - "{\"long\":\"daemon\", \"short\":\"d\", \"default\":0, \"long_off\":\"nodaemon\", \"short_off\":\"n\", \"help\":\"Turns deamon mode on (-d) or off (-n). -d runs quietly in background, -n (default) enables verbose in foreground.\"}")); - Controller::conf.addOption("account", - JSON::fromString( - "{\"long\":\"account\", \"short\":\"a\", \"arg\":\"string\" \"default\":\"\", \"help\":\"A username:password string to create a new account with.\"}")); - Controller::conf.addOption("logfile", - JSON::fromString( - "{\"long\":\"logfile\", \"short\":\"L\", \"arg\":\"string\" \"default\":\"\",\"help\":\"Redirect all standard output to a log file, provided with an argument\"}")); - Controller::conf.addOption("configFile", - JSON::fromString( - "{\"long\":\"config\", \"short\":\"c\", \"arg\":\"string\" \"default\":\"config.json\", \"help\":\"Specify a config file other than default.\"}")); - Controller::conf.addOption("uplink", - JSON::fromString( - "{\"default\":\"\", \"arg\":\"string\", \"help\":\"MistSteward uplink host and port.\", \"short\":\"U\", \"long\":\"uplink\"}")); - Controller::conf.addOption("uplink-name", - JSON::fromString( - "{\"default\":\"" COMPILED_USERNAME "\", \"arg\":\"string\", \"help\":\"MistSteward uplink username.\", \"short\":\"N\", \"long\":\"uplink-name\"}")); - Controller::conf.addOption("uplink-pass", - JSON::fromString( - "{\"default\":\"" COMPILED_PASSWORD "\", \"arg\":\"string\", \"help\":\"MistSteward uplink password.\", \"short\":\"P\", \"long\":\"uplink-pass\"}")); + Controller::conf.addOption("daemonize", JSON::fromString("{\"long\":\"daemon\", \"short\":\"d\", \"default\":0, \"long_off\":\"nodaemon\", \"short_off\":\"n\", \"help\":\"Turns deamon mode on (-d) or off (-n). -d runs quietly in background, -n (default) enables verbose in foreground.\"}")); + Controller::conf.addOption("account", JSON::fromString("{\"long\":\"account\", \"short\":\"a\", \"arg\":\"string\" \"default\":\"\", \"help\":\"A username:password string to create a new account with.\"}")); + Controller::conf.addOption("logfile", JSON::fromString("{\"long\":\"logfile\", \"short\":\"L\", \"arg\":\"string\" \"default\":\"\",\"help\":\"Redirect all standard output to a log file, provided with an argument\"}")); + Controller::conf.addOption("configFile", JSON::fromString("{\"long\":\"config\", \"short\":\"c\", \"arg\":\"string\" \"default\":\"config.json\", \"help\":\"Specify a config file other than default.\"}")); Controller::conf.parseArgs(argc, argv); if(Controller::conf.getString("logfile")!= ""){ //open logfile, dup stdout to logfile @@ -331,11 +146,10 @@ int main(int argc, char ** argv){ std::cerr << std::endl << std::endl <<"!----MistServer Started at " << buffer << " ----!" << std::endl; } } - //Input custom config here + //reload config from config file Controller::Storage = JSON::fromFile(Controller::conf.getString("configFile")); - { - //spawn thread that reads stderr of process + {//spawn thread that reads stderr of process int pipeErr[2]; if (pipe(pipeErr) >= 0){ dup2(pipeErr[1], STDERR_FILENO);//cause stderr to write to the pipe @@ -369,382 +183,95 @@ int main(int argc, char ** argv){ Controller::conf.getOption("username") = Controller::Storage["config"]["controller"]["username"]; } } - JSON::Value capabilities; - //list available protocols and report about them - std::deque execs; - Util::getMyExec(execs); - std::string arg_one; - char const * conn_args[] = {0, "-j", 0}; - for (std::deque::iterator it = execs.begin(); it != execs.end(); it++){ - if ((*it).substr(0, 8) == "MistConn"){ - //skip if an MistOut already existed - MistOut takes precedence! - if (capabilities["connectors"].isMember((*it).substr(8))){ - continue; - } - arg_one = Util::getMyPath() + (*it); - conn_args[0] = arg_one.c_str(); - capabilities["connectors"][(*it).substr(8)] = JSON::fromString(Util::Procs::getOutputOf((char**)conn_args)); - if (capabilities["connectors"][(*it).substr(8)].size() < 1){ - capabilities["connectors"].removeMember((*it).substr(8)); - } - } - if ((*it).substr(0, 7) == "MistOut"){ - arg_one = Util::getMyPath() + (*it); - conn_args[0] = arg_one.c_str(); - capabilities["connectors"][(*it).substr(7)] = JSON::fromString(Util::Procs::getOutputOf((char**)conn_args)); - if (capabilities["connectors"][(*it).substr(7)].size() < 1){ - capabilities["connectors"].removeMember((*it).substr(7)); - } - } - } - + Controller::checkAvailProtocols(); createAccount(Controller::conf.getString("account")); - /// User friendliness input added at this line - if (isatty(fileno(stdin)) && Controller::conf.getString("logfile") == ""){ - //check for username - if ( !Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ - std::string in_string = ""; - while(yna(in_string) == 'x'){ - std::cout << "Account not set, do you want to create an account? (y)es, (n)o, (a)bort: "; - std::cout.flush(); - std::getline(std::cin, in_string); - if (yna(in_string) == 'y'){ - //create account - std::string usr_string = ""; - while(!(Controller::Storage.isMember("account") && Controller::Storage["account"].size() > 0)){ - std::cout << "Please type in the username, a colon and a password in the following format; username:password" << std::endl << ": "; - std::cout.flush(); - std::getline(std::cin, usr_string); - createAccount(usr_string); - } - }else if(yna(in_string) == 'a'){ - //abort controller startup - return 0; - } - } - } - //check for protocols - if ( !Controller::Storage.isMember("config") || !Controller::Storage["config"].isMember("protocols") || Controller::Storage["config"]["protocols"].size() < 1){ - std::string in_string = ""; - while(yna(in_string) == 'x'){ - std::cout << "Protocols not set, do you want to enable default protocols? (y)es, (n)o, (a)bort: "; - std::cout.flush(); - std::getline(std::cin, in_string); - if (yna(in_string) == 'y'){ - //create protocols - for (JSON::ObjIter it = capabilities["connectors"].ObjBegin(); it != capabilities["connectors"].ObjEnd(); it++){ - if ( !it->second.isMember("required")){ - JSON::Value newProtocol; - newProtocol["connector"] = it->first; - Controller::Storage["config"]["protocols"].append(newProtocol); + //if a terminal is connected and we're not logging to file + if (isatty(fileno(stdin))){ + if (Controller::conf.getString("logfile") == ""){ + //check for username + if ( !Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ + std::string in_string = ""; + while(yna(in_string) == 'x'){ + std::cout << "Account not set, do you want to create an account? (y)es, (n)o, (a)bort: "; + std::cout.flush(); + std::getline(std::cin, in_string); + if (yna(in_string) == 'y'){ + //create account + std::string usr_string = ""; + while(!(Controller::Storage.isMember("account") && Controller::Storage["account"].size() > 0)){ + std::cout << "Please type in the username, a colon and a password in the following format; username:password" << std::endl << ": "; + std::cout.flush(); + std::getline(std::cin, usr_string); + createAccount(usr_string); } + }else if(yna(in_string) == 'a'){ + //abort controller startup + return 0; } - }else if(yna(in_string) == 'a'){ - //abort controller startup - return 0; } } + //check for protocols + if ( !Controller::Storage.isMember("config") || !Controller::Storage["config"].isMember("protocols") || Controller::Storage["config"]["protocols"].size() < 1){ + std::string in_string = ""; + while(yna(in_string) == 'x'){ + std::cout << "Protocols not set, do you want to enable default protocols? (y)es, (n)o, (a)bort: "; + std::cout.flush(); + std::getline(std::cin, in_string); + if (yna(in_string) == 'y'){ + //create protocols + for (JSON::ObjIter it = Controller::capabilities["connectors"].ObjBegin(); it != Controller::capabilities["connectors"].ObjEnd(); it++){ + if ( !it->second.isMember("required")){ + JSON::Value newProtocol; + newProtocol["connector"] = it->first; + Controller::Storage["config"]["protocols"].append(newProtocol); + } + } + }else if(yna(in_string) == 'a'){ + //abort controller startup + return 0; + } + } + } + }else{//logfile is enabled + //check for username + if ( !Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ + std::cout << "No login configured. To create one, attempt to login through the web interface on port " << Controller::conf.getInteger("listen_port") << " and follow the instructions." << std::endl; + } + //check for protocols + if ( !Controller::Storage.isMember("config") || !Controller::Storage["config"].isMember("protocols") || Controller::Storage["config"]["protocols"].size() < 1){ + std::cout << "No protocols enabled, remember to set them up through the web interface on port " << Controller::conf.getInteger("listen_port") << " or API." << std::endl; + } } - //check for streams + //check for streams - regardless of logfile setting if ( !Controller::Storage.isMember("streams") || Controller::Storage["streams"].size() < 1){ - std::cout << "No streams configured, remember to set up streams through local settings page on port " << Controller::conf.getInteger("listen_port") << " or using the API." << std::endl; + std::cout << "No streams configured, remember to set up streams through the web interface on port " << Controller::conf.getInteger("listen_port") << " or API." << std::endl; } - } + }//connected to a terminal - std::string uplink_addr = Controller::conf.getString("uplink"); - std::string uplink_host = ""; - int uplink_port = 0; - if (uplink_addr.size() > 0){ - size_t colon = uplink_addr.find(':'); - if (colon != std::string::npos && colon != 0 && colon != uplink_addr.size()){ - uplink_host = uplink_addr.substr(0, colon); - uplink_port = atoi(uplink_addr.substr(colon + 1, std::string::npos).c_str()); - Controller::Log("CONF", - "Connection to uplink enabled on host " + uplink_host + " and port " + uplink_addr.substr(colon + 1, std::string::npos)); - } - } - - time_t lastuplink = 0; - time_t processchecker = 0; - Socket::Server API_Socket = Socket::Server(Controller::conf.getInteger("listen_port"), Controller::conf.getString("listen_interface"), true); - Socket::Connection Incoming; - std::vector users; - std::vector buffers; - JSON::Value Request; - JSON::Value Response; - std::string jsonp; - Controller::ConnectedUser * uplink = 0; Controller::Log("CONF", "Controller started"); - Controller::conf.activate(); + Controller::conf.is_active = true;//set this to true here already so the below threads don't instantly exit - //Create a converter class and automatically load in all encoders. - Converter::Converter myConverter; - + //start stats thread tthread::thread statsThread(Controller::SharedMemStats, &Controller::conf); + //start monitoring thread + tthread::thread monitorThread(statusMonitor, 0); - while (API_Socket.connected() && Controller::conf.is_active){ - Util::sleep(10);//sleep for 10 ms - prevents 100% CPU time - - - if (Util::epoch() - processchecker > 5){ - processchecker = Util::epoch(); - Controller::CheckProtocols(Controller::Storage["config"]["protocols"], capabilities); - Controller::CheckAllStreams(Controller::Storage["streams"]); - myConverter.updateStatus(); - } - if (uplink_port && Util::epoch() - lastuplink > UPLINK_INTERVAL){ - lastuplink = Util::epoch(); - bool gotUplink = false; - if (users.size() > 0){ - for (std::vector::iterator it = users.end() - 1; it >= users.begin(); it--){ - if ( !it->C.connected()){ - it->C.close(); - users.erase(it); - break; - } - if (it->clientMode){ - uplink = & *it; - gotUplink = true; - } - } - } - if ( !gotUplink){ - Incoming = Socket::Connection(uplink_host, uplink_port, true); - if (Incoming.connected()){ - users.push_back((Controller::ConnectedUser)Incoming); - users.back().clientMode = true; - uplink = &users.back(); - gotUplink = true; - } - } - if (gotUplink){ - Response.null(); //make sure no data leaks from previous requests - Response["config"] = Controller::Storage["config"]; - Response["streams"] = Controller::Storage["streams"]; - Response["log"] = Controller::Storage["log"]; - /// \todo Put this back in, someway, somehow... - //Response["statistics"] = Controller::Storage["statistics"]; - Response["now"] = (unsigned int)lastuplink; - uplink->H.Clean(); - uplink->H.SetBody("command=" + HTTP::Parser::urlencode(Response.toString())); - uplink->H.BuildRequest(); - uplink->H.SendResponse("200", "OK", uplink->C); - uplink->H.Clean(); - //Controller::Log("UPLK", "Sending server data to uplink."); - }else{ - Controller::Log("UPLK", "Could not connect to uplink."); - } - } - - Incoming = API_Socket.accept(true); - if (Incoming.connected()){ - users.push_back((Controller::ConnectedUser)Incoming); - } - if (users.size() > 0){ - for (std::vector::iterator it = users.begin(); it != users.end(); it++){ - if ( !it->C.connected() || it->logins > 3){ - it->C.close(); - users.erase(it); - break; - } - if (it->C.spool() || it->C.Received().size()){ - if (it->H.Read(it->C)){ - Response.null(); //make sure no data leaks from previous requests - if (it->clientMode){ - // In clientMode, requests are reversed. These are connections we initiated to GearBox. - // They are assumed to be authorized, but authorization to gearbox is still done. - // This authorization uses the compiled-in or commandline username and password (account). - Request = JSON::fromString(it->H.body); - if (Request["authorize"]["status"] != "OK"){ - if (Request["authorize"].isMember("challenge")){ - it->logins++; - if (it->logins > 2){ - Controller::Log("UPLK", "Max login attempts passed - dropping connection to uplink."); - it->C.close(); - }else{ - Response["config"] = Controller::Storage["config"]; - Response["streams"] = Controller::Storage["streams"]; - Response["log"] = Controller::Storage["log"]; - /// \todo Put this back in, someway, somehow... - //Response["statistics"] = Controller::Storage["statistics"]; - Response["authorize"]["username"] = Controller::conf.getString("uplink-name"); - Controller::checkCapable(capabilities); - Response["capabilities"] = capabilities; - Controller::Log("UPLK", "Responding to login challenge: " + Request["authorize"]["challenge"].asString()); - Response["authorize"]["password"] = Secure::md5(Controller::conf.getString("uplink-pass") + Request["authorize"]["challenge"].asString()); - it->H.Clean(); - it->H.SetBody("command=" + HTTP::Parser::urlencode(Response.toString())); - it->H.BuildRequest(); - it->H.SendResponse("200", "OK", it->C); - it->H.Clean(); - Controller::Log("UPLK", "Attempting login to uplink."); - } - } - }else{ - if (Request.isMember("config")){ - Controller::CheckConfig(Request["config"], Controller::Storage["config"]); - Controller::CheckProtocols(Controller::Storage["config"]["protocols"], capabilities); - } - if (Request.isMember("streams")){ - Controller::CheckStreams(Request["streams"], Controller::Storage["streams"]); - } - if (Request.isMember("clearstatlogs")){ - Controller::Storage["log"].null(); - } - } - }else{ - Request = JSON::fromString(it->H.GetVar("command")); - if ( !Request.isObject() && it->H.url != "/api"){ - it->H.Clean(); - it->H.SetHeader("Content-Type", "text/html"); - it->H.SetHeader("X-Info", "To force an API response, request the file /api"); - it->H.SetHeader("Server", "mistserver/" PACKAGE_VERSION "/" + Util::Config::libver + "/" RELEASE); - it->H.SetHeader("Content-Length", server_html_len); - it->H.SendResponse("200", "OK", it->C); - it->C.SendNow(server_html, server_html_len); - it->H.Clean(); - }else{ - Authorize(Request, Response, ( *it)); - if (it->Authorized){ - //Parse config and streams from the request. - if (Request.isMember("config")){ - Controller::CheckConfig(Request["config"], Controller::Storage["config"]); - Controller::CheckProtocols(Controller::Storage["config"]["protocols"], capabilities); - } - if (Request.isMember("streams")){ - Controller::CheckStreams(Request["streams"], Controller::Storage["streams"]); - } - if (Request.isMember("capabilities")){ - Controller::checkCapable(capabilities); - Response["capabilities"] = capabilities; - } - if (Request.isMember("conversion")){ - if (Request["conversion"].isMember("encoders")){ - Response["conversion"]["encoders"] = myConverter.getEncoders(); - } - if (Request["conversion"].isMember("query")){ - if (Request["conversion"]["query"].isMember("path")){ - Response["conversion"]["query"] = myConverter.queryPath(Request["conversion"]["query"]["path"].asString()); - }else{ - Response["conversion"]["query"] = myConverter.queryPath("./"); - } - } - if (Request["conversion"].isMember("convert")){ - for (JSON::ObjIter it = Request["conversion"]["convert"].ObjBegin(); it != Request["conversion"]["convert"].ObjEnd(); it++){ - myConverter.startConversion(it->first,it->second); - Controller::Log("CONV","Conversion " + it->second["input"].asString() + " to " + it->second["output"].asString() + " started."); - } - } - if (Request["conversion"].isMember("status") || Request["conversion"].isMember("convert")){ - if (Request["conversion"].isMember("clear")){ - myConverter.clearStatus(); - } - Response["conversion"]["status"] = myConverter.getStatus(); - } - } - /// - /// \api - /// `"save"` requests are always empty: - /// ~~~~~~~~~~~~~~~{.js} - /// {} - /// ~~~~~~~~~~~~~~~ - /// Sending this request will cause the controller to write out its currently active configuration to the configuration file it was loaded from (the default being `./config.json`). - /// - if (Request.isMember("save")){ - if( Controller::WriteFile(Controller::conf.getString("configFile"), Controller::Storage.toString())){ - Controller::Log("CONF", "Config written to file on request through API"); - }else{ - Controller::Log("ERROR", "Config " + Controller::conf.getString("configFile") + " could not be written"); - } - } - //sent current configuration, no matter if it was changed or not - Response["config"] = Controller::Storage["config"]; - Response["config"]["version"] = PACKAGE_VERSION "/" + Util::Config::libver + "/" RELEASE; - Response["streams"] = Controller::Storage["streams"]; - //add required data to the current unix time to the config, for syncing reasons - Response["config"]["time"] = Util::epoch(); - if ( !Response["config"].isMember("serverid")){ - Response["config"]["serverid"] = ""; - } - //sent any available logs and statistics - /// - /// \api - /// `"log"` responses are always sent, and cannot be requested: - /// ~~~~~~~~~~~~~~~{.js} - /// [ - /// [ - /// 1398978357, //unix timestamp of this log message - /// "CONF", //shortcode indicating the type of log message - /// "Starting connector: {\"connector\":\"HTTP\"}" //string containing the log message itself - /// ], - /// //the above structure repeated for all logs - /// ] - /// ~~~~~~~~~~~~~~~ - /// It's possible to clear the stored logs by sending an empty `"clearstatlogs"` request. - /// - Response["log"] = Controller::Storage["log"]; - //clear log and statistics if requested - if (Request.isMember("clearstatlogs")){ - Controller::Storage["log"].null(); - } - if (Request.isMember("clients")){ - if (Request["clients"].isArray()){ - for (unsigned int i = 0; i < Request["clients"].size(); ++i){ - Controller::fillClients(Request["clients"][i], Response["clients"][i]); - } - }else{ - Controller::fillClients(Request["clients"], Response["clients"]); - } - } - if (Request.isMember("totals")){ - if (Request["totals"].isArray()){ - for (unsigned int i = 0; i < Request["totals"].size(); ++i){ - Controller::fillTotals(Request["totals"][i], Response["totals"][i]); - } - }else{ - Controller::fillTotals(Request["totals"], Response["totals"]); - } - } - - } - jsonp = ""; - if (it->H.GetVar("callback") != ""){ - jsonp = it->H.GetVar("callback"); - } - if (it->H.GetVar("jsonp") != ""){ - jsonp = it->H.GetVar("jsonp"); - } - it->H.Clean(); - it->H.SetHeader("Content-Type", "text/javascript"); - it->H.SetHeader("Access-Control-Allow-Origin", "*"); - it->H.SetHeader("Access-Control-Allow-Methods", "GET, POST"); - it->H.SetHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With"); - it->H.SetHeader("Access-Control-Allow-Credentials", "true"); - - if (jsonp == ""){ - it->H.SetBody(Response.toString() + "\n\n"); - }else{ - it->H.SetBody(jsonp + "(" + Response.toString() + ");\n\n"); - } - it->H.SendResponse("200", "OK", it->C); - it->H.Clean(); - } - } - } - } - } - } - } + //start main loop + Controller::conf.serveThreadedSocket(Controller::handleAPIConnection); + //print shutdown reason if (!Controller::conf.is_active){ Controller::Log("CONF", "Controller shutting down because of user request (received shutdown signal)"); - } - if (!API_Socket.connected()){ + }else{ Controller::Log("CONF", "Controller shutting down because of socket problem (API port closed)"); } Controller::conf.is_active = false; - API_Socket.close(); + //close stderr to make the stderr reading thread exit + close(STDERR_FILENO); + //join all joinable threads statsThread.join(); + monitorThread.join(); + //write config if ( !Controller::WriteFile(Controller::conf.getString("configFile"), Controller::Storage.toString())){ std::cerr << "Error writing config " << Controller::conf.getString("configFile") << std::endl; Controller::Storage.removeMember("log"); @@ -755,6 +282,7 @@ int main(int argc, char ** argv){ std::cerr << Controller::Storage.toString() << std::endl; std::cerr << "**End config**" << std::endl; } + //stop all child processes Util::Procs::StopAll(); std::cout << "Killed all processes, wrote config to disk. Exiting." << std::endl; return 0; diff --git a/src/controller/controller_api.cpp b/src/controller/controller_api.cpp new file mode 100644 index 00000000..837e253c --- /dev/null +++ b/src/controller/controller_api.cpp @@ -0,0 +1,312 @@ +#include +#include +#include +#include +#include +#include "controller_api.h" +#include "controller_storage.h" +#include "controller_streams.h" +#include "controller_connectors.h" +#include "controller_capabilities.h" +#include "controller_statistics.h" + +///\brief Check the submitted configuration and handle things accordingly. +///\param in The new configuration. +///\param out The location to store the resulting configuration. +/// +/// \api +/// `"config"` requests take the form of: +/// ~~~~~~~~~~~~~~~{.js} +/// { +/// "controller": { //controller settings +/// "interface": null, //interface to listen on. Defaults to all interfaces. +/// "port": 4242, //port to listen on. Defaults to 4242. +/// "username": null //username to drop privileges to. Defaults to root. +/// }, +/// "protocols": [ //enabled connectors / protocols +/// { +/// "connector": "HTTP" //Name of the connector to enable +/// //any required and/or optional settings may be given here as "name": "value" pairs inside this object. +/// }, +/// //above structure repeated for all enabled connectors / protocols +/// ], +/// "serverid": "", //human-readable server identifier, optional. +/// } +/// ~~~~~~~~~~~~~~~ +/// and are responded to as: +/// ~~~~~~~~~~~~~~~{.js} +/// { +/// "controller": { //controller settings +/// "interface": null, //interface to listen on. Defaults to all interfaces. +/// "port": 4242, //port to listen on. Defaults to 4242. +/// "username": null //username to drop privileges to. Defaults to root. +/// }, +/// "protocols": [ //enabled connectors / protocols +/// { +/// "connector": "HTTP" //Name of the connector to enable +/// //any required and/or optional settings may be given here as "name": "value" pairs inside this object. +/// "online": 1 //boolean value indicating if the executable is running or not +/// }, +/// //above structure repeated for all enabled connectors / protocols +/// ], +/// "serverid": "", //human-readable server identifier, as configured. +/// "time": 1398982430, //current unix time +/// "version": "2.0.2/8.0.1-23-gfeb9322/Generic_64" //currently running server version string +/// } +/// ~~~~~~~~~~~~~~~ +void Controller::checkConfig(JSON::Value & in, JSON::Value & out){ + out = in; + if (out["basepath"].asString()[out["basepath"].asString().size() - 1] == '/'){ + out["basepath"] = out["basepath"].asString().substr(0, out["basepath"].asString().size() - 1); + } + if (out.isMember("debug")){ + if (Util::Config::printDebugLevel != out["debug"].asInt()){ + Util::Config::printDebugLevel = out["debug"].asInt(); + INFO_MSG("Debug level set to %u", Util::Config::printDebugLevel); + } + } +} + +///\brief Checks an authorization request for a given user. +///\param Request The request to be parsed. +///\param Response The location to store the generated response. +///\param conn The user to be checked for authorization. +///\return True on successfull authorization, false otherwise. +/// +/// \api +/// To login, an `"authorize"` request must be sent. Since HTTP does not use persistent connections, you are required to re-sent authentication with every API request made. To prevent plaintext sending of the password, a random challenge string is sent first, and then the password is hashed together with this challenge string to create a one-time-use string to login with. +/// If the user is not authorized, this request is the only request the server will respond to until properly authorized. +/// `"authorize"` requests take the form of: +/// ~~~~~~~~~~~~~~~{.js} +/// { +/// //username to login as +/// "username": "test", +/// //hash of password to login with. Send empty value when no challenge for the hash is known yet. +/// //When the challenge is known, the value to be used here can be calculated as follows: +/// // MD5( MD5("secret") + challenge) +/// //Where "secret" is the plaintext password. +/// "password": "" +/// } +/// ~~~~~~~~~~~~~~~ +/// and are responded to as: +/// ~~~~~~~~~~~~~~~{.js} +/// { +/// //current login status. Either "OK", "CHALL", "NOACC" or "ACC_MADE". +/// "status": "CHALL", +/// //Random value to be used in hashing the password. +/// "challenge": "abcdef1234567890" +/// } +/// ~~~~~~~~~~~~~~~ +/// The challenge string is sent for all statuses, except `"NOACC"`, where it is left out. +/// A status of `"OK"` means you are currently logged in and have access to all other API requests. +/// A status of `"CHALL"` means you are not logged in, and a challenge has been provided to login with. +/// A status of `"NOACC"` means there are no valid accounts to login with. In this case - and ONLY in this case - it is possible to create a initial login through the API itself. To do so, send a request as follows: +/// ~~~~~~~~~~~~~~~{.js} +/// { +/// //username to create, as plain text +/// "new_username": "test", +/// //password to set, as plain text +/// "new_password": "secret" +/// } +/// ~~~~~~~~~~~~~~~ +/// Please note that this is NOT secure. At all. Never use this mechanism over a public network! +/// A status of `"ACC_MADE"` indicates the account was created successfully and can now be used to login as normal. +bool Controller::authorize(JSON::Value & Request, JSON::Value & Response, Socket::Connection & conn){ + time_t Time = time(0); + tm * TimeInfo = localtime( &Time); + std::stringstream Date; + std::string retval; + Date << TimeInfo->tm_mday << "-" << TimeInfo->tm_mon << "-" << TimeInfo->tm_year + 1900; + std::string Challenge = Secure::md5(Date.str().c_str() + conn.getHost()); + if (Request.isMember("authorize") && Request["authorize"]["username"].asString() != ""){ + std::string UserID = Request["authorize"]["username"]; + if (Storage["account"].isMember(UserID)){ + if (Secure::md5(Storage["account"][UserID]["password"].asString() + Challenge) == Request["authorize"]["password"].asString()){ + Response["authorize"]["status"] = "OK"; + return true; + } + } + if (Request["authorize"]["password"].asString() != "" && Secure::md5(Storage["account"][UserID]["password"].asString()) != Request["authorize"]["password"].asString()){ + Log("AUTH", "Failed login attempt " + UserID + " from " + conn.getHost()); + } + } + Response["authorize"]["status"] = "CHALL"; + Response["authorize"]["challenge"] = Challenge; + //the following is used to add the first account through the LSP + if (!Storage["account"]){ + Response["authorize"]["status"] = "NOACC"; + if (Request["authorize"]["new_username"] && Request["authorize"]["new_password"]){ + //create account + Controller::Log("CONF", "Created account " + Request["authorize"]["new_username"].asString() + " through API"); + Controller::Storage["account"][Request["authorize"]["new_username"].asString()]["password"] = Secure::md5(Request["authorize"]["new_password"].asString()); + Response["authorize"]["status"] = "ACC_MADE"; + }else{ + Response["authorize"].removeMember("challenge"); + } + } + return false; +}//Authorize + +/// Handles a single incoming API connection. +/// Assumes the connection is unauthorized and will allow for 4 requests without authorization before disconnecting. +int Controller::handleAPIConnection(Socket::Connection & conn){ + //set up defaults + unsigned int logins = 0; + bool authorized = false; + HTTP::Parser H; + //while connected and not past login attempt limit + while (conn && logins < 4){ + if ((conn.spool() || conn.Received().size()) && H.Read(conn)){ + JSON::Value Response; + JSON::Value Request = JSON::fromString(H.GetVar("command")); + //invalid request? send the web interface, unless requested as "/api" + if ( !Request.isObject() && H.url != "/api"){ + #include "server.html.h" + H.Clean(); + H.SetHeader("Content-Type", "text/html"); + H.SetHeader("X-Info", "To force an API response, request the file /api"); + H.SetHeader("Server", "mistserver/" PACKAGE_VERSION "/" + Util::Config::libver + "/" RELEASE); + H.SetHeader("Content-Length", server_html_len); + H.SendResponse("200", "OK", conn); + conn.SendNow(server_html, server_html_len); + H.Clean(); + break; + } + {//lock the config mutex here - do not unlock until done processing + tthread::lock_guard guard(configMutex); + //if already authorized, do not re-check for authorization + if (authorized){ + Response["authorize"]["status"] = "OK"; + }else{ + authorized |= authorize(Request, Response, conn); + } + if (authorized){ + //Parse config and streams from the request. + if (Request.isMember("config")){ + Controller::checkConfig(Request["config"], Controller::Storage["config"]); + Controller::CheckProtocols(Controller::Storage["config"]["protocols"], capabilities); + } + if (Request.isMember("streams")){ + Controller::CheckStreams(Request["streams"], Controller::Storage["streams"]); + } + if (Request.isMember("capabilities")){ + Controller::checkCapable(capabilities); + Response["capabilities"] = capabilities; + } + if (Request.isMember("conversion")){ + if (Request["conversion"].isMember("encoders")){ + Response["conversion"]["encoders"] = myConverter.getEncoders(); + } + if (Request["conversion"].isMember("query")){ + if (Request["conversion"]["query"].isMember("path")){ + Response["conversion"]["query"] = myConverter.queryPath(Request["conversion"]["query"]["path"].asString()); + }else{ + Response["conversion"]["query"] = myConverter.queryPath("./"); + } + } + if (Request["conversion"].isMember("convert")){ + for (JSON::ObjIter it = Request["conversion"]["convert"].ObjBegin(); it != Request["conversion"]["convert"].ObjEnd(); it++){ + myConverter.startConversion(it->first,it->second); + Controller::Log("CONV","Conversion " + it->second["input"].asString() + " to " + it->second["output"].asString() + " started."); + } + } + if (Request["conversion"].isMember("status") || Request["conversion"].isMember("convert")){ + if (Request["conversion"].isMember("clear")){ + myConverter.clearStatus(); + } + Response["conversion"]["status"] = myConverter.getStatus(); + } + } + /// + /// \api + /// `"save"` requests are always empty: + /// ~~~~~~~~~~~~~~~{.js} + /// {} + /// ~~~~~~~~~~~~~~~ + /// Sending this request will cause the controller to write out its currently active configuration to the configuration file it was loaded from (the default being `./config.json`). + /// + if (Request.isMember("save")){ + if( Controller::WriteFile(Controller::conf.getString("configFile"), Controller::Storage.toString())){ + Controller::Log("CONF", "Config written to file on request through API"); + }else{ + Controller::Log("ERROR", "Config " + Controller::conf.getString("configFile") + " could not be written"); + } + } + //sent current configuration, no matter if it was changed or not + Response["config"] = Controller::Storage["config"]; + Response["config"]["version"] = PACKAGE_VERSION "/" + Util::Config::libver + "/" RELEASE; + Response["streams"] = Controller::Storage["streams"]; + //add required data to the current unix time to the config, for syncing reasons + Response["config"]["time"] = Util::epoch(); + if ( !Response["config"].isMember("serverid")){ + Response["config"]["serverid"] = ""; + } + //sent any available logs and statistics + /// + /// \api + /// `"log"` responses are always sent, and cannot be requested: + /// ~~~~~~~~~~~~~~~{.js} + /// [ + /// [ + /// 1398978357, //unix timestamp of this log message + /// "CONF", //shortcode indicating the type of log message + /// "Starting connector: {\"connector\":\"HTTP\"}" //string containing the log message itself + /// ], + /// //the above structure repeated for all logs + /// ] + /// ~~~~~~~~~~~~~~~ + /// It's possible to clear the stored logs by sending an empty `"clearstatlogs"` request. + /// + Response["log"] = Controller::Storage["log"]; + //clear log and statistics if requested + if (Request.isMember("clearstatlogs")){ + Controller::Storage["log"].null(); + } + if (Request.isMember("clients")){ + if (Request["clients"].isArray()){ + for (unsigned int i = 0; i < Request["clients"].size(); ++i){ + Controller::fillClients(Request["clients"][i], Response["clients"][i]); + } + }else{ + Controller::fillClients(Request["clients"], Response["clients"]); + } + } + if (Request.isMember("totals")){ + if (Request["totals"].isArray()){ + for (unsigned int i = 0; i < Request["totals"].size(); ++i){ + Controller::fillTotals(Request["totals"][i], Response["totals"][i]); + } + }else{ + Controller::fillTotals(Request["totals"], Response["totals"]); + } + } + }else{//unauthorized + Util::sleep(1000);//sleep a second to prevent bruteforcing + logins++; + } + }//config mutex lock + //send the response, either normally or through JSONP callback. + std::string jsonp = ""; + if (H.GetVar("callback") != ""){ + jsonp = H.GetVar("callback"); + } + if (H.GetVar("jsonp") != ""){ + jsonp = H.GetVar("jsonp"); + } + H.Clean(); + H.SetHeader("Content-Type", "text/javascript"); + H.SetHeader("Access-Control-Allow-Origin", "*"); + H.SetHeader("Access-Control-Allow-Methods", "GET, POST"); + H.SetHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With"); + H.SetHeader("Access-Control-Allow-Credentials", "true"); + if (jsonp == ""){ + H.SetBody(Response.toString() + "\n\n"); + }else{ + H.SetBody(jsonp + "(" + Response.toString() + ");\n\n"); + } + H.SendResponse("200", "OK", conn); + H.Clean(); + }//if HTTP request received + }//while connected + return 0; +} diff --git a/src/controller/controller_api.h b/src/controller/controller_api.h new file mode 100644 index 00000000..bd50f58f --- /dev/null +++ b/src/controller/controller_api.h @@ -0,0 +1,8 @@ +#include +#include + +namespace Controller { + void checkConfig(JSON::Value & in, JSON::Value & out); + bool authorize(JSON::Value & Request, JSON::Value & Response, Socket::Connection & conn); + int handleAPIConnection(Socket::Connection & conn); +} diff --git a/src/controller/controller_capabilities.cpp b/src/controller/controller_capabilities.cpp index 884bf5b3..76798ad5 100644 --- a/src/controller/controller_capabilities.cpp +++ b/src/controller/controller_capabilities.cpp @@ -2,10 +2,48 @@ #include #include #include +#include +#include #include "controller_capabilities.h" ///\brief Holds everything unique to the controller. namespace Controller { + + ///Create a converter class and automatically load in all encoders. + Converter::Converter myConverter; + + JSON::Value capabilities; + + ///Aquire list of available protocols, storing in global 'capabilities' JSON::Value. + void checkAvailProtocols(){ + std::deque execs; + Util::getMyExec(execs); + std::string arg_one; + char const * conn_args[] = {0, "-j", 0}; + for (std::deque::iterator it = execs.begin(); it != execs.end(); it++){ + if ((*it).substr(0, 8) == "MistConn"){ + //skip if an MistOut already existed - MistOut takes precedence! + if (capabilities["connectors"].isMember((*it).substr(8))){ + continue; + } + arg_one = Util::getMyPath() + (*it); + conn_args[0] = arg_one.c_str(); + capabilities["connectors"][(*it).substr(8)] = JSON::fromString(Util::Procs::getOutputOf((char**)conn_args)); + if (capabilities["connectors"][(*it).substr(8)].size() < 1){ + capabilities["connectors"].removeMember((*it).substr(8)); + } + } + if ((*it).substr(0, 7) == "MistOut"){ + arg_one = Util::getMyPath() + (*it); + conn_args[0] = arg_one.c_str(); + capabilities["connectors"][(*it).substr(7)] = JSON::fromString(Util::Procs::getOutputOf((char**)conn_args)); + if (capabilities["connectors"][(*it).substr(7)].size() < 1){ + capabilities["connectors"].removeMember((*it).substr(7)); + } + } + } + } + ///\brief A class storing information about the cpu the server is running on. class cpudata{ public: diff --git a/src/controller/controller_capabilities.h b/src/controller/controller_capabilities.h index 726be634..5ff9c14d 100644 --- a/src/controller/controller_capabilities.h +++ b/src/controller/controller_capabilities.h @@ -1,5 +1,9 @@ +#include #include namespace Controller { + extern JSON::Value capabilities; ///< Global storage of capabilities + extern Converter::Converter myConverter; void checkCapable(JSON::Value & capa); + void checkAvailProtocols(); } diff --git a/src/controller/controller_storage.cpp b/src/controller/controller_storage.cpp index 13afa680..8548978e 100644 --- a/src/controller/controller_storage.cpp +++ b/src/controller/controller_storage.cpp @@ -6,8 +6,10 @@ ///\brief Holds everything unique to the controller. namespace Controller { + Util::Config conf; JSON::Value Storage; ///< Global storage of data. - tthread::mutex logMutex;///< Mutex for log thread. + tthread::mutex configMutex; + tthread::mutex logMutex; ///\brief Store and print a log message. ///\param kind The type of message. ///\param message The message to be logged. diff --git a/src/controller/controller_storage.h b/src/controller/controller_storage.h index ada5a277..f3abf7d7 100644 --- a/src/controller/controller_storage.h +++ b/src/controller/controller_storage.h @@ -1,13 +1,14 @@ #include #include - +#include #include + namespace Controller { - + extern Util::Config conf;///< Global storage of configuration. extern JSON::Value Storage; ///< Global storage of data. - extern tthread::mutex logMutex;///< Mutex for log thread. - + extern tthread::mutex configMutex;///< Mutex for server config access. + /// Store and print a log message. void Log(std::string kind, std::string message);