Backported many Pro API calls to OS edition, improved storage method of config file

This commit is contained in:
Thulinma 2017-05-08 10:20:00 +02:00
parent 502ed31ef7
commit 16637b3138
5 changed files with 275 additions and 254 deletions

View file

@ -311,16 +311,7 @@ int main_loop(int argc, char ** argv){
monitorThread.join(); monitorThread.join();
//write config //write config
tthread::lock_guard<tthread::mutex> guard(Controller::logMutex); tthread::lock_guard<tthread::mutex> guard(Controller::logMutex);
Controller::Storage.removeMember("log"); Controller::writeConfigToDisk();
jsonForEach(Controller::Storage["streams"], it) {
it->removeMember("meta");
}
if ( !Controller::WriteFile(Controller::conf.getString("configFile"), Controller::Storage.toString())){
std::cerr << "Error writing config " << Controller::conf.getString("configFile") << std::endl;
std::cerr << "**Config**" << std::endl;
std::cerr << Controller::Storage.toString() << std::endl;
std::cerr << "**End config**" << std::endl;
}
//stop all child processes //stop all child processes
Util::Procs::StopAll(); Util::Procs::StopAll();
//give everything some time to print messages //give everything some time to print messages

View file

@ -12,60 +12,6 @@
#include "controller_capabilities.h" #include "controller_capabilities.h"
#include "controller_statistics.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.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. ///\brief Checks an authorization request for a given user.
///\param Request The request to be parsed. ///\param Request The request to be parsed.
///\param Response The location to store the generated response. ///\param Response The location to store the generated response.
@ -159,7 +105,7 @@ int Controller::handleAPIConnection(Socket::Connection & conn){
JSON::Value Response; JSON::Value Response;
JSON::Value Request = JSON::fromString(H.GetVar("command")); JSON::Value Request = JSON::fromString(H.GetVar("command"));
//invalid request? send the web interface, unless requested as "/api" //invalid request? send the web interface, unless requested as "/api"
if ( !Request.isObject() && H.url != "/api"){ if ( !Request.isObject() && H.url != "/api" && H.url != "/api2"){
#include "server.html.h" #include "server.html.h"
H.Clean(); H.Clean();
H.SetHeader("Content-Type", "text/html"); H.SetHeader("Content-Type", "text/html");
@ -172,6 +118,9 @@ int Controller::handleAPIConnection(Socket::Connection & conn){
H.Clean(); H.Clean();
break; break;
} }
if (H.url == "/api2"){
Request["minimal"] = true;
}
{//lock the config mutex here - do not unlock until done processing {//lock the config mutex here - do not unlock until done processing
tthread::lock_guard<tthread::mutex> guard(configMutex); tthread::lock_guard<tthread::mutex> guard(configMutex);
//Are we local and not forwarded? Instant-authorized. //Are we local and not forwarded? Instant-authorized.
@ -186,189 +135,7 @@ int Controller::handleAPIConnection(Socket::Connection & conn){
authorized |= authorize(Request, Response, conn); authorized |= authorize(Request, Response, conn);
} }
if (authorized){ if (authorized){
//Parse config and streams from the request. handleAPICommands(Request, Response);
if (Request.isMember("config")){
Controller::checkConfig(Request["config"], Controller::Storage["config"]);
}
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");
}
}
///
/// \api
/// `"ui_settings"` requests can take two forms. The first is the "set" form:
/// ~~~~~~~~~~~~~~~{.js}
/// {
/// //Any data here
/// }
/// ~~~~~~~~~~~~~~~
/// The second is the "request" form, and takes any non-object as argument.
/// When using the set form, this will write the given object verbatim into the controller storage.
/// No matter which form is used, the current contents of the ui_settings object are always returned in the response.
/// This API call is intended to store User Interface settings across sessions, and its contents are completely ignored by the controller itself. Besides the requirement of being an object, the contents are entirely free-form and may technically be used for any purpose.
///
if (Request.isMember("ui_settings")){
if (Request["ui_settings"].isObject()){
Storage["ui_settings"] = Request["ui_settings"];
}
Response["ui_settings"] = Storage["ui_settings"];
}
//sent current configuration, no matter if it was changed or not
Response["config"] = Controller::Storage["config"];
Response["config"]["version"] = PACKAGE_VERSION;
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<tthread::mutex> 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 }else{//unauthorized
Util::sleep(1000);//sleep a second to prevent bruteforcing Util::sleep(1000);//sleep a second to prevent bruteforcing
logins++; logins++;
@ -396,3 +163,245 @@ int Controller::handleAPIConnection(Socket::Connection & conn){
}//while connected }//while connected
return 0; return 0;
} }
/// Local-only helper function that checks for duplicate protocols and removes them
static void removeDuplicateProtocols(){
JSON::Value & P = Controller::Storage["config"]["protocols"];
std::set<std::string> ignores;
ignores.insert("online");
bool reloop = true;
while (reloop){
reloop = false;
jsonForEach(P, it){
jsonForEach(P, jt){
if (it.num() == jt.num()){continue;}
if ((*it).compareExcept(*jt, ignores)){
jt.remove();
reloop = true;
break;
}
}
if (reloop){break;}
}
}
}
void Controller::handleAPICommands(JSON::Value & Request, JSON::Value & Response){
//Parse config and streams from the request.
if (Request.isMember("config") && Request["config"].isObject()){
const JSON::Value & in = Request["config"];
JSON::Value & out = Controller::Storage["config"];
if (in.isMember("debug")){
out["debug"] = in["debug"];
if (Util::Config::printDebugLevel != out["debug"].asInt()){
Util::Config::printDebugLevel = out["debug"].asInt();
INFO_MSG("Debug level set to %u", Util::Config::printDebugLevel);
}
}
if (in.isMember("protocols")){
out["protocols"] = in["protocols"];
removeDuplicateProtocols();
}
if (in.isMember("controller")){
out["controller"] = in["controller"];
}
if (in.isMember("serverid")){
out["serverid"] = in["serverid"];
}
}
if (Request.isMember("streams")){
Controller::CheckStreams(Request["streams"], Controller::Storage["streams"]);
}
if (Request.isMember("addstream")){
Controller::AddStreams(Request["addstream"], Controller::Storage["streams"]);
}
if (Request.isMember("deletestream")){
//if array, delete all elements
//if object, delete all entries
//if string, delete just the one
if (Request["deletestream"].isString()){
Controller::deleteStream(Request["deletestream"].asStringRef(), Controller::Storage["streams"]);
}
if (Request["deletestream"].isArray()){
jsonForEach(Request["deletestream"], it){
Controller::deleteStream(it->asStringRef(), Controller::Storage["streams"]);
}
}
if (Request["deletestream"].isObject()){
jsonForEach(Request["deletestream"], it){
Controller::deleteStream(it.key(), Controller::Storage["streams"]);
}
}
}
if (Request.isMember("addprotocol")){
if (Request["addprotocol"].isArray()){
jsonForEach(Request["addprotocol"], it){
Controller::Storage["config"]["protocols"].append(*it);
}
}
if (Request["addprotocol"].isObject()){
Controller::Storage["config"]["protocols"].append(Request["addprotocol"]);
}
removeDuplicateProtocols();
}
if (Request.isMember("deleteprotocol")){
std::set<std::string> ignores;
ignores.insert("online");
if (Request["deleteprotocol"].isArray() && Request["deleteprotocol"].size()){
JSON::Value newProtocols;
jsonForEach(Controller::Storage["config"]["protocols"], it){
bool add = true;
jsonForEach(Request["deleteprotocol"], pit){
if ((*it).compareExcept(*pit, ignores)){
add = false;
break;
}
}
if (add){
newProtocols.append(*it);
}
}
Controller::Storage["config"]["protocols"] = newProtocols;
}
if (Request["deleteprotocol"].isObject()){
JSON::Value newProtocols;
jsonForEach(Controller::Storage["config"]["protocols"], it){
if (!(*it).compareExcept(Request["deleteprotocol"], ignores)){
newProtocols.append(*it);
}
}
Controller::Storage["config"]["protocols"] = newProtocols;
}
}
if (Request.isMember("updateprotocol")){
std::set<std::string> ignores;
ignores.insert("online");
if (Request["updateprotocol"].isArray() && Request["updateprotocol"].size() == 2){
jsonForEach(Controller::Storage["config"]["protocols"], it){
if ((*it).compareExcept(Request["updateprotocol"][0u], ignores)){
(*it) = Request["updateprotocol"][1u];
}
}
removeDuplicateProtocols();
}else{
FAIL_MSG("Cannot parse updateprotocol call: needs to be in the form [A, B]");
}
}
if (Request.isMember("capabilities")){
Controller::checkCapable(capabilities);
Response["capabilities"] = capabilities;
}
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);
}
if (Request.isMember("save")){
Controller::Log("CONF", "Writing config to file on request through API");
Controller::writeConfigToDisk();
}
if (Request.isMember("ui_settings")){
if (Request["ui_settings"].isObject()){
Storage["ui_settings"] = Request["ui_settings"];
}
Response["ui_settings"] = Storage["ui_settings"];
}
if (!Request.isMember("minimal") || Request.isMember("streams") || Request.isMember("addstream") || Request.isMember("deletestream")){
if (!Request.isMember("streams") && (Request.isMember("addstream") || Request.isMember("deletestream"))){
Response["streams"]["incomplete list"] = 1ll;
if (Request.isMember("addstream")){
jsonForEach(Request["addstream"], jit){
if (Controller::Storage["streams"].isMember(jit.key())){
Response["streams"][jit.key()] = Controller::Storage["streams"][jit.key()];
}
}
}
}else{
Response["streams"] = Controller::Storage["streams"];
}
}
//sent current configuration, if not minimal or was changed/requested
if (!Request.isMember("minimal") || Request.isMember("config")){
Response["config"] = Controller::Storage["config"];
Response["config"]["version"] = PACKAGE_VERSION;
//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.
///
if (Request.isMember("clearstatlogs") || Request.isMember("log") || !Request.isMember("minimal")){
tthread::lock_guard<tthread::mutex> guard(logMutex);
if (!Request.isMember("minimal") || Request.isMember("log")){
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::configChanged = true;
}

View file

@ -2,7 +2,7 @@
#include <mist/json.h> #include <mist/json.h>
namespace Controller { namespace Controller {
void checkConfig(JSON::Value & in, JSON::Value & out);
bool authorize(JSON::Value & Request, JSON::Value & Response, Socket::Connection & conn); bool authorize(JSON::Value & Request, JSON::Value & Response, Socket::Connection & conn);
int handleAPIConnection(Socket::Connection & conn); int handleAPIConnection(Socket::Connection & conn);
void handleAPICommands(JSON::Value & Request, JSON::Value & Response);
} }

View file

@ -90,18 +90,38 @@ namespace Controller {
fclose(output); fclose(output);
close((long long int)err); close((long long int)err);
} }
/// Writes the current config to the location set in the configFile setting.
/// On error, prints an error-level message and the config to stdout.
void writeConfigToDisk(){
JSON::Value tmp;
std::set<std::string> skip;
skip.insert("log");
skip.insert("online");
skip.insert("error");
tmp.assignFrom(Controller::Storage, skip);
if ( !Controller::WriteFile(Controller::conf.getString("configFile"), tmp.toString())){
ERROR_MSG("Error writing config to %s", Controller::conf.getString("configFile").c_str());
std::cout << "**Config**" << std::endl;
std::cout << tmp.toString() << std::endl;
std::cout << "**End config**" << std::endl;
}
}
/// Writes the current config to shared memory to be used in other processes /// Writes the current config to shared memory to be used in other processes
void writeConfig(){ void writeConfig(){
static JSON::Value writeConf; static JSON::Value writeConf;
bool changed = false; bool changed = false;
if (writeConf["config"] != Storage["config"]){ std::set<std::string> skip;
writeConf["config"] = Storage["config"]; skip.insert("online");
skip.insert("error");
if (!writeConf["config"].compareExcept(Storage["config"], skip)){
writeConf["config"].assignFrom(Storage["config"], skip);
VERYHIGH_MSG("Saving new config because of edit in server config structure"); VERYHIGH_MSG("Saving new config because of edit in server config structure");
changed = true; changed = true;
} }
if (writeConf["streams"] != Storage["streams"]){ if (!writeConf["streams"].compareExcept(Storage["streams"], skip)){
writeConf["streams"] = Storage["streams"]; writeConf["streams"].assignFrom(Storage["streams"], skip);
VERYHIGH_MSG("Saving new config because of edit in streams"); VERYHIGH_MSG("Saving new config because of edit in streams");
changed = true; changed = true;
} }

View file

@ -18,6 +18,7 @@ namespace Controller {
/// Write contents to Filename. /// Write contents to Filename.
bool WriteFile(std::string Filename, std::string contents); bool WriteFile(std::string Filename, std::string contents);
void writeConfigToDisk();
void handleMsg(void * err); void handleMsg(void * err);