diff --git a/build_scripts/OBS b/build_scripts/OBS
index c064abd74..879d5dfe8 160000
--- a/build_scripts/OBS
+++ b/build_scripts/OBS
@@ -1 +1 @@
-Subproject commit c064abd74b27e1cc440917e9dbac800316bb8470
+Subproject commit 879d5dfe8dcd8995be753120cf1b8bab4dd2ec82
diff --git a/libretroshare/src/jsonapi/jsonapi.cpp b/libretroshare/src/jsonapi/jsonapi.cpp
index 3888117a5..0c180ce52 100644
--- a/libretroshare/src/jsonapi/jsonapi.cpp
+++ b/libretroshare/src/jsonapi/jsonapi.cpp
@@ -16,7 +16,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see
*
- * SPDX-FileCopyrightText: 2004-2019 RetroShare Team
+ * SPDX-FileCopyrightText: 2004-2020 RetroShare Team
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -73,6 +73,8 @@ JsonApiServer::corsOptionsHeaders =
{ "Content-Length", "0" }
};
+/* static */ const RsJsonApiErrorCategory RsJsonApiErrorCategory::instance;
+
#define INITIALIZE_API_CALL_JSON_CONTEXT \
RsGenericSerializer::SerializeContext cReq( \
nullptr, 0, \
@@ -124,32 +126,45 @@ JsonApiServer::corsOptionsHeaders =
return false;
}
+void JsonApiServer::unProtectedRestart()
+{
+ /* Extremely sensitive stuff!
+ * Make sure you read documentation in header before changing or use!! */
+
+ fullstop();
+ RsThread::start("JSON API Server");
+}
+
bool RsJsonApi::parseToken(
const std::string& clear_token, std::string& user,std::string& passwd )
{
- uint32_t colonIndex = 0;
- uint32_t colonCounter = 0;
+ auto colonIndex = std::string::npos;
const auto tkLen = clear_token.length();
- for(uint32_t i=0; i < tkLen && colonCounter < 2; ++i)
- if(clear_token[i] == ':') { ++colonCounter; colonIndex = i; }
- else if(!librs::util::is_alphanumeric(clear_token[i])) return false;
+ for(uint32_t i=0; i < tkLen; ++i)
+ if(clear_token[i] == ':') { colonIndex = i; break; }
- if(colonCounter != 1) return false;
+ user = clear_token.substr(0, colonIndex);
- user = clear_token.substr(0, colonIndex);
- passwd = clear_token.substr(colonIndex + 1);
+ if(colonIndex < tkLen)
+ passwd = clear_token.substr(colonIndex + 1);
return true;
}
-
JsonApiServer::JsonApiServer(): configMutex("JsonApiServer config"),
- mService(std::make_shared()),
- mServiceMutex("JsonApiServer restbed ptr"),
+ mService(nullptr),
mListeningPort(RsJsonApi::DEFAULT_PORT),
- mBindingAddress(RsJsonApi::DEFAULT_BINDING_ADDRESS)
+ mBindingAddress(RsJsonApi::DEFAULT_BINDING_ADDRESS),
+ mRestartReqTS(0)
{
+#if defined(RS_THREAD_FORCE_STOP) && defined(RS_JSONAPI_DEBUG_SERVICE_STOP)
+ /* When called in bursts it seems that Restbed::Service::stop() doesn't
+ * always does the job, to debug those cases it has been useful to ask
+ * RsThread to force it to stop for us. */
+ RsThread::setStopTimeout(10);
+#endif
+
registerHandler("/rsLoginHelper/createLocation",
[this](const std::shared_ptr session)
{
@@ -248,7 +263,13 @@ JsonApiServer::JsonApiServer(): configMutex("JsonApiServer config"),
{
INITIALIZE_API_CALL_JSON_CONTEXT;
DEFAULT_API_CALL_JSON_RETURN(rb::OK);
- rsControl->rsGlobalShutDown();
+
+ /* Wrap inside RsThread::async because this call
+ * RsThread::fullstop() also on JSON API server thread.
+ * Calling RsThread::fullstop() from it's own thread should never
+ * happen and if it happens an error message is printed
+ * accordingly by RsThread::fullstop() */
+ RsThread::async([](){ rsControl->rsGlobalShutDown(); });
} );
}, true);
@@ -388,6 +409,41 @@ JsonApiServer::JsonApiServer(): configMutex("JsonApiServer config"),
session->yield(message.str());
} );
}, true);
+
+ registerHandler("/rsJsonApi/restart",
+ [this](const std::shared_ptr session)
+ {
+ auto reqSize = session->get_request()->get_header("Content-Length", 0);
+ session->fetch( static_cast(reqSize), [this](
+ const std::shared_ptr session,
+ const rb::Bytes& body )
+ {
+ INITIALIZE_API_CALL_JSON_CONTEXT;
+
+ std::error_condition retval;
+
+ const auto now = time(nullptr);
+ if(mRestartReqTS.exchange(now) + RESTART_BURST_PROTECTION > now)
+ retval = RsJsonApiErrorNum::NOT_A_MACHINE_GUN;
+
+ // serialize out parameters and return value to JSON
+ {
+ RsGenericSerializer::SerializeContext& ctx(cAns);
+ RsGenericSerializer::SerializeJob j(RsGenericSerializer::TO_JSON);
+ RS_SERIAL_PROCESS(retval);
+ }
+
+ DEFAULT_API_CALL_JSON_RETURN(rb::OK);
+
+ /* Wrap inside RsThread::async because this call fullstop() on
+ * JSON API server thread.
+ * Calling RsThread::fullstop() from it's own thread should never
+ * happen and if it happens an error message is printed
+ * accordingly by RsThread::fullstop() */
+ if(!retval) RsThread::async([this](){ unProtectedRestart(); });
+ } );
+ }, true);
+
// Generated at compile time
#include "jsonapi-wrappers.inl"
}
@@ -405,10 +461,22 @@ void JsonApiServer::registerHandler(
if(requiresAutentication)
resource->set_authentication_handler(
- [this](
+ [this, path](
const std::shared_ptr session,
const std::function)>& callback )
{
+ const auto authFail =
+ [&path, &session](int status) -> RsWarn::stream_type&
+ {
+ /* Capture session by reference as it is cheaper then copying
+ * shared_ptr by value which is not needed in this case */
+
+ session->close(status, corsOptionsHeaders);
+ return RsWarn() << "JsonApiServer authentication handler "
+ "blocked an attempt to call JSON API "
+ "authenticated method: " << path;
+ };
+
if(session->get_request()->get_method() == "OPTIONS")
{
callback(session);
@@ -417,7 +485,8 @@ void JsonApiServer::registerHandler(
if(!rsLoginHelper->isLoggedIn())
{
- session->close(rb::CONFLICT, corsOptionsHeaders);
+ authFail(rb::CONFLICT) << " before RetroShare login"
+ << std::endl;
return;
}
@@ -429,15 +498,24 @@ void JsonApiServer::registerHandler(
if(authToken != "Basic")
{
- session->close(rb::UNAUTHORIZED, corsOptionsHeaders);
+ authFail(rb::UNAUTHORIZED)
+ << " with wrong Authorization header: "
+ << authHeader.str() << std::endl;
return;
}
std::getline(authHeader, authToken, ' ');
authToken = decodeToken(authToken);
- if(isAuthTokenValid(authToken)) callback(session);
- else session->close(rb::UNAUTHORIZED, corsOptionsHeaders);
+ std::error_condition ec;
+ if(isAuthTokenValid(authToken, ec)) callback(session);
+ else
+ {
+ std::string tUser;
+ parseToken(authToken, tUser, RS_DEFAULT_STORAGE_PARAM(std::string));
+ authFail(rb::UNAUTHORIZED)
+ << " user: " << tUser << ec << std::endl;
+ }
} );
mResources.push_back(resource);
@@ -447,24 +525,62 @@ void JsonApiServer::setNewAccessRequestCallback(
const std::function& callback )
{ mNewAccessRequestCallback = callback; }
-bool JsonApiServer::requestNewTokenAutorization(
+/*static*/ std::error_condition JsonApiServer::badApiCredientalsFormat(
const std::string& user, const std::string& passwd )
{
- if(rsLoginHelper->isLoggedIn() && mNewAccessRequestCallback(user, passwd))
- return authorizeUser(user, passwd);
+ if(user.find(':') < std::string::npos)
+ return RsJsonApiErrorNum::API_USER_CONTAIN_COLON;
- return false;
+ if(user.empty())
+ RsWarn() << __PRETTY_FUNCTION__ << " User is empty, are you sure "
+ << "this what you wanted?" << std::endl;
+
+ if(passwd.empty())
+ RsWarn() << __PRETTY_FUNCTION__ << " Password is empty, are you sure "
+ << "this what you wanted?" << std::endl;
+
+ return std::error_condition();
}
-bool JsonApiServer::isAuthTokenValid(const std::string& token)
+std::error_condition JsonApiServer::requestNewTokenAutorization(
+ const std::string& user, const std::string& passwd )
+{
+ auto ec = badApiCredientalsFormat(user, passwd);
+ if(ec) return ec;
+
+ if(!rsLoginHelper->isLoggedIn())
+ return RsJsonApiErrorNum::CANNOT_EXECUTE_BEFORE_RS_LOGIN;
+
+ if(mNewAccessRequestCallback(user, passwd))
+ return authorizeUser(user, passwd);
+
+ return RsJsonApiErrorNum::AUTHORIZATION_REQUEST_DENIED;
+}
+
+bool JsonApiServer::isAuthTokenValid(
+ const std::string& token, std::error_condition& error )
{
RS_STACK_MUTEX(configMutex);
+ const auto failure = [&error](RsJsonApiErrorNum e) -> bool
+ {
+ error = e;
+ return false;
+ };
+
+ const auto success = [&error]()
+ {
+ error.clear();
+ return true;
+ };
+
std::string user,passwd;
- if(!parseToken(token,user,passwd)) return false;
+ if(!parseToken(token, user, passwd))
+ return failure(RsJsonApiErrorNum::TOKEN_FORMAT_INVALID);
auto it = mAuthTokenStorage.mAuthorizedTokens.find(user);
- if(it == mAuthTokenStorage.mAuthorizedTokens.end()) return false;
+ if(it == mAuthTokenStorage.mAuthorizedTokens.end())
+ return failure(RsJsonApiErrorNum::UNKNOWN_API_USER);
// attempt avoiding +else CRYPTO_memcmp+ being optimized away
int noOptimiz = 1;
@@ -476,12 +592,16 @@ bool JsonApiServer::isAuthTokenValid(const std::string& token)
if( passwd.size() == it->second.size() &&
( noOptimiz = CRYPTO_memcmp(
passwd.data(), it->second.data(), it->second.size() ) ) == 0 )
- return true;
+ return success();
// Make token size guessing harder
else noOptimiz = CRYPTO_memcmp(passwd.data(), passwd.data(), passwd.size());
- // attempt avoiding +else CRYPTO_memcmp+ being optimized away
- return static_cast(noOptimiz) + 1 == 0;
+ /* At this point we are sure password is wrong, and one could think to
+ * plainly `return false` still this ugly and apparently unuseful extra
+ * calculation is here to avoid `else CRYPTO_memcmp` being optimized away,
+ * so a pontential attacker cannot guess password size based on timing */
+ return static_cast(noOptimiz) + 1 == 0 ?
+ success() : failure(RsJsonApiErrorNum::WRONG_API_PASSWORD);
}
std::map JsonApiServer::getAuthorizedTokens()
@@ -509,22 +629,11 @@ void JsonApiServer::connectToConfigManager(p3ConfigMgr& cfgmgr)
loadConfiguration(hash);
}
-bool JsonApiServer::authorizeUser(
+std::error_condition JsonApiServer::authorizeUser(
const std::string& user, const std::string& passwd )
{
- if(!librs::util::is_alphanumeric(user))
- {
- RsErr() << __PRETTY_FUNCTION__ << " User name is not alphanumeric"
- << std::endl;
- return false;
- }
-
- if(passwd.empty())
- {
- RsWarn() << __PRETTY_FUNCTION__ << " Password is empty, are you sure "
- << "this what you wanted?" << std::endl;
- return false;
- }
+ auto ec = badApiCredientalsFormat(user, passwd);
+ if(ec) return ec;
RS_STACK_MUTEX(configMutex);
@@ -534,7 +643,7 @@ bool JsonApiServer::authorizeUser(
p = passwd;
IndicateConfigChanged();
}
- return true;
+ return ec;
}
/*static*/ std::string JsonApiServer::decodeToken(const std::string& radix64_token)
@@ -600,21 +709,21 @@ std::vector > JsonApiServer::getResources() const
return tab;
}
-void JsonApiServer::restart()
+std::error_condition JsonApiServer::restart()
{
- /* It is important to wrap into async(...) because fullstop() method can't
- * be called from same thread of execution hence from JSON API thread! */
- RsThread::async([this]()
- {
- fullstop();
- RsThread::start("JSON API Server");
- });
+ const auto now = time(nullptr);
+ if(mRestartReqTS.exchange(now) + RESTART_BURST_PROTECTION > now)
+ return RsJsonApiErrorNum::NOT_A_MACHINE_GUN;
+
+ unProtectedRestart();
+ return std::error_condition();
}
void JsonApiServer::onStopRequested()
{
- RS_STACK_MUTEX(mServiceMutex);
- mService->stop();
+ auto tService = std::atomic_exchange(
+ &mService, std::shared_ptr(nullptr) );
+ if(tService) tService->stop();
}
uint16_t JsonApiServer::listeningPort() const { return mListeningPort; }
@@ -630,14 +739,9 @@ void JsonApiServer::run()
settings->set_bind_address(mBindingAddress);
settings->set_default_header("Connection", "close");
- /* re-allocating mService is important because it deletes the existing
- * service and therefore leaves the listening port open */
- {
- RS_STACK_MUTEX(mServiceMutex);
- mService = std::make_shared();
- }
+ auto tService = std::make_shared();
- for(auto& r: getResources()) mService->publish(r);
+ for(auto& r: getResources()) tService->publish(r);
try
{
@@ -645,7 +749,20 @@ void JsonApiServer::run()
.setPort(mListeningPort);
RsInfo() << __PRETTY_FUNCTION__ << " JSON API server listening on "
<< apiUrl.toString() << std::endl;
- mService->start(settings);
+
+ /* re-allocating mService is important because it deletes the existing
+ * service and therefore leaves the listening port open */
+ auto tExpected = std::shared_ptr(nullptr);
+ if(atomic_compare_exchange_strong(&mService, &tExpected, tService))
+ tService->start(settings);
+ else
+ {
+ RsErr() << __PRETTY_FUNCTION__ << " mService was expected to be "
+ << " null, instead we got: " << tExpected
+ << " something wrong happened JsonApiServer won't start"
+ << std::endl;
+ print_stacktrace();
+ }
}
catch(std::exception& e)
{
@@ -668,3 +785,19 @@ void JsonApiServer::run()
extra = RS_EXTRA_VERSION;
human = RS_HUMAN_READABLE_VERSION;
}
+
+std::error_condition RsJsonApiErrorCategory::default_error_condition(int ev) const noexcept
+{
+ switch(static_cast(ev))
+ {
+ case RsJsonApiErrorNum::TOKEN_FORMAT_INVALID: // fallthrough
+ case RsJsonApiErrorNum::UNKNOWN_API_USER: // fallthrough
+ case RsJsonApiErrorNum::WRONG_API_PASSWORD: // fallthrough
+ case RsJsonApiErrorNum::AUTHORIZATION_REQUEST_DENIED:
+ return std::errc::permission_denied;
+ case RsJsonApiErrorNum::API_USER_CONTAIN_COLON:
+ return std::errc::invalid_argument;
+ default:
+ return std::error_condition(ev, *this);
+ }
+}
diff --git a/libretroshare/src/jsonapi/jsonapi.h b/libretroshare/src/jsonapi/jsonapi.h
index 803be8459..70cc5e309 100644
--- a/libretroshare/src/jsonapi/jsonapi.h
+++ b/libretroshare/src/jsonapi/jsonapi.h
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
#include "util/rsthreads.h"
#include "pqi/p3cfgmgr.h"
@@ -40,6 +41,7 @@
namespace rb = restbed;
+/** Interface to provide addotional resources to JsonApiServer */
class JsonApiResourceProvider
{
public:
@@ -66,7 +68,7 @@ public:
void fullstop() override { RsThread::fullstop(); }
/// @see RsJsonApi
- void restart() override;
+ std::error_condition restart() override;
/// @see RsJsonApi
void askForStop() override { RsThread::askForStop(); }
@@ -90,9 +92,8 @@ public:
void connectToConfigManager(p3ConfigMgr& cfgmgr) override;
/// @see RsJsonApi
- virtual bool authorizeUser(
- const std::string& alphanumeric_user,
- const std::string& alphanumeric_passwd ) override;
+ virtual std::error_condition authorizeUser(
+ const std::string& user, const std::string& passwd ) override;
/// @see RsJsonApi
std::map getAuthorizedTokens() override;
@@ -101,10 +102,13 @@ public:
bool revokeAuthToken(const std::string& user) override;
/// @see RsJsonApi
- bool isAuthTokenValid(const std::string& token) override;
+ bool isAuthTokenValid(
+ const std::string& token,
+ std::error_condition& error = RS_DEFAULT_STORAGE_PARAM(std::error_condition)
+ ) override;
/// @see RsJsonAPI
- bool requestNewTokenAutorization(
+ std::error_condition requestNewTokenAutorization(
const std::string& user, const std::string& password ) override;
/// @see RsJsonApi
@@ -147,6 +151,11 @@ protected:
/// @see RsThread
void onStopRequested() override;
+ static const RsJsonApiErrorCategory sErrorCategory;
+
+ static std::error_condition badApiCredientalsFormat(
+ const std::string& user, const std::string& passwd );
+
private:
/// @see RsThread
void run() override;
@@ -195,13 +204,33 @@ private:
std::reference_wrapper,
std::less > mResourceProviders;
+ /**
+ * This pointer should be accessed via std::atomic_* operations, up until
+ * now only very critical operations like reallocation, are done that way,
+ * but this is not still 100% thread safe, but seems to handle all of the
+ * test cases (no crash, no deadlock), once we switch to C++20 we shoud
+ * change this into std::atomic> which
+ * will automatically handle atomic access properly all the times
+ */
std::shared_ptr mService;
- /** Protect service only during very critical operation like resetting the
- * pointer, still not 100% thread safe, but hopefully we can avoid
- * crashes/freeze with this */
- RsMutex mServiceMutex;
uint16_t mListeningPort;
std::string mBindingAddress;
+
+ /// @see unProtectedRestart()
+ std::atomic mRestartReqTS;
+
+ /// @see unProtectedRestart()
+ constexpr static rstime_t RESTART_BURST_PROTECTION = 7;
+
+ /** It is very important to protect this method from being called in bursts,
+ * because Restbed::Service::stop() together with
+ * Restbed::Service::start(...), which are called internally, silently fails
+ * if combined in bursts, probably because they have to deal with
+ * listening/releasing TCP port.
+ * @see JsonApiServer::restart() and @see JsonApiServer::JsonApiServer()
+ * implementation to understand how correctly use this.
+ */
+ void unProtectedRestart();
};
diff --git a/libretroshare/src/retroshare/rsjsonapi.h b/libretroshare/src/retroshare/rsjsonapi.h
index 251310aaa..98b052d58 100644
--- a/libretroshare/src/retroshare/rsjsonapi.h
+++ b/libretroshare/src/retroshare/rsjsonapi.h
@@ -1,8 +1,9 @@
/*
* RetroShare JSON API public header
*
- * Copyright (C) 2018-2019 Gioacchino Mazzurco
+ * Copyright (C) 2018-2020 Gioacchino Mazzurco
* Copyright (C) 2019 Cyril Soler
+ * Copyright (C) 2020 Asociación Civil Altermundi
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
@@ -25,9 +26,11 @@
#include