#include "rename_object.hpp" #include "functions.hpp" #include #include #include #include #include #include #include #include #include #include #include "jwt.hpp" #include "filesystem/filesystem.hpp" #include "config/config.hpp" #include "fileobject.hpp" // TODO add customKeyOptions and customKeyDefault endpoints std::vector libraries{}; Configuration cfg; void sendResponse(const std::string &response, int status_code, const std::shared_ptr< restbed::Session > &session) { session->close(status_code, response, { { "Content-Length", std::to_string(response.length()) }, { "Access-Control-Allow-Origin", "*" } }); } void performPostFunc(const std::shared_ptr &session, const std::function, rapidjson::GenericDocument>&)>& callback) { const auto request = session->get_request( ); int content_length = request->get_header( "Content-Length", 0 ); session->fetch( content_length, [callback]( const std::shared_ptr< restbed::Session > &session, const restbed::Bytes & body ) { rapidjson::Document doc; doc.Parse(reinterpret_cast(body.data()), body.size()); if(doc.HasParseError()) { sendResponse("ERROR: Invalid body!", 401, session); return; } callback(session, doc); }); } bool verifyLogin( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(doc.FindMember("token") == doc.MemberEnd() || !doc["token"].IsString()) { sendResponse("ERROR: Invalid token!", 401, session); return false; } const auto *token = doc["token"].GetString(); auto res = verifyJWT(token); if(!res) { sendResponse("ERROR: Invalid token!", 401, session); } return res; } std::vector> getLibraries() { std::vector> result{}; for(size_t i = 0; i < libraries.size(); i++) { result.emplace_back(&libraries[i], i); } return result; } std::string getLibrariesJson() { auto libraries = getLibraries(); std::ostringstream result; result << "{\n \"libraries\": [\n"; if(!libraries.empty()) { for(const auto &library : libraries) { result << " {\n \"id\": " << library.second << ",\n"; result << " \"name\": \"" << safeJson(library.first->getName()) << "\"\n,"; result << " \"multiple_files\": " << (library.first->canRenameMultipleFiles() ? "true" : "false") << "\n },\n"; } result.seekp(-2, std::ios_base::end); result << "\n"; } result << " ]\n}"; auto res = result.str(); return res; } void getLibrariesRest( const std::shared_ptr< restbed::Session > &session ) { sendResponse(getLibrariesJson(), restbed::OK, session); } std::vector< RenameObject > getOptions(const RenameObject &search) { auto library_id = search.getLibraryId(); if(library_id >= libraries.size()) { return {}; } auto result = libraries[library_id].getOptions(search); for(auto &res : result) { res.setLibraryId(library_id); } return result; } std::string getOptionsJson(const RenameObject &search) { std::ostringstream res; res << "{\n \"options\": [\n"; auto options = getOptions(search); if(!options.empty()) { for(auto &option : options) { res << option.toJson(); res << ",\n"; } res.seekp( -2, std::ios_base::end ); res << "\n"; } res << " ]\n}"; auto result = res.str(); return result; } void getOptionsRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(doc.HasParseError()) { sendResponse("ERROR: Invalid body!", 401, session); return; } if(!verifyLogin(session, doc)) { return; } if(doc.FindMember("info") == doc.MemberEnd() || !doc["info"].IsObject()) { sendResponse("ERROR: Invalid search!", 401, session); return; } RenameObject search; rapidjson::StringBuffer sb; rapidjson::Writer writer(sb); doc["info"].Accept(writer); std::string s = sb.GetString(); search.fromJson(s); if(search.getPresentedName().empty()) { sendResponse("Empty search", 401, session); return; } sendResponse(getOptionsJson(search), 200, session); } std::vector< std::unordered_map< std::string, std::string > > getCustomKeys(size_t library_id) { if(library_id >= libraries.size()) { return {}; } auto result = libraries[library_id].getCustomKeys(); return result; } std::string getCustomKeysJson(size_t library_id) { std::ostringstream res; res << "{\n \"custom_keys\": [\n"; auto custom_keys = getCustomKeys(library_id); if(!custom_keys.empty()) { for(auto &key : custom_keys) { res << " {\n"; res << " \"name\": \"" << safeJson(key["name"]) << "\",\n"; res << " \"type\": \"" << safeJson(key["type"]) << "\",\n"; res << " \"input\": \"" << safeJson(key["input"]) << "\"\n"; res << " },\n"; } res.seekp( -2, std::ios_base::end ); res << "\n"; } res << " ]\n}"; return res.str(); } void getCustomKeysRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(doc.FindMember("library_id") == doc.MemberEnd() || !doc["library_id"].IsUint64()) { sendResponse("ERROR: Invalid library_id!", 401, session); return; } size_t library_id = doc["library_id"].GetUint64(); sendResponse(getCustomKeysJson(library_id), 200, session); } std::pair renamePath(std::string path, const RenameObject &renamer) { if(renamer.getLibraryId() >= libraries.size()) { return {false, "Invalid library id"}; } if(path[0] != '/') { path = cfg.getSourcePath() + "/" + path; } auto canon_path = FSLib::canonical(path); if(canon_path.substr(0, cfg.getSourcePath().length()) != cfg.getSourcePath()) { return {false, "Invalid path"}; } if(!FSLib::exists(path)) { return {false, "Source doesn't exist"}; } return {libraries[renamer.getLibraryId()].renamePath(path, renamer), "Library error"}; } std::string renamePathJson(const std::string &path, const RenameObject &renamer) { std::ostringstream res; res << "{\n \"success\": "; auto rename_result = renamePath(path, renamer); if(rename_result.first) { res << "true"; } else { res << "false"; } res << ",\n"; if(!rename_result.first) { res << " \"error\": \"" << safeJson(rename_result.second) << "\"\n"; } res << "}"; return res.str(); } void renamePathRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(!verifyLogin(session, doc)) { return; } if(doc.FindMember("path") == doc.MemberEnd() || !doc["path"].IsString()) { // TODO validate path, also validate against config sendResponse("ERROR: Invalid path!", 401, session); return; } if(doc.FindMember("info") == doc.MemberEnd() || !doc["info"].IsObject()) { sendResponse("ERROR: Invalid info!", 401, session); return; } std::string path = doc["path"].GetString(); RenameObject renamer; rapidjson::StringBuffer sb; rapidjson::Writer writer(sb); doc["info"].Accept(writer); std::string s = sb.GetString(); renamer.fromJson(s); sendResponse(renamePathJson(path, renamer), 200, session); } std::string getFilesJson() { std::ostringstream res; res << "{\n \"files\": [\n"; auto files = getFilesInSource(cfg.getSourcePath()); if(!files.empty()) { for(const auto &file : files) { res << " {\n \"path\": \"" << safeJson(file.getPath()) << "\",\n"; res << " \"depth\": " << file.getDepth() << ",\n"; res << " \"type\": \"" << (file.getFileType() == TYPE_FILE ? "file" : "directory") << "\"\n },\n"; } res.seekp(-2, std::ios_base::end); } res << "\n ]\n}"; return res.str(); } void getFilesRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(!verifyLogin(session, doc)) { return; } sendResponse(getFilesJson(), restbed::OK, session); } std::vector> getTargets() { std::vector> result; const auto &targets = cfg.getTargetPaths(); for(uint64_t i = 0; i < targets.size(); i++) { result.emplace_back(i, targets[i].second); } return result; } std::string getTargetsJson() { std::ostringstream res; res << "{\n \"targets\": [\n"; auto targets = getTargets(); if(!targets.empty()) { for(const auto &target : targets) { res << " {\n" << " \"id\": " << target.first << ",\n"; res << " \"name\": \"" << safeJson(target.second) << "\"\n },\n"; } res.seekp(-2, std::ios_base::end); } res << "\n ]\n}"; return res.str(); } void getTargetsRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(!verifyLogin(session, doc)) { return; } sendResponse(getTargetsJson(), restbed::OK, session); } std::string getTargetDirectoriesJson(uint64_t id) { if(id >= cfg.getTargetPaths().size()) { return ""; } std::ostringstream res; res << "{\n \"target_directories\": [\n"; auto dirs = getTargetDirectories(cfg.getTargetPaths()[id].first); if(!dirs.empty()) { for(const auto &dir : dirs) { res << " \"" << safeJson(dir.getPath()) << "\",\n"; } res.seekp(-2, std::ios_base::end); } res << "\n ]\n}"; return res.str(); } void getTargetDirectoriesRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(!verifyLogin(session, doc)) { return; } uint64_t id = 0; if(doc.FindMember("path_id") == doc.MemberEnd() || !doc["path_id"].IsUint64() || (id = doc["path_id"].GetUint64()) >= cfg.getTargetPaths().size()) { sendResponse("ERROR: Invalid path_id!", 401, session); return; } sendResponse(getTargetDirectoriesJson(id), restbed::OK, session); } std::pair move(std::string path, uint64_t target_id, const std::string &containing_dir) { if(path[0] != '/') { path = cfg.getSourcePath() + "/" + path; } auto canon_path = FSLib::canonical(path); if(canon_path.substr(0, cfg.getSourcePath().length()) != cfg.getSourcePath()) { return {false, "Invalid path"}; } if(target_id >= cfg.getTargetPaths().size()) { return {false, "Invalid target_id"}; } if(!FSLib::exists(path)) { return {false, "Source doesn't exist"}; } auto target_start = cfg.getTargetPaths()[target_id].first; auto target_dir = target_start + FSLib::dir_divisor + containing_dir; auto target_canon = FSLib::canonical(target_dir); if(target_canon.substr(0, target_start.length()) != target_start && !target_canon.empty()) { return {false, "Invalid target"}; } // might result in needless false positives, but better be safe than sorry if(target_canon.empty() && target_dir.find("..") != std::string::npos ) { return {false, "Invalid target"}; } if(!FSLib::exists(target_dir)) { FSLib::createDirectoryFull(target_dir); } return {FSLib::rename(path, target_dir + FSLib::dir_divisor + FSLib::getFileName(path)), "Library error"}; } std::string moveJson(const std::string &path, uint64_t target_id, const std::string &containing_dir) { std::ostringstream res; res << "{\n \"success\": "; auto move_result = move(path, target_id, containing_dir); if(move_result.first) { res << "true"; } else { res << "false"; } res << ",\n"; if(!move_result.first) { res << " \"error\": \"" << safeJson(move_result.second) << "\"\n"; } res << "}"; return res.str(); } void moveRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(!verifyLogin(session, doc)) { return; } std::string containing_dir; if(doc.FindMember("path") == doc.MemberEnd() || !doc["path"].IsString()) { // TODO validate path, also validate against config sendResponse("ERROR: Invalid path!", 401, session); return; } uint64_t id = 0; if(doc.FindMember("target_id") == doc.MemberEnd() || !doc["target_id"].IsUint64() || (id = doc["target_id"].GetUint64()) >= cfg.getTargetPaths().size()) { sendResponse("ERROR: Invalid target_id!", 401, session); return; } if(doc.FindMember("containing_dir") != doc.MemberEnd() && doc["containing_dir"].IsString()) { containing_dir = doc["containing_dir"].GetString(); } std::string path = doc["path"].GetString(); // TODO correct response code sendResponse(moveJson(path, id, containing_dir), 200, session); } std::pair remove(std::string path) { if(path[0] != '/') { path = cfg.getSourcePath() + "/" + path; } auto canon_path = FSLib::canonical(path); if(canon_path.substr(0, cfg.getSourcePath().length()) != cfg.getSourcePath()) { return {false, "Invalid path"}; } if(!FSLib::exists(path)) { return {false, "Source doesn't exist"}; } return {FSLib::deleteFile(path), "Library error"}; } std::string removeJson(const std::string &path) { std::ostringstream res; res << "{\n \"success\": "; auto remove_result = remove(path); if(remove_result.first) { res << "true"; } else { res << "false"; } res << ",\n"; if(!remove_result.first) { res << " \"error\": \"" << safeJson(remove_result.second) << "\"\n"; } res << "}"; return res.str(); } void removeRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(!verifyLogin(session, doc)) { return; } if(doc.FindMember("path") == doc.MemberEnd() || !doc["path"].IsString()) { // TODO validate path, also validate against config sendResponse("ERROR: Invalid path!", 401, session); return; } std::string path = doc["path"].GetString(); // TODO correct response code sendResponse(removeJson(path), 200, session); } std::string loginJson(const std::string &user) { std::ostringstream res; res << "{\n \"token\": \"" << createLoginToken(user) << "\"\n}"; return res.str(); } void loginRest( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { if(doc.FindMember("user") == doc.MemberEnd() || !doc["user"].IsString()) { sendResponse("ERROR: Invalid user!", 401, session); return; } if(doc.FindMember("password") == doc.MemberEnd() || !doc["password"].IsString()) { sendResponse("ERROR: Invalid password!", 401, session); return; } const auto *user = doc["user"].GetString(); const auto *password = doc["password"].GetString(); bool valid = false; for(const auto &user_cfg : cfg.getUsers()) { if(user_cfg.first == user) { valid = user_cfg.second == password; break; } } if(valid) { sendResponse(loginJson(user), 200, session); } else { sendResponse("Invalid user/password", 401, session); } } void getOptionsCall(const std::shared_ptr &session) { performPostFunc(session, getOptionsRest); } void getCustomKeysCall(const std::shared_ptr &session) { performPostFunc(session, getCustomKeysRest); } void renameCall(const std::shared_ptr &session) { performPostFunc(session, renamePathRest); } void getFilesCall(const std::shared_ptr &session) { performPostFunc(session, getFilesRest); } void getTargetsCall(const std::shared_ptr &session) { performPostFunc(session, getTargetsRest); } void getTargetDirectoriesCall(const std::shared_ptr &session) { performPostFunc(session, getTargetDirectoriesRest); } void moveCall(const std::shared_ptr &session) { performPostFunc(session, moveRest); } void removeCall(const std::shared_ptr &session) { performPostFunc(session, removeRest); } void loginCall(const std::shared_ptr &session) { performPostFunc(session, loginRest); } int main(int argc, char **argv) { cfg.readConfiguration("/etc/renameserver/main.cfg"); libraries = getLibraries(cfg.getLibraries()); for(auto &library : libraries) { library.init(library.config); } restbed::Service service; auto get_libraries = std::make_shared< restbed::Resource >(); get_libraries->set_path("/get_libraries"); get_libraries->set_method_handler( "GET", getLibrariesRest ); service.publish(get_libraries); auto search = std::make_shared< restbed::Resource >(); search->set_path("/search"); search->set_method_handler( "POST", getOptionsCall ); service.publish(search); auto custom_fields = std::make_shared< restbed::Resource >(); custom_fields->set_path("/get_custom_fields"); custom_fields->set_method_handler( "POST", getCustomKeysCall ); service.publish(custom_fields); auto rename_path = std::make_shared< restbed::Resource >(); rename_path->set_path("/rename"); rename_path->set_method_handler( "POST", renameCall ); service.publish(rename_path); auto get_files = std::make_shared< restbed::Resource >(); get_files->set_path("/get_files"); get_files->set_method_handler( "POST", getFilesCall ); service.publish(get_files); auto get_targets = std::make_shared< restbed::Resource >(); get_targets->set_path("/get_targets"); get_targets->set_method_handler( "POST", getTargetsCall ); service.publish(get_targets); auto get_target_directories = std::make_shared< restbed::Resource >(); get_target_directories->set_path("/get_target_directories"); get_target_directories->set_method_handler( "POST", getTargetDirectoriesCall ); service.publish(get_target_directories); auto move = std::make_shared< restbed::Resource >(); move->set_path("/move"); move->set_method_handler( "POST", moveCall ); service.publish(move); auto remove = std::make_shared< restbed::Resource >(); remove->set_path("/remove"); remove->set_method_handler( "POST", removeCall ); service.publish(remove); auto login = std::make_shared< restbed::Resource >(); login->set_path("/login"); login->set_method_handler( "POST", loginCall ); service.publish(login); auto settings = std::make_shared< restbed::Settings >(); settings->set_port(1984); settings->set_default_header( "Connection", "close" ); service.start(settings); return 0; }