MistController: now with even more threads!
This commit is contained in:
		
							parent
							
								
									c5f6956766
								
							
						
					
					
						commit
						5264fbf546
					
				
					 7 changed files with 470 additions and 577 deletions
				
			
		|  | @ -27,7 +27,6 @@ | |||
| 
 | ||||
| 
 | ||||
| #include <stdio.h> | ||||
| //#include <io.h>
 | ||||
| #include <iostream> | ||||
| #include <ctime> | ||||
| #include <vector> | ||||
|  | @ -38,203 +37,24 @@ | |||
| #include <mist/procs.h> | ||||
| #include <mist/auth.h> | ||||
| #include <mist/timing.h> | ||||
| #include <mist/converter.h> | ||||
| #include <mist/stream.h> | ||||
| #include <mist/defines.h> | ||||
| #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 <mist/tinythread.h> | ||||
| #include <mist/shared_memory.h> | ||||
| 
 | ||||
| 
 | ||||
| #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;///<The socket through which the user is connected.
 | ||||
|       HTTP::Parser H;///<The HTTP::Parser associated to this user, for the lsp.
 | ||||
|       bool Authorized;///<Indicates whether the current user is authorized.
 | ||||
|       bool clientMode;///<Indicates how to parse the commands.
 | ||||
|       int logins;///<Keeps track of the amount of login-tries.
 | ||||
|       std::string Username;///<The username of the user.
 | ||||
|        | ||||
|       ///\brief Construct a new user from a connection.
 | ||||
|       ///\param c The socket through which the user is connected.
 | ||||
|       ConnectedUser(Socket::Connection c){ | ||||
|         C = c; | ||||
|         H.Clean(); | ||||
|         logins = 0; | ||||
|         Authorized = false; | ||||
|         clientMode = false; | ||||
|       } | ||||
|   }; | ||||
| 
 | ||||
|   ///\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.
 | ||||
|   ///
 | ||||
|   /// \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.
 | ||||
|   void Authorize(JSON::Value & Request, JSON::Value & Response, ConnectedUser & 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.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<tthread::mutex> 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<std::string> execs; | ||||
|   Util::getMyExec(execs); | ||||
|   std::string arg_one; | ||||
|   char const * conn_args[] = {0, "-j", 0}; | ||||
|   for (std::deque<std::string>::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<Controller::ConnectedUser> users; | ||||
|   std::vector<Socket::Connection> 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<Controller::ConnectedUser>::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<Controller::ConnectedUser>::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; | ||||
|  |  | |||
							
								
								
									
										312
									
								
								src/controller/controller_api.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								src/controller/controller_api.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,312 @@ | |||
| #include <mist/http_parser.h> | ||||
| #include <mist/auth.h> | ||||
| #include <mist/config.h> | ||||
| #include <mist/defines.h> | ||||
| #include <mist/timing.h> | ||||
| #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<tthread::mutex> 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; | ||||
| } | ||||
							
								
								
									
										8
									
								
								src/controller/controller_api.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/controller/controller_api.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| #include <mist/socket.h> | ||||
| #include <mist/json.h> | ||||
| 
 | ||||
| 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); | ||||
| } | ||||
|  | @ -2,10 +2,48 @@ | |||
| #include <string.h> | ||||
| #include <fstream> | ||||
| #include <set> | ||||
| #include <mist/config.h> | ||||
| #include <mist/procs.h> | ||||
| #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<std::string> execs; | ||||
|     Util::getMyExec(execs); | ||||
|     std::string arg_one; | ||||
|     char const * conn_args[] = {0, "-j", 0}; | ||||
|     for (std::deque<std::string>::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: | ||||
|  |  | |||
|  | @ -1,5 +1,9 @@ | |||
| #include <mist/converter.h> | ||||
| #include <mist/json.h> | ||||
| 
 | ||||
| namespace Controller { | ||||
|   extern JSON::Value capabilities; ///< Global storage of capabilities
 | ||||
|   extern Converter::Converter myConverter; | ||||
|   void checkCapable(JSON::Value & capa); | ||||
|   void checkAvailProtocols(); | ||||
| } | ||||
|  |  | |||
|  | @ -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.
 | ||||
|  |  | |||
|  | @ -1,13 +1,14 @@ | |||
| #include <string> | ||||
| #include <mist/json.h> | ||||
| 
 | ||||
| #include <mist/config.h> | ||||
| #include <mist/tinythread.h> | ||||
| 
 | ||||
| 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); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Thulinma
						Thulinma