New licensing system
By Wouter Spruit and Balder Viëtor with minor edits by me
This commit is contained in:
parent
d0c81bb9a0
commit
2062d83858
10 changed files with 252 additions and 24 deletions
|
@ -86,6 +86,9 @@ endif()
|
||||||
if (NOT DEFINED NOUPDATE )
|
if (NOT DEFINED NOUPDATE )
|
||||||
add_definitions(-DUPDATER=1)
|
add_definitions(-DUPDATER=1)
|
||||||
endif()
|
endif()
|
||||||
|
if (NOT DEFINED PERPETUAL )
|
||||||
|
add_definitions(-DLICENSING=1)
|
||||||
|
endif()
|
||||||
if (DEFINED NOAUTH )
|
if (DEFINED NOAUTH )
|
||||||
add_definitions(-DNOAUTH=1)
|
add_definitions(-DNOAUTH=1)
|
||||||
endif()
|
endif()
|
||||||
|
@ -498,6 +501,7 @@ set(controllerHeaders
|
||||||
${SOURCE_DIR}/src/controller/controller_capabilities.h
|
${SOURCE_DIR}/src/controller/controller_capabilities.h
|
||||||
${SOURCE_DIR}/src/controller/controller_streams.h
|
${SOURCE_DIR}/src/controller/controller_streams.h
|
||||||
${SOURCE_DIR}/src/controller/controller_push.h
|
${SOURCE_DIR}/src/controller/controller_push.h
|
||||||
|
${SOURCE_DIR}/src/controller/controller_license.h
|
||||||
)
|
)
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
|
@ -515,6 +519,7 @@ set(controllerSources
|
||||||
${SOURCE_DIR}/src/controller/controller_uplink.cpp
|
${SOURCE_DIR}/src/controller/controller_uplink.cpp
|
||||||
${SOURCE_DIR}/src/controller/controller_api.cpp
|
${SOURCE_DIR}/src/controller/controller_api.cpp
|
||||||
${SOURCE_DIR}/src/controller/controller_push.cpp
|
${SOURCE_DIR}/src/controller/controller_push.cpp
|
||||||
|
${SOURCE_DIR}/src/controller/controller_license.cpp
|
||||||
)
|
)
|
||||||
########################################
|
########################################
|
||||||
# MistController - Build #
|
# MistController - Build #
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
#include "controller_updater.h"
|
#include "controller_updater.h"
|
||||||
#include "controller_limits.h"
|
#include "controller_limits.h"
|
||||||
#include "controller_uplink.h"
|
#include "controller_uplink.h"
|
||||||
|
#include "controller_license.h"
|
||||||
/*LTS-END*/
|
/*LTS-END*/
|
||||||
#include "controller_api.h"
|
#include "controller_api.h"
|
||||||
#include "controller_push.h"
|
#include "controller_push.h"
|
||||||
|
@ -307,15 +308,28 @@ int main_loop(int argc, char ** argv){
|
||||||
|
|
||||||
Controller::Log("CONF", "Controller started");
|
Controller::Log("CONF", "Controller started");
|
||||||
Controller::conf.activate();//activate early, so threads aren't killed.
|
Controller::conf.activate();//activate early, so threads aren't killed.
|
||||||
|
//Generate instanceId once per boot.
|
||||||
|
if (Controller::instanceId == ""){
|
||||||
|
srand(time(NULL));
|
||||||
|
do{
|
||||||
|
Controller::instanceId += (char)(64 + rand() % 62);
|
||||||
|
}while(Controller::instanceId.size() < 16);
|
||||||
|
}
|
||||||
|
|
||||||
/*LTS-START*/
|
/*LTS-START*/
|
||||||
#ifdef UPDATER
|
#ifdef UPDATER
|
||||||
if (Controller::conf.getBool("update")){
|
if (Controller::conf.getBool("update")){
|
||||||
Controller::CheckUpdates();
|
Controller::CheckUpdates();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef LICENSING
|
||||||
|
Controller::initLicense();
|
||||||
|
Controller::checkLicense();
|
||||||
|
//start license checking thread
|
||||||
|
tthread::thread licenseThread(Controller::licenseLoop, 0);
|
||||||
|
#endif
|
||||||
/*LTS-END*/
|
/*LTS-END*/
|
||||||
|
|
||||||
//start stats thread
|
//start stats thread
|
||||||
tthread::thread statsThread(Controller::SharedMemStats, &Controller::conf);
|
tthread::thread statsThread(Controller::SharedMemStats, &Controller::conf);
|
||||||
//start monitoring thread
|
//start monitoring thread
|
||||||
|
@ -324,6 +338,7 @@ int main_loop(int argc, char ** argv){
|
||||||
tthread::thread uplinkThread(Controller::uplinkConnection, 0);/*LTS*/
|
tthread::thread uplinkThread(Controller::uplinkConnection, 0);/*LTS*/
|
||||||
//start push checking thread
|
//start push checking thread
|
||||||
tthread::thread pushThread(Controller::pushCheckLoop, 0);
|
tthread::thread pushThread(Controller::pushCheckLoop, 0);
|
||||||
|
|
||||||
|
|
||||||
//start main loop
|
//start main loop
|
||||||
while (Controller::conf.is_active){/*LTS*/
|
while (Controller::conf.is_active){/*LTS*/
|
||||||
|
@ -339,6 +354,11 @@ int main_loop(int argc, char ** argv){
|
||||||
shutdown_reason = "restart (on request)";
|
shutdown_reason = "restart (on request)";
|
||||||
}
|
}
|
||||||
/*LTS-START*/
|
/*LTS-START*/
|
||||||
|
#ifdef LICENSING
|
||||||
|
if (!Controller::isLicensed()){
|
||||||
|
shutdown_reason = "no valid license";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if(Triggers::shouldTrigger("SYSTEM_STOP")){
|
if(Triggers::shouldTrigger("SYSTEM_STOP")){
|
||||||
if (!Triggers::doTrigger("SYSTEM_STOP", shutdown_reason)){
|
if (!Triggers::doTrigger("SYSTEM_STOP", shutdown_reason)){
|
||||||
Controller::conf.is_active = true;
|
Controller::conf.is_active = true;
|
||||||
|
@ -359,8 +379,13 @@ int main_loop(int argc, char ** argv){
|
||||||
//join all joinable threads
|
//join all joinable threads
|
||||||
statsThread.join();
|
statsThread.join();
|
||||||
monitorThread.join();
|
monitorThread.join();
|
||||||
uplinkThread.join();/*LTS*/
|
/*LTS-START*/
|
||||||
pushThread.join();/*LTS*/
|
uplinkThread.join();
|
||||||
|
pushThread.join();
|
||||||
|
#ifdef LICENSING
|
||||||
|
licenseThread.join();
|
||||||
|
#endif
|
||||||
|
/*LTS-END*/
|
||||||
//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::Storage.removeMember("log");
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
#include "controller_updater.h"
|
#include "controller_updater.h"
|
||||||
#include "controller_limits.h"
|
#include "controller_limits.h"
|
||||||
#include "controller_push.h"
|
#include "controller_push.h"
|
||||||
|
#include "controller_license.h"
|
||||||
/*LTS-END*/
|
/*LTS-END*/
|
||||||
|
|
||||||
///\brief Check the submitted configuration and handle things accordingly.
|
///\brief Check the submitted configuration and handle things accordingly.
|
||||||
|
@ -512,6 +513,11 @@ int Controller::handleAPIConnection(Socket::Connection & conn){
|
||||||
if (!Request.isMember("minimal") || Response.isMember("config")){
|
if (!Request.isMember("minimal") || Response.isMember("config")){
|
||||||
Response["config"] = Controller::Storage["config"];
|
Response["config"] = Controller::Storage["config"];
|
||||||
Response["config"]["version"] = PACKAGE_VERSION;
|
Response["config"]["version"] = PACKAGE_VERSION;
|
||||||
|
/*LTS-START*/
|
||||||
|
#ifdef LICENSING
|
||||||
|
Response["config"]["license"] = getLicense();
|
||||||
|
#endif
|
||||||
|
/*LTS-END*/
|
||||||
//add required data to the current unix time to the config, for syncing reasons
|
//add required data to the current unix time to the config, for syncing reasons
|
||||||
Response["config"]["time"] = Util::epoch();
|
Response["config"]["time"] = Util::epoch();
|
||||||
if ( !Response["config"].isMember("serverid")){
|
if ( !Response["config"].isMember("serverid")){
|
||||||
|
|
189
src/controller/controller_license.cpp
Normal file
189
src/controller/controller_license.cpp
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
#include "controller_license.h"
|
||||||
|
#include "controller_storage.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <mist/defines.h>
|
||||||
|
#include <mist/http_parser.h>
|
||||||
|
#include <mist/socket.h>
|
||||||
|
#include <mist/auth.h>
|
||||||
|
#include <mist/timing.h>
|
||||||
|
#include <mist/config.h>
|
||||||
|
#include <mist/encryption.h>
|
||||||
|
#include <mist/encode.h>
|
||||||
|
|
||||||
|
|
||||||
|
namespace Controller{
|
||||||
|
|
||||||
|
static JSON::Value currentLicense;
|
||||||
|
|
||||||
|
const JSON::Value & getLicense(){
|
||||||
|
return currentLicense;
|
||||||
|
}
|
||||||
|
|
||||||
|
//PACKAGE_VERSION = MistServer version
|
||||||
|
//RELEASE = OS + user_ID
|
||||||
|
|
||||||
|
void initLicense(){
|
||||||
|
if (Storage.isMember("license") && Storage.isMember("license_id")){
|
||||||
|
INFO_MSG("Reading license from storage")
|
||||||
|
readLicense(Storage["license_id"].asInt(), Storage["license"].asStringRef());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool isLicensed(){
|
||||||
|
uint64_t now = Util::epoch();
|
||||||
|
#if DEBUG >= DLVL_DEVEL
|
||||||
|
INFO_MSG("Verifying license against %llu:", now);
|
||||||
|
std::cout << currentLicense.toPrettyString() << std::endl;
|
||||||
|
#endif
|
||||||
|
//The loop below is timechecker loop
|
||||||
|
if (!currentLicense.isMember("valid_from") || !currentLicense.isMember("valid_till")){
|
||||||
|
#if DEBUG >= DLVL_DEVEL
|
||||||
|
INFO_MSG("Accepting license despite lack of date range, because early build");
|
||||||
|
#endif
|
||||||
|
return true;//temporary
|
||||||
|
}
|
||||||
|
if (now < currentLicense["valid_from"].asInt() || now > currentLicense["valid_till"].asInt()){
|
||||||
|
return false;//license is expired
|
||||||
|
}
|
||||||
|
if (RELEASE != currentLicense["release"].asStringRef() || PACKAGE_VERSION != currentLicense["version"].asStringRef()){
|
||||||
|
FAIL_MSG("Could not verify license");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//everything seems okay
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkLicense(){
|
||||||
|
updateLicense();
|
||||||
|
if (!currentLicense.isMember("interval")){
|
||||||
|
currentLicense["interval"] = 3600ll;
|
||||||
|
}
|
||||||
|
INFO_MSG("Checking license time");
|
||||||
|
if(!isLicensed()){
|
||||||
|
FAIL_MSG("License expired, shutting down");
|
||||||
|
kill(getpid(), SIGINT);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseKey(std::string key, char * newKey, unsigned int len){
|
||||||
|
memset(newKey, 0, len);
|
||||||
|
for (size_t i = 0; i < key.size() && i < (len << 1); ++i){
|
||||||
|
char c = key[i];
|
||||||
|
newKey[i>>1] |= ((c&15) + (((c&64)>>6) | ((c&64)>>3))) << ((~i&1) << 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateLicense(){
|
||||||
|
INFO_MSG("Running license updater");
|
||||||
|
JSON::Value response;
|
||||||
|
|
||||||
|
HTTP::Parser http;
|
||||||
|
Socket::Connection updrConn("releases.mistserver.org", 80, true);
|
||||||
|
if ( !updrConn){
|
||||||
|
WARN_MSG("Failed to reach licensing server");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Sending request to server.
|
||||||
|
//http.url = "/licensing.php"
|
||||||
|
//also see statics at start function.
|
||||||
|
http.url = "/license.php?release="+Encodings::URL::encode(RELEASE)+"&version="+Encodings::URL::encode(PACKAGE_VERSION)+"&iid="+Encodings::URL::encode(instanceId)+"&lid="+currentLicense["lic_id"].asString();
|
||||||
|
long long currID = currentLicense["lic_id"].asInt();
|
||||||
|
http.method = "GET";
|
||||||
|
http.SetHeader("Host", "releases.mistserver.org");
|
||||||
|
http.SetHeader("X-Version", PACKAGE_VERSION);
|
||||||
|
if (currID){
|
||||||
|
char aesKey[16];
|
||||||
|
if (strlen(SUPER_SECRET) >= 32){
|
||||||
|
parseKey(SUPER_SECRET SUPER_SECRET + 7,aesKey,16);
|
||||||
|
}else{
|
||||||
|
parseKey("4E56721C67306E1F473156F755FF5570",aesKey,16);
|
||||||
|
}
|
||||||
|
for (unsigned int i = 0; i < 8; ++i){
|
||||||
|
aesKey[15-i] = ((currID >> (i*8)) + aesKey[15-i]) & 0xFF;
|
||||||
|
}
|
||||||
|
char ivec[16];
|
||||||
|
memset(ivec, 0, 16);
|
||||||
|
http.SetHeader("X-IRDGAF", Encodings::Base64::encode(Encryption::AES_Crypt(RELEASE "|" PACKAGE_VERSION, sizeof(RELEASE "|" PACKAGE_VERSION), aesKey, ivec)));
|
||||||
|
}
|
||||||
|
updrConn.SendNow(http.BuildRequest());
|
||||||
|
http.Clean();
|
||||||
|
unsigned int startTime = Util::epoch();
|
||||||
|
while ((Util::epoch() - startTime < 10) && (updrConn || updrConn.Received().size())){
|
||||||
|
if (updrConn.spool() || updrConn.Received().size()){
|
||||||
|
if ( *(updrConn.Received().get().rbegin()) != '\n'){
|
||||||
|
std::string tmp = updrConn.Received().get();
|
||||||
|
updrConn.Received().get().clear();
|
||||||
|
if (updrConn.Received().size()){
|
||||||
|
updrConn.Received().get().insert(0, tmp);
|
||||||
|
}else{
|
||||||
|
updrConn.Received().append(tmp);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (http.Read(updrConn.Received().get())){
|
||||||
|
response = JSON::fromString(http.body);
|
||||||
|
break; //break out of while loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updrConn.close();
|
||||||
|
|
||||||
|
//read license
|
||||||
|
readLicense(response["lic_id"].asInt(), response["license"].asStringRef());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void readLicense(uint64_t licID, const std::string & input){
|
||||||
|
char aesKey[16];
|
||||||
|
if (strlen(SUPER_SECRET) >= 32){
|
||||||
|
parseKey(SUPER_SECRET SUPER_SECRET + 7,aesKey,16);
|
||||||
|
}else{
|
||||||
|
parseKey("4E56721C67306E1F473156F755FF5570",aesKey,16);
|
||||||
|
}
|
||||||
|
for (unsigned int i = 0; i < 8; ++i){
|
||||||
|
aesKey[15-i] = ((licID >> (i*8)) + aesKey[15-i]) & 0xFF;
|
||||||
|
}
|
||||||
|
std::string cipher = Encodings::Base64::decode(input);
|
||||||
|
std::string deCrypted;
|
||||||
|
//magic ivecs, they are empty. It's secretly 16 times \0.
|
||||||
|
char ivec[16];
|
||||||
|
memset(ivec, 0, 16);
|
||||||
|
deCrypted = Encryption::AES_Crypt(cipher.c_str(), cipher.size(), aesKey, ivec);
|
||||||
|
//get time stamps and license.
|
||||||
|
|
||||||
|
//verify checksum
|
||||||
|
if (deCrypted.size() < 33 || Secure::md5(deCrypted.substr(32)) != deCrypted.substr(0,32)){
|
||||||
|
FAIL_MSG("Could not decode license");
|
||||||
|
Storage.removeMember("license");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentLicense = JSON::fromString(deCrypted.substr(32));
|
||||||
|
if (RELEASE != currentLicense["release"].asStringRef() || PACKAGE_VERSION != currentLicense["version"].asStringRef()){
|
||||||
|
FAIL_MSG("Could not verify license");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Store license here.
|
||||||
|
if (currentLicense["store"].asBool()){
|
||||||
|
if (Storage["license"].asStringRef() != input){
|
||||||
|
Storage["license"] = input;
|
||||||
|
Storage["license_id"] = (long long)licID;
|
||||||
|
INFO_MSG("Stored license for offline use");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void licenseLoop(void * np){
|
||||||
|
unsigned long now = Util::epoch();
|
||||||
|
while (conf.is_active){
|
||||||
|
if (Util::epoch() - now > currentLicense["interval"].asInt()){
|
||||||
|
if (checkLicense()){
|
||||||
|
now = Util::epoch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Util::wait(2000);//wait at least 2 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/controller/controller_license.h
Normal file
17
src/controller/controller_license.h
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
#include <mist/json.h>
|
||||||
|
|
||||||
|
namespace Controller{
|
||||||
|
|
||||||
|
const JSON::Value & getLicense();
|
||||||
|
void initLicense();
|
||||||
|
bool isLicensed(); //checks/verifies license time
|
||||||
|
bool checkLicense(); //Call from Mainloop.
|
||||||
|
void updateLicense(); //retrieves update from license server
|
||||||
|
void licenseLoop(void * np);
|
||||||
|
void readLicense(uint64_t licId, const std::string & input); //checks/interprets license
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
///\brief Holds everything unique to the controller.
|
///\brief Holds everything unique to the controller.
|
||||||
namespace Controller {
|
namespace Controller {
|
||||||
|
std::string instanceId; ///instanceId (previously uniqId) is first set in controller.cpp before licensing or update calls.
|
||||||
Util::Config conf;
|
Util::Config conf;
|
||||||
JSON::Value Storage; ///< Global storage of data.
|
JSON::Value Storage; ///< Global storage of data.
|
||||||
tthread::mutex configMutex;
|
tthread::mutex configMutex;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include <mist/tinythread.h>
|
#include <mist/tinythread.h>
|
||||||
|
|
||||||
namespace Controller {
|
namespace Controller {
|
||||||
|
extern std::string instanceId; ///global storage of instanceId (previously uniqID) for updater
|
||||||
extern Util::Config conf;///< Global storage of configuration.
|
extern Util::Config conf;///< Global storage of configuration.
|
||||||
extern JSON::Value Storage; ///< Global storage of data.
|
extern JSON::Value Storage; ///< Global storage of data.
|
||||||
extern tthread::mutex logMutex;///< Mutex for log thread.
|
extern tthread::mutex logMutex;///< Mutex for log thread.
|
||||||
|
|
|
@ -17,9 +17,10 @@
|
||||||
#include "controller_connectors.h"
|
#include "controller_connectors.h"
|
||||||
#include "controller_updater.h"
|
#include "controller_updater.h"
|
||||||
|
|
||||||
|
|
||||||
namespace Controller {
|
namespace Controller {
|
||||||
JSON::Value updates;
|
JSON::Value updates;
|
||||||
std::string uniqId;
|
|
||||||
|
|
||||||
std::string readFile(std::string filename){
|
std::string readFile(std::string filename){
|
||||||
std::ifstream file(filename.c_str());
|
std::ifstream file(filename.c_str());
|
||||||
|
@ -69,13 +70,6 @@ namespace Controller {
|
||||||
JSON::Value CheckUpdateInfo(){
|
JSON::Value CheckUpdateInfo(){
|
||||||
JSON::Value ret;
|
JSON::Value ret;
|
||||||
|
|
||||||
if (uniqId == ""){
|
|
||||||
srand(time(NULL));
|
|
||||||
do{
|
|
||||||
char meh = 64 + rand() % 62;
|
|
||||||
uniqId += meh;
|
|
||||||
}while(uniqId.size() < 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
//initialize connection
|
//initialize connection
|
||||||
HTTP::Parser http;
|
HTTP::Parser http;
|
||||||
|
@ -88,7 +82,7 @@ namespace Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
//retrieve update information
|
//retrieve update information
|
||||||
http.url = "/getsums.php?verinfo=1&rel=" RELEASE "&pass=" SHARED_SECRET "&uniqId=" + uniqId;
|
http.url = "/getsums.php?verinfo=1&rel=" RELEASE "&pass=" SHARED_SECRET "&iid=" + instanceId;
|
||||||
http.method = "GET";
|
http.method = "GET";
|
||||||
http.SetHeader("Host", "releases.mistserver.org");
|
http.SetHeader("Host", "releases.mistserver.org");
|
||||||
http.SetHeader("X-Version", PACKAGE_VERSION);
|
http.SetHeader("X-Version", PACKAGE_VERSION);
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
|
|
||||||
namespace Controller {
|
namespace Controller {
|
||||||
extern JSON::Value updates;
|
extern JSON::Value updates;
|
||||||
extern std::string uniqId;
|
|
||||||
|
|
||||||
std::string readFile(std::string filename);
|
std::string readFile(std::string filename);
|
||||||
bool writeFile(std::string filename, std::string & contents);
|
bool writeFile(std::string filename, std::string & contents);
|
||||||
|
|
|
@ -33,14 +33,6 @@ void Controller::uplinkConnection(void * np) {
|
||||||
if (!uplink_port) {
|
if (!uplink_port) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uniqId == ""){
|
|
||||||
srand(time(NULL));
|
|
||||||
do{
|
|
||||||
char meh = 64 + rand() % 62;
|
|
||||||
uniqId += meh;
|
|
||||||
}while(uniqId.size() < 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long long lastSend = Util::epoch() - 5;
|
unsigned long long lastSend = Util::epoch() - 5;
|
||||||
Socket::Connection uplink;
|
Socket::Connection uplink;
|
||||||
|
@ -119,7 +111,7 @@ void Controller::uplinkConnection(void * np) {
|
||||||
it->removeMember("name");
|
it->removeMember("name");
|
||||||
}
|
}
|
||||||
data["config"] = Controller::Storage["config"];
|
data["config"] = Controller::Storage["config"];
|
||||||
data["config"]["uniq"] = uniqId;
|
data["config"]["uniq"] = instanceId;
|
||||||
data["config"]["version"] = PACKAGE_VERSION;
|
data["config"]["version"] = PACKAGE_VERSION;
|
||||||
Controller::checkCapable(capabilities);
|
Controller::checkCapable(capabilities);
|
||||||
data["capabilities"] = capabilities;
|
data["capabilities"] = capabilities;
|
||||||
|
|
Loading…
Add table
Reference in a new issue