#include //for browse API call #include //for browse API call #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; } /// \todo Re-enable conversion API at some point. /* 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(); } } */ /// This takes a "browse" request, and fills in the response data. /// /// \api /// `"browse"` requests take the form of: /// ~~~~~~~~~~~~~~~{.js} /// //A string, containing the path for which to discover contents. Empty means current working directory. /// "/tmp/example" /// ~~~~~~~~~~~~~~~ /// and are responded to as: /// ~~~~~~~~~~~~~~~{.js} /// [ /// //The folder path /// "path":"/tmp/example" /// //An array of strings showing all files /// "files": /// ["file1.dtsc", /// "file2.mp3", /// "file3.exe" /// ] /// //An array of strings showing all subdirectories /// "subdirectories":[ /// "folder1" /// ] /// ] /// ~~~~~~~~~~~~~~~ if(Request.isMember("browse")){ if(Request["browse"] == ""){ Request["browse"] = "."; } DIR *dir; struct dirent *ent; struct stat filestat; char* rpath = realpath(Request["browse"].asString().c_str(),0); if(rpath == NULL){ Response["browse"]["path"].append(Request["browse"].asString()); }else{ Response["browse"]["path"].append(rpath);//Request["browse"].asString()); if ((dir = opendir (Request["browse"].asString().c_str())) != NULL) { while ((ent = readdir (dir)) != NULL) { if(strcmp(ent->d_name,".")!=0 && strcmp(ent->d_name,"..")!=0 ){ std::string filepath = Request["browse"].asString() + "/" + std::string(ent->d_name); if (stat( filepath.c_str(), &filestat )) continue; if (S_ISDIR( filestat.st_mode)){ Response["browse"]["subdirectories"].append(ent->d_name); }else{ Response["browse"]["files"].append(ent->d_name); } } } closedir (dir); } } free(rpath); } /// /// \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. /// { tthread::lock_guard guard(logMutex); Response["log"] = Controller::Storage["log"]; //clear log 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"]); } } Controller::writeConfig(); }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; }