/* * RetroShare JSON API * * Copyright (C) 2018-2019 Gioacchino Mazzurco * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the * Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License for more details. * * 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-License-Identifier: AGPL-3.0-only */ #include #include #include #include #include #include #include "jsonapi.h" #include "util/rsjson.h" #include "retroshare/rsfiles.h" #include "util/radix64.h" #include "retroshare/rsinit.h" #include "util/rsnet.h" #include "retroshare/rsiface.h" #include "retroshare/rsinit.h" #include "util/rsurl.h" #include "util/rstime.h" #include "retroshare/rsevents.h" #include "retroshare/rsversion.h" // Generated at compile time #include "jsonapi-includes.inl" /*extern*/ RsJsonApi* rsJsonApi = nullptr; const std::string RsJsonApi::DEFAULT_BINDING_ADDRESS = "127.0.0.1"; /*static*/ const std::multimap JsonApiServer::corsHeaders = { { "Access-Control-Allow-Origin", "*" }, { "Access-Control-Allow-Methods", "GET, POST, OPTIONS"}, { "Access-Control-Allow-Headers", "Authorization,DNT,User-Agent," "X-Requested-With,If-Modified-Since," "Cache-Control,Content-Type,Range" }, { "Access-Control-Expose-Headers", "Content-Length,Content-Range" } }; /*static*/ const std::multimap JsonApiServer::corsOptionsHeaders = { { "Access-Control-Allow-Origin", "*" }, { "Access-Control-Allow-Methods", "GET, POST, OPTIONS"}, { "Access-Control-Allow-Headers", "Authorization,DNT,User-Agent," "X-Requested-With,If-Modified-Since," "Cache-Control,Content-Type,Range" }, { "Access-Control-Max-Age", "1728000" }, // 20 days { "Content-Type", "text/plain; charset=utf-8" }, { "Content-Length", "0" } }; #define INITIALIZE_API_CALL_JSON_CONTEXT \ RsGenericSerializer::SerializeContext cReq( \ nullptr, 0, \ RsGenericSerializer::SERIALIZATION_FLAG_YIELDING ); \ RsJson& jReq(cReq.mJson); \ if(session->get_request()->get_method() == "GET") \ { \ const std::string jrqp(session->get_request()->get_query_parameter("jsonData")); \ jReq.Parse(jrqp.c_str(), jrqp.size()); \ } \ else \ jReq.Parse(reinterpret_cast(body.data()), body.size()); \ \ RsGenericSerializer::SerializeContext cAns; \ RsJson& jAns(cAns.mJson); \ \ /* if caller specified caller_data put it back in the answhere */ \ const char kcd[] = "caller_data"; \ if(jReq.HasMember(kcd)) \ jAns.AddMember(kcd, jReq[kcd], jAns.GetAllocator()) #define DEFAULT_API_CALL_JSON_RETURN(RET_CODE) \ std::stringstream ss; \ ss << jAns; \ std::string&& ans(ss.str()); \ auto headers = corsHeaders; \ headers.insert({ "Content-Type", "text/json" }); \ headers.insert({ "Content-Length", std::to_string(ans.length()) }); \ session->close(RET_CODE, ans, headers) /*static*/ bool JsonApiServer::checkRsServicePtrReady( const void* serviceInstance, const std::string& serviceName, RsGenericSerializer::SerializeContext& ctx, const std::shared_ptr session ) { if(serviceInstance) return true; std::string jsonApiError = __PRETTY_FUNCTION__; jsonApiError += "Service: "; jsonApiError += serviceName; jsonApiError += " not initialized!"; RsGenericSerializer::SerializeJob j(RsGenericSerializer::TO_JSON); RS_SERIAL_PROCESS(jsonApiError); RsJson& jAns(ctx.mJson); DEFAULT_API_CALL_JSON_RETURN(rb::CONFLICT); return false; } bool RsJsonApi::parseToken( const std::string& clear_token, std::string& user,std::string& passwd ) { uint32_t colonIndex = 0; uint32_t colonCounter = 0; 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; if(colonCounter != 1) return false; user = clear_token.substr(0, colonIndex); passwd = clear_token.substr(colonIndex + 1); return true; } JsonApiServer::JsonApiServer(): configMutex("JsonApiServer config"), mService(std::make_shared()), mListeningPort(RsJsonApi::DEFAULT_PORT), mBindingAddress(RsJsonApi::DEFAULT_BINDING_ADDRESS) { registerHandler("/rsLoginHelper/createLocation", [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; RsLoginHelper::Location location; std::string password; std::string errorMessage; bool makeHidden = false; bool makeAutoTor = false; // deserialize input parameters from JSON { RsGenericSerializer::SerializeContext& ctx(cReq); RsGenericSerializer::SerializeJob j(RsGenericSerializer::FROM_JSON); RS_SERIAL_PROCESS(location); RS_SERIAL_PROCESS(password); RS_SERIAL_PROCESS(makeHidden); RS_SERIAL_PROCESS(makeAutoTor); } // call retroshare C++ API bool retval = rsLoginHelper->createLocation( location, password, errorMessage, makeHidden, makeAutoTor ); if(retval) authorizeUser(location.mLocationId.toStdString(),password); // serialize out parameters and return value to JSON { RsGenericSerializer::SerializeContext& ctx(cAns); RsGenericSerializer::SerializeJob j(RsGenericSerializer::TO_JSON); RS_SERIAL_PROCESS(location); RS_SERIAL_PROCESS(errorMessage); RS_SERIAL_PROCESS(retval); } // return them to the API caller DEFAULT_API_CALL_JSON_RETURN(rb::OK); } ); }, false); registerHandler("/rsLoginHelper/attemptLogin", [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; RsPeerId account; std::string password; // deserialize input parameters from JSON { RsGenericSerializer::SerializeContext& ctx(cReq); RsGenericSerializer::SerializeJob j(RsGenericSerializer::FROM_JSON); RS_SERIAL_PROCESS(account); RS_SERIAL_PROCESS(password); } // call retroshare C++ API RsInit::LoadCertificateStatus retval = rsLoginHelper->attemptLogin(account, password); if( retval == RsInit::OK ) authorizeUser(account.toStdString(),password); // serialize out parameters and return value to JSON { RsGenericSerializer::SerializeContext& ctx(cAns); RsGenericSerializer::SerializeJob j(RsGenericSerializer::TO_JSON); RS_SERIAL_PROCESS(retval); } // return them to the API caller DEFAULT_API_CALL_JSON_RETURN(rb::OK); } ); }, false); registerHandler("/rsControl/rsGlobalShutDown", [](const std::shared_ptr session) { auto reqSize = session->get_request()->get_header("Content-Length", 0); session->fetch( static_cast(reqSize), []( const std::shared_ptr session, const rb::Bytes& body ) { INITIALIZE_API_CALL_JSON_CONTEXT; DEFAULT_API_CALL_JSON_RETURN(rb::OK); rsControl->rsGlobalShutDown(); } ); }, true); registerHandler("/rsFiles/getFileData", [](const std::shared_ptr session) { auto reqSize = session->get_request()->get_header("Content-Length", 0); session->fetch( static_cast(reqSize), []( const std::shared_ptr session, const rb::Bytes& body ) { INITIALIZE_API_CALL_JSON_CONTEXT; if(!checkRsServicePtrReady(rsFiles, "rsFiles", cAns, session)) return; RsFileHash hash; uint64_t offset; uint32_t requested_size; bool retval = false; std::string errorMessage; std::string base64data; // deserialize input parameters from JSON { RsGenericSerializer::SerializeContext& ctx(cReq); RsGenericSerializer::SerializeJob j(RsGenericSerializer::FROM_JSON); RS_SERIAL_PROCESS(hash); RS_SERIAL_PROCESS(offset); RS_SERIAL_PROCESS(requested_size); } if(requested_size > 10485760) errorMessage = "requested_size is too big! Better less then 1M"; else { std::vector buffer(requested_size); // call retroshare C++ API retval = rsFiles->getFileData( hash, offset, requested_size, buffer.data()); Radix64::encode(buffer.data(), requested_size, base64data); } // serialize out parameters and return value to JSON { RsGenericSerializer::SerializeContext& ctx(cAns); RsGenericSerializer::SerializeJob j(RsGenericSerializer::TO_JSON); RS_SERIAL_PROCESS(retval); RS_SERIAL_PROCESS(requested_size); RS_SERIAL_PROCESS(base64data); if(!errorMessage.empty()) RS_SERIAL_PROCESS(errorMessage); } DEFAULT_API_CALL_JSON_RETURN(rb::OK); } ); }, true); registerHandler("/rsEvents/registerEventsHandler", [this](const std::shared_ptr session) { const std::multimap headers { { "Connection", "keep-alive" }, { "Content-Type", "text/event-stream" } }; session->yield(rb::OK, headers); size_t reqSize = static_cast( session->get_request()->get_header("Content-Length", 0) ); session->fetch( reqSize, [this]( const std::shared_ptr session, const rb::Bytes& body ) { INITIALIZE_API_CALL_JSON_CONTEXT; if( !checkRsServicePtrReady( rsEvents, "rsEvents", cAns, session ) ) return; const std::weak_ptr weakSession(session); RsEventsHandlerId_t hId = rsEvents->generateUniqueHandlerId(); std::function)> multiCallback = [this, weakSession, hId](std::shared_ptr event) { mService->schedule( [weakSession, hId, event]() { auto session = weakSession.lock(); if(!session || session->is_closed()) { if(rsEvents) rsEvents->unregisterEventsHandler(hId); return; } RsGenericSerializer::SerializeContext ctx; RsTypeSerializer::serial_process( RsGenericSerializer::TO_JSON, ctx, *const_cast(event.get()), "event" ); std::stringstream message; message << "data: " << compactJSON << ctx.mJson << "\n\n"; session->yield(message.str()); } ); }; bool retval = rsEvents->registerEventsHandler(multiCallback, hId); { RsGenericSerializer::SerializeContext& ctx(cAns); RsGenericSerializer::SerializeJob j(RsGenericSerializer::TO_JSON); RS_SERIAL_PROCESS(retval); } // return them to the API caller std::stringstream message; message << "data: " << compactJSON << cAns.mJson << "\n\n"; session->yield(message.str()); } ); }, true); // Generated at compile time #include "jsonapi-wrappers.inl" } void JsonApiServer::registerHandler( const std::string& path, const std::function)>& handler, bool requiresAutentication ) { std::shared_ptr resource(new rb::Resource); resource->set_path(path); resource->set_method_handler("GET", handler); resource->set_method_handler("POST", handler); resource->set_method_handler("OPTIONS", handleCorsOptions); if(requiresAutentication) resource->set_authentication_handler( [this]( const std::shared_ptr session, const std::function)>& callback ) { if(session->get_request()->get_method() == "OPTIONS") { callback(session); return; } if(!rsLoginHelper->isLoggedIn()) { session->close(rb::CONFLICT); return; } std::istringstream authHeader; authHeader.str(session->get_request()->get_header("Authorization")); std::string authToken; std::getline(authHeader, authToken, ' '); if(authToken != "Basic") { session->close(rb::UNAUTHORIZED); return; } std::getline(authHeader, authToken, ' '); authToken = decodeToken(authToken); if(isAuthTokenValid(authToken)) callback(session); else session->close(rb::UNAUTHORIZED); } ); mResources.push_back(resource); } void JsonApiServer::setNewAccessRequestCallback( const std::function& callback ) { mNewAccessRequestCallback = callback; } bool JsonApiServer::requestNewTokenAutorization( const std::string& user, const std::string& passwd ) { if(rsLoginHelper->isLoggedIn() && mNewAccessRequestCallback(user, passwd)) return authorizeUser(user, passwd); return false; } bool JsonApiServer::isAuthTokenValid(const std::string& token) { RS_STACK_MUTEX(configMutex); std::string user,passwd; if(!parseToken(token,user,passwd)) return false; auto it = mAuthTokenStorage.mAuthorizedTokens.find(user); if(it == mAuthTokenStorage.mAuthorizedTokens.end()) return false; // attempt avoiding +else CRYPTO_memcmp+ being optimized away int noOptimiz = 1; /* Do not use mAuthTokenStorage.mAuthorizedTokens.count(token), because * std::string comparison is usually not constant time on content to be * faster, so an attacker may use timings to guess authorized tokens */ if( passwd.size() == it->second.size() && ( noOptimiz = CRYPTO_memcmp( passwd.data(), it->second.data(), it->second.size() ) ) == 0 ) return true; // 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; } std::map JsonApiServer::getAuthorizedTokens() { RS_STACK_MUTEX(configMutex); return mAuthTokenStorage.mAuthorizedTokens; } bool JsonApiServer::revokeAuthToken(const std::string& token) { RS_STACK_MUTEX(configMutex); if(mAuthTokenStorage.mAuthorizedTokens.erase(token)) { IndicateConfigChanged(); return true; } return false; } void JsonApiServer::connectToConfigManager(p3ConfigMgr& cfgmgr) { cfgmgr.addConfiguration("jsonapi.cfg",this); RsFileHash hash; loadConfiguration(hash); } bool 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(!librs::util::is_alphanumeric(passwd)) { RsErr() << __PRETTY_FUNCTION__ << " Password is not alphanumeric" << std::endl; return false; } RS_STACK_MUTEX(configMutex); std::string& p(mAuthTokenStorage.mAuthorizedTokens[user]); if(p != passwd) { p = passwd; IndicateConfigChanged(); } return true; } /*static*/ std::string JsonApiServer::decodeToken(const std::string& radix64_token) { std::vector decodedVect(Radix64::decode(radix64_token)); std::string decodedToken( reinterpret_cast(&decodedVect[0]), decodedVect.size() ); return decodedToken; } RsSerialiser* JsonApiServer::setupSerialiser() { RsSerialiser* rss = new RsSerialiser; rss->addSerialType(new JsonApiConfigSerializer); return rss; } bool JsonApiServer::saveList(bool& cleanup, std::list& saveItems) { cleanup = false; configMutex.lock(); saveItems.push_back(&mAuthTokenStorage); return true; } bool JsonApiServer::loadList(std::list& loadList) { for(RsItem* it : loadList) switch (static_cast(it->PacketSubType())) { case JsonApiItemsType::AuthTokenItem: mAuthTokenStorage = *static_cast(it); delete it; break; default: delete it; break; } return true; } void JsonApiServer::saveDone() { configMutex.unlock(); } void JsonApiServer::handleCorsOptions( const std::shared_ptr session ) { session->close(rb::NO_CONTENT, corsOptionsHeaders); } void JsonApiServer::registerResourceProvider(const JsonApiResourceProvider& rp) { mResourceProviders.insert(rp); } void JsonApiServer::unregisterResourceProvider(const JsonApiResourceProvider& rp) { mResourceProviders.erase(rp); } bool JsonApiServer::hasResourceProvider(const JsonApiResourceProvider& rp) { return mResourceProviders.find(rp) != mResourceProviders.end(); } std::vector > JsonApiServer::getResources() const { auto tab = mResources; for(auto& rp: mResourceProviders) for(auto r: rp.get().getResources()) tab.push_back(r); return tab; } bool JsonApiServer::restart() { fullstop(); RsThread::start("JSON API Server"); return true; } bool JsonApiServer::fullstop() { if(!mService->is_up()) return true; mService->stop(); RsThread::ask_for_stop(); while(isRunning()) { RsDbg() << __PRETTY_FUNCTION__ << " shutting down JSON API service." << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(200)); } return true; } uint16_t JsonApiServer::listeningPort() const { return mListeningPort; } void JsonApiServer::setListeningPort(uint16_t p) { mListeningPort = p; } void JsonApiServer::setBindingAddress(const std::string& bindAddress) { mBindingAddress = bindAddress; } std::string JsonApiServer::getBindingAddress() const { return mBindingAddress; } void JsonApiServer::runloop() { auto settings = std::make_shared(); settings->set_port(mListeningPort); settings->set_bind_address(mBindingAddress); settings->set_default_header("Connection", "close"); if(mService->is_up()) { RsWarn() << __PRETTY_FUNCTION__ << " restbed is already running. " << " stopping it before starting again!" << std::endl; mService->stop(); } /* re-allocating mService is important because it deletes the existing * service and therefore leaves the listening port open */ mService = std::make_shared(); for(auto& r: getResources()) mService->publish(r); try { RsUrl apiUrl; apiUrl.setScheme("http").setHost(mBindingAddress) .setPort(mListeningPort); RsDbg() << __PRETTY_FUNCTION__ << " JSON API server listening on " << apiUrl.toString() << std::endl; mService->start(settings); } catch(std::exception& e) { RsErr() << __PRETTY_FUNCTION__ << " Failure starting JSON API server: " << e.what() << std::endl; print_stacktrace(); return; } RsInfo() << __PRETTY_FUNCTION__ << " finished!" << std::endl; } /*static*/ void RsJsonApi::version( uint32_t& major, uint32_t& minor, uint32_t& mini, std::string& extra, std::string& human ) { major = RS_MAJOR_VERSION; minor = RS_MINOR_VERSION; mini = RS_MINI_VERSION; extra = RS_EXTRA_VERSION; human = RS_HUMAN_READABLE_VERSION; }