Rewrote the server in cpp with the frontend in svelte
This commit is contained in:
		
							
								
								
									
										78
									
								
								src/data/data.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/data/data.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
#include <fstream>
 | 
			
		||||
#include <inipp.h>
 | 
			
		||||
#include "data_internal.hxx"
 | 
			
		||||
 | 
			
		||||
static constexpr std::string ini_name = "config.ini";
 | 
			
		||||
 | 
			
		||||
std::shared_ptr<spdlog::logger> data_logger;
 | 
			
		||||
 | 
			
		||||
Data::Data() {
 | 
			
		||||
    if (!data_logger)
 | 
			
		||||
        data_logger = spdlog::default_logger()->clone("data");
 | 
			
		||||
 | 
			
		||||
    if (!std::filesystem::exists(files_dir))
 | 
			
		||||
        std::filesystem::create_directory(files_dir);
 | 
			
		||||
 | 
			
		||||
    load_config();
 | 
			
		||||
    load();
 | 
			
		||||
    validate();
 | 
			
		||||
    start_save_thread();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Data::~Data() {
 | 
			
		||||
    shutdown();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Data::shutdown() {
 | 
			
		||||
    data_logger->info("Stopping data saver");
 | 
			
		||||
    shutdown_flag.test_and_set();
 | 
			
		||||
    save();
 | 
			
		||||
    save_thread.join();
 | 
			
		||||
    data_logger->info("Data saver stopped");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Data::load_config() {
 | 
			
		||||
    inipp::Ini<char> ini;
 | 
			
		||||
    if (!std::filesystem::exists(ini_name)) {
 | 
			
		||||
        ini.sections["web"] = {
 | 
			
		||||
            {"port", "80"}
 | 
			
		||||
        };
 | 
			
		||||
        ini.sections["smtp"] = {
 | 
			
		||||
            {"host", "127.0.0.1"},
 | 
			
		||||
            {"port", "25"},
 | 
			
		||||
            {"user", "username"},
 | 
			
		||||
            {"pass", "password"},
 | 
			
		||||
            {"from", "fileserver@example.com"}
 | 
			
		||||
        };
 | 
			
		||||
        ini.sections["admin"] = {
 | 
			
		||||
            {"mail", "admin@example.com"}
 | 
			
		||||
        };
 | 
			
		||||
        std::ofstream f{ini_name};
 | 
			
		||||
        ini.generate(f);
 | 
			
		||||
        data_logger->critical("Missing config, generated example");
 | 
			
		||||
        crash();
 | 
			
		||||
    }
 | 
			
		||||
    data_logger->info("Loading config");
 | 
			
		||||
    std::ifstream f{ini_name};
 | 
			
		||||
    ini.parse(f);
 | 
			
		||||
    if (!ini.errors.empty()) {
 | 
			
		||||
        data_logger->critical("Failed to parse config:");
 | 
			
		||||
        for (const auto &err : ini.errors)
 | 
			
		||||
            data_logger->critical(err);
 | 
			
		||||
        crash();
 | 
			
		||||
    }
 | 
			
		||||
    if (!ini.sections.contains("web")) { data_logger->critical("Missing section web"); crash(); }
 | 
			
		||||
    if (!ini.sections.contains("smtp")) { data_logger->critical("Missing section smtp"); crash(); }
 | 
			
		||||
    if (!ini.sections.contains("admin")) { data_logger->critical("Missing section admin"); crash(); }
 | 
			
		||||
 | 
			
		||||
    std::string entry;
 | 
			
		||||
#define get_entry(section, key) if (!ini.sections[#section].contains(#key)) {data_logger->critical("Missing " #section ":" #key); crash();} entry = ini.sections[#section][#key]
 | 
			
		||||
    get_entry(web, port); config.server_port = std::stoul(entry);
 | 
			
		||||
    get_entry(smtp, host); config.smtp_host = entry;
 | 
			
		||||
    get_entry(smtp, port); config.smtp_port = std::stoul(entry);
 | 
			
		||||
    get_entry(smtp, user); config.smtp_user = entry;
 | 
			
		||||
    get_entry(smtp, pass); config.smtp_pass = entry;
 | 
			
		||||
    get_entry(smtp, from); config.smtp_from = entry;
 | 
			
		||||
    get_entry(admin, mail); config.admin_mail = entry;
 | 
			
		||||
#undef get_entry
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								src/data/data.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/data/data.hxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
#ifndef FILESERVER_DATA_HXX
 | 
			
		||||
#define FILESERVER_DATA_HXX
 | 
			
		||||
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <list>
 | 
			
		||||
#include <unordered_map>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <mutex>
 | 
			
		||||
#include <shared_mutex>
 | 
			
		||||
#include <filesystem>
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
 | 
			
		||||
static const std::filesystem::path files_dir = "files";
 | 
			
		||||
 | 
			
		||||
struct Node {
 | 
			
		||||
    std::uint64_t id;
 | 
			
		||||
    std::string name;
 | 
			
		||||
    bool file, preview;
 | 
			
		||||
    std::shared_ptr<Node> parent;
 | 
			
		||||
    std::uint64_t size;
 | 
			
		||||
    std::list<std::shared_ptr<Node>> children;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct User {
 | 
			
		||||
    std::uint64_t id;
 | 
			
		||||
    std::string name, password, tfa_secret;
 | 
			
		||||
    bool enabled, admin, tfa_enabled, tfa_mail;
 | 
			
		||||
    std::uint64_t next_node_id = 1;
 | 
			
		||||
    std::unordered_map<std::uint64_t, std::shared_ptr<Node>> nodes;
 | 
			
		||||
    std::filesystem::path user_dir;
 | 
			
		||||
 | 
			
		||||
    std::shared_mutex node_lock;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct Token {
 | 
			
		||||
    using clock = std::chrono::steady_clock;
 | 
			
		||||
    static constexpr clock::duration token_lifetime = std::chrono::minutes{60};
 | 
			
		||||
 | 
			
		||||
    explicit Token(const std::shared_ptr<User> &user) : user(user) { refresh(); }
 | 
			
		||||
 | 
			
		||||
    void refresh() { expire = clock::now() + token_lifetime; }
 | 
			
		||||
 | 
			
		||||
    clock::time_point expire;
 | 
			
		||||
    std::shared_ptr<User> user, sudo_original_user{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct Config {
 | 
			
		||||
    std::string smtp_host, smtp_user, smtp_pass, smtp_from, admin_mail; // TODO: Send mail to admin on crash
 | 
			
		||||
    std::uint16_t smtp_port, server_port;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct Data {
 | 
			
		||||
    static constexpr std::uint64_t current_version = 1;
 | 
			
		||||
 | 
			
		||||
    Data();
 | 
			
		||||
    ~Data();
 | 
			
		||||
    void load_config();
 | 
			
		||||
    void load();
 | 
			
		||||
    void validate();
 | 
			
		||||
    void save();
 | 
			
		||||
    void start_save_thread();
 | 
			
		||||
    void shutdown();
 | 
			
		||||
 | 
			
		||||
    Config config;
 | 
			
		||||
 | 
			
		||||
    std::uint64_t version = current_version, next_user_id = 0;
 | 
			
		||||
    std::unordered_map<std::uint64_t, std::shared_ptr<User>> users;
 | 
			
		||||
    std::unordered_map<std::string, std::shared_ptr<Token>> tokens;
 | 
			
		||||
    std::unordered_map<std::string, std::pair<std::uint64_t, Token::clock::time_point>> mail_otp;
 | 
			
		||||
    std::unordered_map<std::string, std::pair<std::uint64_t, Token::clock::time_point>> recovery_keys;
 | 
			
		||||
 | 
			
		||||
    std::thread save_thread;
 | 
			
		||||
    std::atomic_flag save_flag, shutdown_flag;
 | 
			
		||||
    std::shared_mutex user_lock, token_lock;
 | 
			
		||||
    std::mutex mail_otp_lock, recovery_lock;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#endif //FILESERVER_DATA_HXX
 | 
			
		||||
							
								
								
									
										34
									
								
								src/data/data_internal.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/data/data_internal.hxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
#ifndef FILESERVER_DATA_INTERNAL_HXX
 | 
			
		||||
#define FILESERVER_DATA_INTERNAL_HXX
 | 
			
		||||
 | 
			
		||||
#include <filesystem>
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
#include "data.hxx"
 | 
			
		||||
#include "../util/crash.hxx"
 | 
			
		||||
 | 
			
		||||
extern std::shared_ptr<spdlog::logger> data_logger;
 | 
			
		||||
 | 
			
		||||
static constexpr std::string data_new_file = "data.new.json";
 | 
			
		||||
static constexpr std::string data_cur_file = "data.json";
 | 
			
		||||
static constexpr std::string data_old_file = "data.old.json";
 | 
			
		||||
 | 
			
		||||
struct SaveNode {
 | 
			
		||||
    std::unique_ptr<Node> node;
 | 
			
		||||
    std::uint64_t id, parent_id;
 | 
			
		||||
    std::list<std::uint64_t> children_ids;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct FileWrapper {
 | 
			
		||||
    FileWrapper(const char *file, const char *mode) {
 | 
			
		||||
        f = std::fopen(file, mode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~FileWrapper() {
 | 
			
		||||
        std::fclose(f);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::FILE *f;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#endif //FILESERVER_DATA_INTERNAL_HXX
 | 
			
		||||
							
								
								
									
										143
									
								
								src/data/data_load.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/data/data_load.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
			
		||||
#define RAPIDJSON_HAS_STDSTRING 1
 | 
			
		||||
#include <rapidjson/filereadstream.h>
 | 
			
		||||
#include <rapidjson/document.h>
 | 
			
		||||
#include <rapidjson/error/en.h>
 | 
			
		||||
#include "data_internal.hxx"
 | 
			
		||||
 | 
			
		||||
#define FIND_MEMBER(name, ty) m = doc.FindMember(#name); \
 | 
			
		||||
    if (m == doc.MemberEnd() || !m->value.Is##ty()) { data_logger->error("Missing or invalid "#name); throw std::exception{}; }
 | 
			
		||||
#define ASSIGN_MEMBER(lhs, name, ty) FIND_MEMBER(name, ty) lhs = m->value.Get##ty()
 | 
			
		||||
 | 
			
		||||
SaveNode load_node(const rapidjson::Value &doc) {
 | 
			
		||||
    rapidjson::Value::ConstMemberIterator m;
 | 
			
		||||
    auto node = std::make_unique<Node>();
 | 
			
		||||
    std::uint64_t id, parent;
 | 
			
		||||
 | 
			
		||||
    node->parent = nullptr;
 | 
			
		||||
 | 
			
		||||
    ASSIGN_MEMBER(node->id, id, Uint64);
 | 
			
		||||
    id = node->id;
 | 
			
		||||
    data_logger->debug("Loading node {}", id);
 | 
			
		||||
 | 
			
		||||
    ASSIGN_MEMBER(node->name, name, String);
 | 
			
		||||
    ASSIGN_MEMBER(node->file, file, Bool);
 | 
			
		||||
    ASSIGN_MEMBER(node->preview, preview, Bool);
 | 
			
		||||
    ASSIGN_MEMBER(node->size, size, Uint64);
 | 
			
		||||
 | 
			
		||||
    ASSIGN_MEMBER(parent, parent, Uint64);
 | 
			
		||||
 | 
			
		||||
    std::list<std::uint64_t> children;
 | 
			
		||||
    FIND_MEMBER(children, Array)
 | 
			
		||||
    for (const auto &v : m->value.GetArray())
 | 
			
		||||
        if (!v.IsUint64()) { data_logger->error("Invalid child id"); throw std::exception{}; }
 | 
			
		||||
        else children.push_back(v.GetUint64());
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        .node = std::move(node),
 | 
			
		||||
        .id = id,
 | 
			
		||||
        .parent_id = parent,
 | 
			
		||||
        .children_ids = children
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::unique_ptr<User> load_user(const rapidjson::Value &doc) {
 | 
			
		||||
    rapidjson::Value::ConstMemberIterator m;
 | 
			
		||||
    auto user = std::make_unique<User>();
 | 
			
		||||
 | 
			
		||||
    ASSIGN_MEMBER(user->id, id, Uint64);
 | 
			
		||||
    data_logger->debug("Loading user {}", user->id);
 | 
			
		||||
 | 
			
		||||
    ASSIGN_MEMBER(user->name, name, String);
 | 
			
		||||
    ASSIGN_MEMBER(user->password, password, String);
 | 
			
		||||
    ASSIGN_MEMBER(user->tfa_secret, tfa_secret, String);
 | 
			
		||||
    ASSIGN_MEMBER(user->enabled, enabled, Bool);
 | 
			
		||||
    ASSIGN_MEMBER(user->admin, admin, Bool);
 | 
			
		||||
    ASSIGN_MEMBER(user->tfa_enabled, tfa_enabled, Bool);
 | 
			
		||||
    ASSIGN_MEMBER(user->tfa_mail, tfa_mail, Bool);
 | 
			
		||||
    ASSIGN_MEMBER(user->next_node_id, next_node_id, Uint64);
 | 
			
		||||
 | 
			
		||||
    std::list<SaveNode> nodes;
 | 
			
		||||
    FIND_MEMBER(nodes, Array)
 | 
			
		||||
    for (const auto &v : m->value.GetArray())
 | 
			
		||||
        nodes.push_back(load_node(v));
 | 
			
		||||
 | 
			
		||||
    for (auto &node : nodes)
 | 
			
		||||
        user->nodes.emplace(node.id, node.node.release());
 | 
			
		||||
 | 
			
		||||
    for (const auto &snode : nodes) {
 | 
			
		||||
        auto &node = user->nodes.at(snode.id);
 | 
			
		||||
        if (node->id != 0)
 | 
			
		||||
            node->parent = user->nodes.at(snode.parent_id);
 | 
			
		||||
        for (const auto &child : snode.children_ids)
 | 
			
		||||
            node->children.push_back(user->nodes.at(child));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    user->user_dir = files_dir / std::to_string(user->id);
 | 
			
		||||
 | 
			
		||||
    return user;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool load_from_file(Data &data, const std::string &file) {
 | 
			
		||||
    char buf[65536];
 | 
			
		||||
    bool success = false;
 | 
			
		||||
    data_logger->info("Loading data from {}", file);
 | 
			
		||||
    data.users.clear();
 | 
			
		||||
    auto start = std::chrono::high_resolution_clock::now();
 | 
			
		||||
    try {
 | 
			
		||||
        FileWrapper f{file.c_str(), "r"};
 | 
			
		||||
        rapidjson::FileReadStream fstream{f.f, buf, sizeof(buf)};
 | 
			
		||||
        rapidjson::Document doc;
 | 
			
		||||
        rapidjson::Document::MemberIterator m;
 | 
			
		||||
        doc.ParseStream(fstream);
 | 
			
		||||
        if (doc.HasParseError()) {
 | 
			
		||||
            data_logger->error("Failed to parse data");
 | 
			
		||||
            auto e = rapidjson::GetParseError_En(doc.GetParseError());
 | 
			
		||||
            data_logger->error("{}", e);
 | 
			
		||||
            throw std::exception{};
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ASSIGN_MEMBER(data.version, version, Uint64);
 | 
			
		||||
        if (data.version != Data::current_version) {
 | 
			
		||||
            data_logger->error("Version is {}, current version is {}. Refusing to load!", data.version, Data::current_version);
 | 
			
		||||
            throw std::exception{};
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ASSIGN_MEMBER(data.next_user_id, next_user_id, Uint64);
 | 
			
		||||
 | 
			
		||||
        FIND_MEMBER(users, Array)
 | 
			
		||||
        for (const auto &v : m->value.GetArray()) {
 | 
			
		||||
            auto user = load_user(v);
 | 
			
		||||
            data.users.emplace(user->id, std::move(user));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        success = true;
 | 
			
		||||
    } catch (std::exception &_) {}
 | 
			
		||||
    auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start);
 | 
			
		||||
    data_logger->info("Data loading took {} ms", dur.count());
 | 
			
		||||
    return success;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Data::load() {
 | 
			
		||||
    if (std::filesystem::exists(data_cur_file)) {
 | 
			
		||||
        bool ok = load_from_file(*this, data_cur_file);
 | 
			
		||||
        if (!ok) {
 | 
			
		||||
            data_logger->warn("Retrying from old file");
 | 
			
		||||
            if (!std::filesystem::exists(data_old_file)) {
 | 
			
		||||
                data_logger->critical("Old file missing");
 | 
			
		||||
                crash();
 | 
			
		||||
            }
 | 
			
		||||
            ok = load_from_file(*this, data_old_file);
 | 
			
		||||
            if (!ok) {
 | 
			
		||||
                data_logger->critical("Failed to load data");
 | 
			
		||||
                crash();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else if (std::filesystem::exists(data_old_file)) {
 | 
			
		||||
        data_logger->warn("Data file missing, loading from old file");
 | 
			
		||||
        bool ok = load_from_file(*this, data_old_file);
 | 
			
		||||
        if (!ok) {
 | 
			
		||||
            data_logger->critical("Failed to load data");
 | 
			
		||||
            crash();
 | 
			
		||||
        }
 | 
			
		||||
    } else data_logger->info("No data file found for loading");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										109
									
								
								src/data/data_save.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/data/data_save.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
#define RAPIDJSON_HAS_STDSTRING 1
 | 
			
		||||
#include <rapidjson/filewritestream.h>
 | 
			
		||||
#include <rapidjson/writer.h>
 | 
			
		||||
#include "data_internal.hxx"
 | 
			
		||||
 | 
			
		||||
#define KEY(x) writer.Key(#x, sizeof(#x)-1)
 | 
			
		||||
 | 
			
		||||
using Writer = rapidjson::Writer<rapidjson::FileWriteStream>;
 | 
			
		||||
 | 
			
		||||
void save_node(Writer &writer, Node *node) {
 | 
			
		||||
    writer.StartObject();
 | 
			
		||||
 | 
			
		||||
    KEY(id); writer.Uint64(node->id);
 | 
			
		||||
    KEY(name); writer.String(node->name);
 | 
			
		||||
    KEY(file); writer.Bool(node->file);
 | 
			
		||||
    KEY(preview); writer.Bool(node->preview);
 | 
			
		||||
    KEY(size); writer.Uint64(node->size);
 | 
			
		||||
    KEY(parent); writer.Uint64(node->parent == nullptr ? 0 : node->parent->id);
 | 
			
		||||
 | 
			
		||||
    KEY(children);
 | 
			
		||||
    writer.StartArray();
 | 
			
		||||
    for (const auto &child : node->children)
 | 
			
		||||
        writer.Uint64(child->id);
 | 
			
		||||
    writer.EndArray();
 | 
			
		||||
 | 
			
		||||
    writer.EndObject();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void save_user(Writer &writer, User *user) {
 | 
			
		||||
    std::shared_lock lock{user->node_lock};
 | 
			
		||||
    writer.StartObject();
 | 
			
		||||
 | 
			
		||||
    KEY(id); writer.Uint64(user->id);
 | 
			
		||||
    KEY(name); writer.String(user->name);
 | 
			
		||||
    KEY(password); writer.String(user->password);
 | 
			
		||||
    KEY(tfa_secret); writer.String(user->tfa_secret);
 | 
			
		||||
    KEY(enabled); writer.Bool(user->enabled);
 | 
			
		||||
    KEY(admin); writer.Bool(user->admin);
 | 
			
		||||
    KEY(tfa_enabled); writer.Bool(user->tfa_enabled);
 | 
			
		||||
    KEY(tfa_mail); writer.Bool(user->tfa_mail);
 | 
			
		||||
    KEY(next_node_id); writer.Uint64(user->next_node_id);
 | 
			
		||||
 | 
			
		||||
    KEY(nodes);
 | 
			
		||||
    writer.StartArray();
 | 
			
		||||
    for (const auto &node : user->nodes)
 | 
			
		||||
        save_node(writer, node.second.get());
 | 
			
		||||
    writer.EndArray();
 | 
			
		||||
 | 
			
		||||
    writer.EndObject();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
char save_buf[65536];
 | 
			
		||||
void save(Data* data) {
 | 
			
		||||
    data_logger->info("Saving data");
 | 
			
		||||
    try {
 | 
			
		||||
        {
 | 
			
		||||
            FileWrapper f{data_new_file.c_str(), "w"};
 | 
			
		||||
            rapidjson::FileWriteStream stream{f.f, save_buf, sizeof(save_buf)};
 | 
			
		||||
            Writer writer{stream};
 | 
			
		||||
 | 
			
		||||
            std::shared_lock lock{data->user_lock};
 | 
			
		||||
 | 
			
		||||
            writer.StartObject();
 | 
			
		||||
 | 
			
		||||
            KEY(version); writer.Uint64(data->version);
 | 
			
		||||
            KEY(next_user_id); writer.Uint64(data->next_user_id);
 | 
			
		||||
 | 
			
		||||
            KEY(users);
 | 
			
		||||
            writer.StartArray();
 | 
			
		||||
            for (const auto &user : data->users)
 | 
			
		||||
                save_user(writer, user.second.get());
 | 
			
		||||
            writer.EndArray();
 | 
			
		||||
 | 
			
		||||
            writer.EndObject();
 | 
			
		||||
        }
 | 
			
		||||
        data_logger->info("Finished writing data");
 | 
			
		||||
        if (std::filesystem::exists(data_cur_file))
 | 
			
		||||
            std::filesystem::copy_file(data_cur_file, data_old_file, std::filesystem::copy_options::overwrite_existing);
 | 
			
		||||
        std::filesystem::rename(data_new_file, data_cur_file);
 | 
			
		||||
        data_logger->info("Save done");
 | 
			
		||||
    } catch (std::exception &e) {
 | 
			
		||||
        data_logger->error("Error while saving: {}, retrying...", e.what());
 | 
			
		||||
        data->save_flag.test_and_set();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void save_worker(Data *data) {
 | 
			
		||||
    while (!data->shutdown_flag.test()) {
 | 
			
		||||
        data->save_flag.wait(false);
 | 
			
		||||
        do {
 | 
			
		||||
            data->save_flag.clear();
 | 
			
		||||
            std::this_thread::sleep_for(std::chrono::seconds{2});
 | 
			
		||||
        } while (data->save_flag.test());
 | 
			
		||||
        save(data);
 | 
			
		||||
    }
 | 
			
		||||
    data_logger->info("Data saver stopping");
 | 
			
		||||
    save(data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Data::start_save_thread() {
 | 
			
		||||
    save();
 | 
			
		||||
    shutdown_flag.clear();
 | 
			
		||||
    this->save_thread = std::thread{save_worker, this};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Data::save() {
 | 
			
		||||
    save_flag.test_and_set();
 | 
			
		||||
    save_flag.notify_all();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										71
									
								
								src/data/data_validate.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/data/data_validate.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
#include "data_internal.hxx"
 | 
			
		||||
 | 
			
		||||
template<typename... Args>
 | 
			
		||||
inline void ok(bool ok, spdlog::format_string_t<Args...> fmt, Args &&...args) {
 | 
			
		||||
    if (!ok) {
 | 
			
		||||
        data_logger->critical("Data validation failed:");
 | 
			
		||||
        data_logger->critical(fmt, std::forward<Args>(args)...);
 | 
			
		||||
        crash();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void validate_node(const std::string &username, const std::filesystem::path &user_dir, Node *node) {
 | 
			
		||||
    auto name = "User " + username + " node " + std::to_string(node->id) + ":" + node->name;
 | 
			
		||||
    if (node->id != 0) {
 | 
			
		||||
        ok(!node->name.empty(), "{} is missing name", name);
 | 
			
		||||
        ok(node->parent != nullptr, "{} is missing parent", name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (node->file) {
 | 
			
		||||
        auto file = user_dir / std::to_string(node->id);
 | 
			
		||||
        ok(std::filesystem::exists(file), "{} is missing file on disk", name);
 | 
			
		||||
        auto size = std::filesystem::file_size(file);
 | 
			
		||||
        ok(node->size == size, "{} size does not match (Node,Disk): {} != {}", name, node->size, size);
 | 
			
		||||
        if (node->preview)
 | 
			
		||||
            ok(std::filesystem::exists(file.replace_extension("png")), "{} is missing preview on disk", name);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void validate_user(User *user) {
 | 
			
		||||
    auto name = std::to_string(user->id) + ":" + user->name;
 | 
			
		||||
 | 
			
		||||
    ok(!user->name.empty(), "User {} is missing name", user->id);
 | 
			
		||||
    ok(!user->password.empty(), "User {} is missing password", name);
 | 
			
		||||
    ok(user->nodes.contains(0), "User {} is missing root", name);
 | 
			
		||||
    if (user->tfa_enabled && !user->tfa_mail)
 | 
			
		||||
        ok(!user->tfa_secret.empty(), "User {} is missing tfa secret", name);
 | 
			
		||||
 | 
			
		||||
    ok(!user->user_dir.empty(), "User {} user_dir is not set", name);
 | 
			
		||||
    ok(std::filesystem::exists(user->user_dir), "User {} is missing files directory", name);
 | 
			
		||||
 | 
			
		||||
    std::uint64_t last_node_id = 0;
 | 
			
		||||
    for (const auto &node : user->nodes) {
 | 
			
		||||
        validate_node(name, user->user_dir, node.second.get());
 | 
			
		||||
        last_node_id = std::max(last_node_id, node.first);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ok(user->next_node_id > last_node_id, "User {}: Next node id {} must be larger than the last used id {}", name, user->next_node_id, last_node_id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Data::validate() {
 | 
			
		||||
    data_logger->info("Validating data");
 | 
			
		||||
 | 
			
		||||
    auto start = std::chrono::high_resolution_clock::now();
 | 
			
		||||
    try {
 | 
			
		||||
        std::uint64_t last_user_id = 0;
 | 
			
		||||
        for (const auto &user: users) {
 | 
			
		||||
            validate_user(user.second.get());
 | 
			
		||||
            last_user_id = std::max(last_user_id, user.first);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!users.empty())
 | 
			
		||||
            ok(next_user_id > last_user_id, "Next user id {} must be larger than the last used id {}", next_user_id, last_user_id);
 | 
			
		||||
    } catch (std::exception &e) {
 | 
			
		||||
        data_logger->critical("Data validation failed with error: {}", e.what());
 | 
			
		||||
        crash();
 | 
			
		||||
    }
 | 
			
		||||
    auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start);
 | 
			
		||||
    data_logger->info("Data validation took {} ms", dur.count());
 | 
			
		||||
 | 
			
		||||
    data_logger->info("Data validated");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								src/main.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/main.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <csignal>
 | 
			
		||||
#include <corvusoft/restbed/resource.hpp>
 | 
			
		||||
#include <corvusoft/restbed/session.hpp>
 | 
			
		||||
#include <corvusoft/restbed/settings.hpp>
 | 
			
		||||
#include <corvusoft/restbed/service.hpp>
 | 
			
		||||
#include <spdlog/sinks/basic_file_sink.h>
 | 
			
		||||
#include "util/logging.hxx"
 | 
			
		||||
#include "server/server.hxx"
 | 
			
		||||
#include "index_html.h"
 | 
			
		||||
#include "favicon_svg.h"
 | 
			
		||||
 | 
			
		||||
std::shared_ptr<restbed::Service> g_service = nullptr;
 | 
			
		||||
 | 
			
		||||
const static restbed::Bytes index_html_bytes{index_html, index_html + index_html_len};
 | 
			
		||||
const static restbed::Bytes favicon_bytes{favicon_svg, favicon_svg + favicon_svg_len};
 | 
			
		||||
 | 
			
		||||
void signal_shutdown(const int) {
 | 
			
		||||
    spdlog::info("Recieved stop signal");
 | 
			
		||||
    g_service->stop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int main() {
 | 
			
		||||
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log.txt");
 | 
			
		||||
    spdlog::default_logger()->sinks().push_back(file_sink);
 | 
			
		||||
    spdlog::set_level(spdlog::level::trace);
 | 
			
		||||
 | 
			
		||||
    auto mrpc_resource = std::make_shared<restbed::Resource>();
 | 
			
		||||
    mrpc_resource->set_path("/mrpc");
 | 
			
		||||
    Server server{mrpc_resource};
 | 
			
		||||
 | 
			
		||||
#define mk_res(url) auto url##_resource = std::make_shared<restbed::Resource>(); \
 | 
			
		||||
    url##_resource->set_path("/" #url); \
 | 
			
		||||
    url##_resource->set_method_handler("POST", [&server](auto s){ server.url(s); })
 | 
			
		||||
 | 
			
		||||
    mk_res(download);
 | 
			
		||||
    mk_res(download_multi);
 | 
			
		||||
    mk_res(upload);
 | 
			
		||||
#undef mk_res
 | 
			
		||||
 | 
			
		||||
    auto index_resource = std::make_shared<restbed::Resource>();
 | 
			
		||||
    index_resource->set_path("/");
 | 
			
		||||
    index_resource->set_method_handler("GET", [](const std::shared_ptr<restbed::Session>& s){
 | 
			
		||||
        s->yield(
 | 
			
		||||
            200,
 | 
			
		||||
            index_html_bytes,
 | 
			
		||||
            std::multimap<std::string, std::string>{
 | 
			
		||||
                {"Content-Type", "text/html"},
 | 
			
		||||
                {"Content-Length", std::to_string(index_html_len)}
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    auto favicon_resource = std::make_shared<restbed::Resource>();
 | 
			
		||||
    favicon_resource->set_path("/favicon.svg");
 | 
			
		||||
    favicon_resource->set_method_handler("GET", [](const std::shared_ptr<restbed::Session>& s){
 | 
			
		||||
        s->yield(
 | 
			
		||||
            200,
 | 
			
		||||
            favicon_bytes,
 | 
			
		||||
            std::multimap<std::string, std::string>{
 | 
			
		||||
                {"Content-Type", "image/svg+xml"},
 | 
			
		||||
                {"Content-Length", std::to_string(favicon_svg_len)}
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    auto settings = std::make_shared<restbed::Settings>();
 | 
			
		||||
    settings->set_port(server.config.server_port);
 | 
			
		||||
    settings->set_default_header("Connection", "keep-alive");
 | 
			
		||||
 | 
			
		||||
    g_service = std::make_shared<restbed::Service>();
 | 
			
		||||
    g_service->set_logger(std::make_shared<logging::RestbedLogger>());
 | 
			
		||||
    g_service->set_signal_handler(SIGINT, signal_shutdown);
 | 
			
		||||
    g_service->set_signal_handler(SIGTERM, signal_shutdown);
 | 
			
		||||
    g_service->publish(mrpc_resource);
 | 
			
		||||
    g_service->publish(download_resource);
 | 
			
		||||
    g_service->publish(download_multi_resource);
 | 
			
		||||
    g_service->publish(upload_resource);
 | 
			
		||||
    g_service->publish(index_resource);
 | 
			
		||||
    g_service->publish(favicon_resource);
 | 
			
		||||
    g_service->start(settings);
 | 
			
		||||
    g_service.reset();
 | 
			
		||||
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										102
									
								
								src/server/admin.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/server/admin.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
#include "server_internal.hxx"
 | 
			
		||||
 | 
			
		||||
#define check_admin_response() check_user_response(); if (!user->admin) return { .e = "Forbidden" };
 | 
			
		||||
#define check_admin_optional() check_user_optional(); if (!user->admin) return "Forbidden";
 | 
			
		||||
 | 
			
		||||
// TODO Log admin action
 | 
			
		||||
 | 
			
		||||
mrpc::Response<std::vector<mrpc::UserInfo>> Server::Admin_list_users(std::string &&token) {
 | 
			
		||||
    check_admin_response();
 | 
			
		||||
    {
 | 
			
		||||
        std::shared_lock lock{user_lock};
 | 
			
		||||
        std::vector<mrpc::UserInfo> info;
 | 
			
		||||
        info.reserve(users.size());
 | 
			
		||||
        for (const auto &us : users) {
 | 
			
		||||
            const auto u = us.second.get();
 | 
			
		||||
            info.push_back(mrpc::UserInfo {
 | 
			
		||||
                .id = u->id,
 | 
			
		||||
                .name = u->name,
 | 
			
		||||
                .tfa = u->tfa_enabled,
 | 
			
		||||
                .admin = u->admin,
 | 
			
		||||
                .enabled = u->enabled
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return { .o = std::move(info) };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Admin_delete_user(std::string &&token, std::uint64_t &&user_id) {
 | 
			
		||||
    check_admin_optional();
 | 
			
		||||
    auto target = get_user(user_id);
 | 
			
		||||
    if (!target) return "Invalid user";
 | 
			
		||||
    delete_user(target);
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Admin_logout(std::string &&token, std::uint64_t &&user_id) {
 | 
			
		||||
    check_admin_optional();
 | 
			
		||||
    logout_user(user_id);
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Admin_disable_tfa(std::string &&token, std::uint64_t &&user_id) {
 | 
			
		||||
    check_admin_optional();
 | 
			
		||||
    auto u = get_user(user_id);
 | 
			
		||||
    if (u) u->tfa_enabled = false;
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Admin_set_admin(std::string &&token, std::uint64_t &&user_id, bool &&admin) {
 | 
			
		||||
    check_admin_optional();
 | 
			
		||||
    auto u = get_user(user_id);
 | 
			
		||||
    if (u) u->admin = admin;
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Admin_set_enabled(std::string &&token, std::uint64_t &&user_id, bool &&enabled) {
 | 
			
		||||
    check_admin_optional();
 | 
			
		||||
    auto u = get_user(user_id);
 | 
			
		||||
    if (u) u->enabled = enabled;
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Admin_sudo(std::string &&token, std::uint64_t &&user_id) {
 | 
			
		||||
    check_admin_optional();
 | 
			
		||||
    auto u = get_user(user_id);
 | 
			
		||||
    if (!u)
 | 
			
		||||
        return "Invalid user";
 | 
			
		||||
    {
 | 
			
		||||
        std::unique_lock tlock{token_lock};
 | 
			
		||||
        auto &t = tokens.at(token);
 | 
			
		||||
        t->sudo_original_user = user;
 | 
			
		||||
        t->user = u;
 | 
			
		||||
        t->refresh();
 | 
			
		||||
    }
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Admin_unsudo(std::string &&token) {
 | 
			
		||||
    check_user_optional();
 | 
			
		||||
    {
 | 
			
		||||
        std::unique_lock lock{token_lock};
 | 
			
		||||
        auto &t = tokens.at(token);
 | 
			
		||||
        if (t->sudo_original_user == nullptr)
 | 
			
		||||
            return "Unauthorized";
 | 
			
		||||
        t->user = t->sudo_original_user;
 | 
			
		||||
        t->sudo_original_user = nullptr;
 | 
			
		||||
        t->refresh();
 | 
			
		||||
    }
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Admin_shutdown(std::string &&token) {
 | 
			
		||||
    check_admin_optional();
 | 
			
		||||
    spdlog::info("Received rpc shutdown request from admin user {}", user->name);
 | 
			
		||||
    g_service->stop();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										226
									
								
								src/server/auth.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								src/server/auth.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,226 @@
 | 
			
		||||
#include <botan_all.h>
 | 
			
		||||
#include "server_internal.hxx"
 | 
			
		||||
 | 
			
		||||
std::string hash_password(const std::string &password) {
 | 
			
		||||
    return Botan::argon2_generate_pwhash(password.c_str(), password.size(), *auth_rng, 1, 64*1024, 4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Auth_signup(std::string &&username, std::string &&password) {
 | 
			
		||||
    std::unique_lock lock{user_lock};
 | 
			
		||||
    for (const auto &user : users) {
 | 
			
		||||
        if (user.second->name == username)
 | 
			
		||||
            return "User already exists";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (password.length() < 6)
 | 
			
		||||
        return "Password must be 6 characters or longer";
 | 
			
		||||
 | 
			
		||||
    auto hash = hash_password(password);
 | 
			
		||||
    auto id = next_user_id++;
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<Node> root{new Node{
 | 
			
		||||
        .id = 0,
 | 
			
		||||
        .name = "",
 | 
			
		||||
        .file = false,
 | 
			
		||||
        .preview = false,
 | 
			
		||||
        .parent = nullptr,
 | 
			
		||||
        .size = 0
 | 
			
		||||
    }};
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<User> user{new User{
 | 
			
		||||
        .id = id,
 | 
			
		||||
        .name = username,
 | 
			
		||||
        .password = hash,
 | 
			
		||||
        .enabled = false,
 | 
			
		||||
        .admin = false,
 | 
			
		||||
        .tfa_enabled = false
 | 
			
		||||
    }};
 | 
			
		||||
    user->nodes.emplace(0, std::move(root));
 | 
			
		||||
    user->user_dir = files_dir / std::to_string(user->id);
 | 
			
		||||
 | 
			
		||||
    std::filesystem::create_directory(user->user_dir);
 | 
			
		||||
 | 
			
		||||
    users.emplace(id, std::move(user));
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<mrpc::LoginResponse> Server::Auth_login(std::string &&username, std::string &&password, std::optional<std::string> &&otp) {
 | 
			
		||||
    std::shared_lock lock{user_lock};
 | 
			
		||||
    std::shared_ptr<User> user = nullptr;
 | 
			
		||||
    for (const auto &u : users) {
 | 
			
		||||
        if (u.second->name == username) {
 | 
			
		||||
            user = u.second;
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (user == nullptr)
 | 
			
		||||
        return { .e = "Invalid username or password" };
 | 
			
		||||
    if (!Botan::argon2_check_pwhash(password.c_str(), password.size(), user->password))
 | 
			
		||||
        return { .e = "Invalid username or password" };
 | 
			
		||||
    if (!user->enabled)
 | 
			
		||||
        return { .e = "User is disabled" };
 | 
			
		||||
    if (user->tfa_enabled) {
 | 
			
		||||
        if (otp.has_value()) {
 | 
			
		||||
            bool ok = user->tfa_mail ? check_mail_code(user, otp.value()) : check_tfa_code(user, otp.value());
 | 
			
		||||
            if (!ok)
 | 
			
		||||
                return { .e = "Invalid code" };
 | 
			
		||||
        } else {
 | 
			
		||||
            if (user->tfa_mail)
 | 
			
		||||
                send_tfa_mail(user);
 | 
			
		||||
            mrpc::LoginResponse response { .otp_needed = true };
 | 
			
		||||
            return { .o = std::move(response) };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto token = Botan::hex_encode(auth_rng->random_vec(16), false);
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        std::unique_lock tlock{token_lock};
 | 
			
		||||
        tokens.emplace(token, std::make_unique<Token>(user));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mrpc::LoginResponse response {
 | 
			
		||||
        .otp_needed = false,
 | 
			
		||||
        .token = token
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return { .o = std::move(response) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
void Server::Auth_logout(std::string &&token) {
 | 
			
		||||
    std::unique_lock lock{token_lock};
 | 
			
		||||
    tokens.erase(token);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Auth_logout_all(std::string &&token) {
 | 
			
		||||
    check_user_optional();
 | 
			
		||||
    logout_user(user->id);
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Server::Auth_send_recovery_key(std::string &&username) {
 | 
			
		||||
    std::shared_lock lock{user_lock};
 | 
			
		||||
    User *user = nullptr;
 | 
			
		||||
    for (const auto &u : users) {
 | 
			
		||||
        if (u.second->name == username) {
 | 
			
		||||
            user = u.second.get();
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (user == nullptr)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    auto code = Botan::hex_encode(auth_rng->random_vec(5), true);
 | 
			
		||||
    {
 | 
			
		||||
        std::lock_guard rlock{recovery_lock};
 | 
			
		||||
        recovery_keys.emplace(code, std::make_pair(user->id, Token::clock::now() + std::chrono::minutes{5}));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    send_mail(user->name, "MFileserver - Password recovery", "Your recovery key is: " + code + "\r\nIt is valid for 5 minutes");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Auth_reset_password(std::string &&key, std::string &&password) {
 | 
			
		||||
    std::lock_guard lock{recovery_lock};
 | 
			
		||||
    auto now = Token::clock::now();
 | 
			
		||||
    for (auto it = recovery_keys.begin(); it != recovery_keys.end();) {
 | 
			
		||||
        if (now >= it->second.second)
 | 
			
		||||
            recovery_keys.erase(it++);
 | 
			
		||||
        else
 | 
			
		||||
            ++it;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto entry = recovery_keys.find(key);
 | 
			
		||||
    if (entry == recovery_keys.end())
 | 
			
		||||
        return "Invalid key";
 | 
			
		||||
    auto user = get_user(entry->second.first);
 | 
			
		||||
    if (!user)
 | 
			
		||||
        return "Invalid key";
 | 
			
		||||
    if (password.length() < 6)
 | 
			
		||||
        return "Password must be 6 characters or longer";
 | 
			
		||||
    recovery_keys.erase(key);
 | 
			
		||||
 | 
			
		||||
    logout_user(user->id);
 | 
			
		||||
    {
 | 
			
		||||
        std::shared_lock ulock{user_lock};
 | 
			
		||||
        user->password = hash_password(password);
 | 
			
		||||
    }
 | 
			
		||||
    save();
 | 
			
		||||
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Auth_change_password(std::string &&token, std::string &&old_pw, std::string &&new_pw) {
 | 
			
		||||
    check_user_optional();
 | 
			
		||||
    std::shared_lock lock{user_lock};
 | 
			
		||||
    if (!Botan::argon2_check_pwhash(old_pw.c_str(), old_pw.size(), user->password))
 | 
			
		||||
        return "Old password is wrong";
 | 
			
		||||
    if (new_pw.length() < 6)
 | 
			
		||||
        return "Password must be 6 characters or longer";
 | 
			
		||||
    user->password = hash_password(new_pw);
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Auth_tfa_setup_mail(std::string &&token) {
 | 
			
		||||
    check_user_optional();
 | 
			
		||||
    if (user->tfa_enabled)
 | 
			
		||||
        return "Tfa is already enabled";
 | 
			
		||||
    user->tfa_mail = true;
 | 
			
		||||
    send_tfa_mail(user);
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<std::string> Server::Auth_tfa_setup_totp(std::string &&token) {
 | 
			
		||||
    check_user_response();
 | 
			
		||||
    if (user->tfa_enabled)
 | 
			
		||||
        return { .e = "Tfa is already enabled" };
 | 
			
		||||
    user->tfa_mail = false;
 | 
			
		||||
    Botan::OctetString secret{*auth_rng, 16};
 | 
			
		||||
    user->tfa_secret = secret.to_string();
 | 
			
		||||
 | 
			
		||||
    return { .o = Botan::base32_encode(secret) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Auth_tfa_complete(std::string &&token, std::string &&otp) {
 | 
			
		||||
    check_user_optional();
 | 
			
		||||
    bool ok = user->tfa_mail ? check_mail_code(user, otp) : check_tfa_code(user, otp);
 | 
			
		||||
    if (!ok)
 | 
			
		||||
        return "Invalid code";
 | 
			
		||||
    user->tfa_enabled = true;
 | 
			
		||||
    logout_user(user->id);
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Auth_tfa_disable(std::string &&token) {
 | 
			
		||||
    check_user_optional();
 | 
			
		||||
    user->tfa_enabled = false;
 | 
			
		||||
    logout_user(user->id);
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::Auth_delete_user(std::string &&token) {
 | 
			
		||||
    check_user_optional();
 | 
			
		||||
    delete_user(user);
 | 
			
		||||
    save();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<mrpc::Session> Server::Auth_session_info(std::string &&token) {
 | 
			
		||||
    check_user_response();
 | 
			
		||||
    auto t = get_token(token);
 | 
			
		||||
    if (!t)
 | 
			
		||||
        return { .e = "Invalid token" };
 | 
			
		||||
 | 
			
		||||
    mrpc::Session info {
 | 
			
		||||
        .name = user->name,
 | 
			
		||||
        .tfa_enabled = user->tfa_enabled,
 | 
			
		||||
        .admin = user->admin,
 | 
			
		||||
        .sudo = t->sudo_original_user != nullptr
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return { .o = info };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										183
									
								
								src/server/download.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								src/server/download.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,183 @@
 | 
			
		||||
#include <filesystem>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <ranges>
 | 
			
		||||
#include <fstream>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <charconv>
 | 
			
		||||
#include <miniz.h>
 | 
			
		||||
#include <corvusoft/restbed/session.hpp>
 | 
			
		||||
#include <corvusoft/restbed/request.hpp>
 | 
			
		||||
#include <corvusoft/restbed/response.hpp>
 | 
			
		||||
#include "server_internal.hxx"
 | 
			
		||||
 | 
			
		||||
void Server::download(const std::shared_ptr<restbed::Session> &s) {
 | 
			
		||||
    const auto req = s->get_request();
 | 
			
		||||
    std::size_t body_len = req->get_header("Content-Length", 0);
 | 
			
		||||
    s->fetch(body_len, [this](const std::shared_ptr<restbed::Session> &s, const restbed::Bytes &b){
 | 
			
		||||
        std::string body{b.cbegin(), b.cend()};
 | 
			
		||||
        if (body.empty())
 | 
			
		||||
            return s->close(400, "empty body");
 | 
			
		||||
        std::string node_str, token;
 | 
			
		||||
        for (const auto part : std::views::split(body, '&')) {
 | 
			
		||||
            std::string_view part_view{part};
 | 
			
		||||
            auto equal_pos = part_view.find_first_of('=');
 | 
			
		||||
            auto key = part_view.substr(0, equal_pos);
 | 
			
		||||
            if (key == "node")
 | 
			
		||||
                node_str = part_view.substr(equal_pos+1);
 | 
			
		||||
            else if (key == "token")
 | 
			
		||||
                token = part_view.substr(equal_pos+1);
 | 
			
		||||
        }
 | 
			
		||||
        if (node_str.empty())
 | 
			
		||||
            return s->close(400, "Missing node");
 | 
			
		||||
        if (token.empty())
 | 
			
		||||
            return s->close(400, "Missing token");
 | 
			
		||||
 | 
			
		||||
        std::uint64_t node_id;
 | 
			
		||||
        auto res = std::from_chars(node_str.data(), node_str.data() + node_str.size(), node_id);
 | 
			
		||||
        if (res.ec != std::errc{})
 | 
			
		||||
            return s->close(400, "Invalid node");
 | 
			
		||||
 | 
			
		||||
        check_user() return s->close(400, "Invalid user");
 | 
			
		||||
        {
 | 
			
		||||
            std::shared_lock lock{user->node_lock};
 | 
			
		||||
            auto node = get_node(user, node_id);
 | 
			
		||||
            if (!node) return s->close(400, "Invalid node");
 | 
			
		||||
 | 
			
		||||
            auto mime = get_mime_type(node->name);
 | 
			
		||||
 | 
			
		||||
            s->yield(
 | 
			
		||||
                200,
 | 
			
		||||
                "",
 | 
			
		||||
                std::multimap<std::string, std::string>{
 | 
			
		||||
                    {"Content-Type", mime},
 | 
			
		||||
                    {"Content-Length", std::to_string(node->size)},
 | 
			
		||||
                    {"Content-Disposition", "attachment; filename=\"" + node->name + "\""}
 | 
			
		||||
                },
 | 
			
		||||
                [&](const std::shared_ptr<restbed::Session>& s) {
 | 
			
		||||
                    std::shared_lock lock{user->node_lock};
 | 
			
		||||
                    restbed::Bytes buf(1024*1024*4, 0);
 | 
			
		||||
                    std::ifstream f{user->user_dir / std::to_string(node->id)};
 | 
			
		||||
                    while (!f.eof()) {
 | 
			
		||||
                        f.read((char*)buf.data(), buf.size());
 | 
			
		||||
                        buf.resize(f.gcount());
 | 
			
		||||
                        s->yield(buf);
 | 
			
		||||
                    }
 | 
			
		||||
                    s->close();
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t zip_write_func(void *pOpaque, mz_uint64 _file_ofs, const void *pBuf, size_t n) {
 | 
			
		||||
    auto s = (restbed::Session*)pOpaque;
 | 
			
		||||
    if (n > 0) {
 | 
			
		||||
        restbed::Bytes buf(n, 0);
 | 
			
		||||
        std::memcpy(buf.data(), pBuf, n);
 | 
			
		||||
        std::stringstream ss;
 | 
			
		||||
        ss << std::hex << n;
 | 
			
		||||
        s->yield(ss.str() + "\r\n");
 | 
			
		||||
        s->yield(buf);
 | 
			
		||||
        s->yield("\r\n");
 | 
			
		||||
    }
 | 
			
		||||
    return n;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Server::download_multi(const std::shared_ptr<restbed::Session> &s) {
 | 
			
		||||
    const auto req = s->get_request();
 | 
			
		||||
    const auto body_len = req->get_header("Content-Length", 0);
 | 
			
		||||
    s->fetch(body_len, [this](const std::shared_ptr<restbed::Session> &s, const restbed::Bytes &b){
 | 
			
		||||
        std::string body{b.cbegin(), b.cend()};
 | 
			
		||||
        if (body.empty())
 | 
			
		||||
            return s->close(400, "empty body");
 | 
			
		||||
        std::string nodes_str, token;
 | 
			
		||||
        for (const auto part : std::views::split(body, '&')) {
 | 
			
		||||
            std::string_view part_view{part};
 | 
			
		||||
            auto equal_pos = part_view.find_first_of('=');
 | 
			
		||||
            auto key = part_view.substr(0, equal_pos);
 | 
			
		||||
            if (key == "nodes")
 | 
			
		||||
                nodes_str = part_view.substr(equal_pos+1);
 | 
			
		||||
            else if (key == "token")
 | 
			
		||||
                token = part_view.substr(equal_pos+1);
 | 
			
		||||
        }
 | 
			
		||||
        if (nodes_str.empty())
 | 
			
		||||
            return s->close(400, "Missing nodes");
 | 
			
		||||
        if (token.empty())
 | 
			
		||||
            return s->close(400, "Missing token");
 | 
			
		||||
 | 
			
		||||
        std::vector<std::uint64_t> node_ids;
 | 
			
		||||
        for (const auto part : std::views::split(nodes_str, '.')) {
 | 
			
		||||
            std::uint64_t node_id;
 | 
			
		||||
            auto res = std::from_chars(part.data(), part.data() + part.size(), node_id);
 | 
			
		||||
            if (res.ec != std::errc{})
 | 
			
		||||
                return s->close(400, "Invalid node " + std::string{std::string_view{part}});
 | 
			
		||||
            node_ids.push_back(node_id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        check_user() return s->close(400, "Invalid user");
 | 
			
		||||
        {
 | 
			
		||||
            std::shared_lock lock{user->node_lock};
 | 
			
		||||
            std::vector<std::shared_ptr<Node>> nodes;
 | 
			
		||||
            for (auto node_id : node_ids) {
 | 
			
		||||
                auto node = get_node(user, node_id);
 | 
			
		||||
                if (!node) return s->close(400, "Invalid node " + std::to_string(node_id));
 | 
			
		||||
                nodes.push_back(node);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            s->yield(
 | 
			
		||||
                200,
 | 
			
		||||
                "",
 | 
			
		||||
                std::multimap<std::string, std::string>{
 | 
			
		||||
                    {"Content-Type", "application/zip"},
 | 
			
		||||
                    {"Content-Disposition", "attachment; filename=\"files.zip\""},
 | 
			
		||||
                    {"Transfer-Encoding", "chunked"}
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            std::thread zip_thread{[nodes = nodes, user = user, s = s] {
 | 
			
		||||
                std::shared_lock lock{user->node_lock};
 | 
			
		||||
 | 
			
		||||
                mz_zip_archive archive;
 | 
			
		||||
                mz_zip_zero_struct(&archive);
 | 
			
		||||
                archive.m_pWrite = zip_write_func;
 | 
			
		||||
                archive.m_pIO_opaque = s.get();
 | 
			
		||||
 | 
			
		||||
                mz_zip_writer_init_v2(&archive, 0, MZ_ZIP_FLAG_WRITE_ZIP64);
 | 
			
		||||
 | 
			
		||||
                std::deque<std::pair<std::shared_ptr<Node>, std::filesystem::path>> todo;
 | 
			
		||||
                for (const auto &node : nodes)
 | 
			
		||||
                    todo.emplace_back(node, std::filesystem::path{});
 | 
			
		||||
 | 
			
		||||
                auto handle_file = [&user, &archive](const std::pair<std::shared_ptr<Node>, std::filesystem::path> &i) {
 | 
			
		||||
                    auto path = i.second / i.first->name;
 | 
			
		||||
                    auto real_path = user->user_dir / std::to_string(i.first->id);
 | 
			
		||||
                    mz_zip_writer_add_file(&archive, path.c_str(), real_path.c_str(), nullptr, 0, MZ_DEFAULT_COMPRESSION);
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                while (!todo.empty()) {
 | 
			
		||||
                    const auto &node = todo.front();
 | 
			
		||||
                    if (node.first->file) {
 | 
			
		||||
                        handle_file(node);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        auto path = node.second / node.first->name;
 | 
			
		||||
                        auto dir_path = path.string() + "/";
 | 
			
		||||
                        mz_zip_writer_add_mem(&archive, dir_path.c_str(), nullptr, 0, 0);
 | 
			
		||||
                        for (const auto &child : node.first->children) {
 | 
			
		||||
                            auto p = std::make_pair(child, path);
 | 
			
		||||
                            if (child->file)
 | 
			
		||||
                                handle_file(p);
 | 
			
		||||
                            else
 | 
			
		||||
                                todo.push_back(p);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    todo.pop_front();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                mz_zip_writer_finalize_archive(&archive);
 | 
			
		||||
                mz_zip_writer_end(&archive);
 | 
			
		||||
 | 
			
		||||
                s->close("0\r\n\r\n");
 | 
			
		||||
            }};
 | 
			
		||||
            zip_thread.detach();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										281
									
								
								src/server/fs.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								src/server/fs.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,281 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <fstream>
 | 
			
		||||
#include <stack>
 | 
			
		||||
#include <unordered_set>
 | 
			
		||||
#include "server_internal.hxx"
 | 
			
		||||
 | 
			
		||||
mrpc::Node node_to_node(const std::shared_ptr<Node>& node) {
 | 
			
		||||
    return {
 | 
			
		||||
        .id = node->id,
 | 
			
		||||
        .name = node->name,
 | 
			
		||||
        .file = node->file,
 | 
			
		||||
        .preview = node->preview,
 | 
			
		||||
        .parent = node->id == 0 ? std::nullopt : std::optional{node->parent->id},
 | 
			
		||||
        .size = node->file ? std::optional{node->size} : std::nullopt,
 | 
			
		||||
        .children = std::nullopt
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::shared_ptr<Node> get_node(const std::shared_ptr<User>& user, std::uint64_t id) {
 | 
			
		||||
    std::shared_lock lock{user->node_lock};
 | 
			
		||||
    const auto &entry = user->nodes.find(id);
 | 
			
		||||
    return entry == user->nodes.end() ? nullptr : entry->second;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string get_path(std::shared_ptr<Node> node) {
 | 
			
		||||
    std::string ret;
 | 
			
		||||
    while(node) {
 | 
			
		||||
        if (node->id != 0) {
 | 
			
		||||
            ret.insert(0, node->name);
 | 
			
		||||
            ret.insert(0, "/");
 | 
			
		||||
        }
 | 
			
		||||
        node = node->parent;
 | 
			
		||||
    }
 | 
			
		||||
    return ret.empty() ? "/" : ret;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Server::delete_node(const std::shared_ptr<User> &user, std::uint64_t id, const std::function<void(std::string)>& log) {
 | 
			
		||||
    std::unique_lock lock{user->node_lock};
 | 
			
		||||
    std::stack<std::shared_ptr<Node>> todo;
 | 
			
		||||
    {
 | 
			
		||||
        auto start = user->nodes.find(id);
 | 
			
		||||
        if (start == user->nodes.end()) return;
 | 
			
		||||
        todo.push(start->second);
 | 
			
		||||
    }
 | 
			
		||||
    while (!todo.empty()) {
 | 
			
		||||
        auto node = todo.top();
 | 
			
		||||
        auto log_path = get_path(node);
 | 
			
		||||
 | 
			
		||||
        if (!node->children.empty()) {
 | 
			
		||||
            log("Entering " + log_path + "\n");
 | 
			
		||||
            for (const auto &child : node->children)
 | 
			
		||||
                todo.push(child);
 | 
			
		||||
            node->children.clear();
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log("Deleting " + log_path + "...");
 | 
			
		||||
        if (node->file) {
 | 
			
		||||
            auto path = user->user_dir / std::to_string(node->id);
 | 
			
		||||
            std::filesystem::remove(path);
 | 
			
		||||
            if (node->preview)
 | 
			
		||||
                std::filesystem::remove(path.replace_extension("png"));
 | 
			
		||||
        }
 | 
			
		||||
        if (node->parent)
 | 
			
		||||
            node->parent->children.remove(node);
 | 
			
		||||
        node->parent.reset();
 | 
			
		||||
        user->nodes.erase(node->id);
 | 
			
		||||
        log(" Done\n");
 | 
			
		||||
 | 
			
		||||
        todo.pop();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::uint64_t Server::nodes_size(const std::shared_ptr<User> &user, const std::vector<std::uint64_t> &ids) {
 | 
			
		||||
    std::uint64_t total = 0;
 | 
			
		||||
    std::deque<Node*> todo;
 | 
			
		||||
    for (const auto &id : ids) {
 | 
			
		||||
        auto node = get_node(user, id);
 | 
			
		||||
        if (node)
 | 
			
		||||
            todo.push_back(node.get());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (!todo.empty()) {
 | 
			
		||||
        auto node = todo.front();
 | 
			
		||||
        if (node->file) {
 | 
			
		||||
            total += node->size;
 | 
			
		||||
        } else {
 | 
			
		||||
            for (const auto &child : node->children)
 | 
			
		||||
                todo.push_back(child.get());
 | 
			
		||||
        }
 | 
			
		||||
        todo.pop_front();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return total;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<mrpc::Node> Server::FS_get_node(std::string &&token, std::uint64_t &&node_id) {
 | 
			
		||||
    check_user_response();
 | 
			
		||||
    auto node = get_node(user, node_id);
 | 
			
		||||
    if (!node) return { .e = "Invalid node" };
 | 
			
		||||
 | 
			
		||||
    mrpc::Node n = node_to_node(node);
 | 
			
		||||
    if (!node->file) {
 | 
			
		||||
        std::vector<mrpc::Node> children;
 | 
			
		||||
        children.reserve(node->children.size());
 | 
			
		||||
        for (const auto &child : node->children)
 | 
			
		||||
            children.push_back(node_to_node(child));
 | 
			
		||||
        n.children = std::move(children);
 | 
			
		||||
    }
 | 
			
		||||
    return { .o = std::move(n) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<std::vector<mrpc::PathSegment>> Server::FS_get_path(std::string &&token, std::uint64_t &&node_id) {
 | 
			
		||||
    check_user_response();
 | 
			
		||||
    std::shared_lock lock{user->node_lock};
 | 
			
		||||
    auto node = get_node(user, node_id);
 | 
			
		||||
    if (!node) return { .e = "Invalid node" };
 | 
			
		||||
 | 
			
		||||
    std::vector<mrpc::PathSegment> segments;
 | 
			
		||||
    while (node) {
 | 
			
		||||
        if (node->file) {
 | 
			
		||||
            segments.push_back(std::move(mrpc::PathSegment { .name = node->name }));
 | 
			
		||||
        } else {
 | 
			
		||||
            segments.push_back(std::move(mrpc::PathSegment { .name = node->id == 0 ? "/" : node->name, .id = node->id }));
 | 
			
		||||
        }
 | 
			
		||||
        node = node->parent;
 | 
			
		||||
    }
 | 
			
		||||
    std::reverse(segments.begin(), segments.end());
 | 
			
		||||
 | 
			
		||||
    return { .o = std::move(segments) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<std::uint64_t> Server::FS_get_nodes_size(std::string &&token, std::vector<std::uint64_t> &&nodes) {
 | 
			
		||||
    check_user_response();
 | 
			
		||||
    std::shared_lock lock{user->node_lock};
 | 
			
		||||
    return { .o = nodes_size(user, nodes) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<mrpc::CreateNodeInfo> Server::FS_create_node(std::string &&token, bool &&file, std::uint64_t &&parent_id, std::string &&name) {
 | 
			
		||||
    check_user_response();
 | 
			
		||||
    auto parent = get_node(user, parent_id);
 | 
			
		||||
    if (!parent) return { .e = "Invalid parent" };
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        std::unique_lock lock{user->node_lock};
 | 
			
		||||
 | 
			
		||||
        std::shared_ptr<Node> child = nullptr;
 | 
			
		||||
        for (const auto &c: parent->children) {
 | 
			
		||||
            if (c->name == name) {
 | 
			
		||||
                child = c;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (child)
 | 
			
		||||
            return {.o = mrpc::CreateNodeInfo{
 | 
			
		||||
                .id = child->id,
 | 
			
		||||
                .exists = true,
 | 
			
		||||
                .file = child->file
 | 
			
		||||
            }};
 | 
			
		||||
        auto id = user->next_node_id++;
 | 
			
		||||
        child = std::make_shared<Node>(Node{
 | 
			
		||||
            .id = id,
 | 
			
		||||
            .name = name,
 | 
			
		||||
            .file = file,
 | 
			
		||||
            .preview = false,
 | 
			
		||||
            .parent = parent,
 | 
			
		||||
            .size = 0
 | 
			
		||||
        });
 | 
			
		||||
        if (file) {
 | 
			
		||||
            auto path = files_dir / std::to_string(user->id) / std::to_string(id);
 | 
			
		||||
            std::ofstream{path};
 | 
			
		||||
        }
 | 
			
		||||
        user->nodes.emplace(id, child);
 | 
			
		||||
        parent->children.push_back(child);
 | 
			
		||||
        save();
 | 
			
		||||
        return {.o = mrpc::CreateNodeInfo{
 | 
			
		||||
            .id = id,
 | 
			
		||||
            .exists = false,
 | 
			
		||||
            .file = file
 | 
			
		||||
        }};
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string> Server::FS_move_nodes(std::string &&token, std::vector<std::uint64_t> &&node_ids_vec, std::uint64_t &&parent_id) {
 | 
			
		||||
    check_user_optional();
 | 
			
		||||
    auto parent = get_node(user, parent_id);
 | 
			
		||||
    if (!parent) return "Invalid parent";
 | 
			
		||||
 | 
			
		||||
    if (node_ids_vec.empty())
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
 | 
			
		||||
    std::unordered_set<std::uint64_t> node_ids{node_ids_vec.begin(), node_ids_vec.end()};
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        std::unique_lock lock{user->node_lock};
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            auto node_parent = parent;
 | 
			
		||||
            while (parent) {
 | 
			
		||||
                if (node_ids.contains(node_parent->id))
 | 
			
		||||
                    return "Tried to move node into one of it's subfolders";
 | 
			
		||||
                node_parent = node_parent->parent;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        std::vector<std::shared_ptr<Node>> nodes;
 | 
			
		||||
        std::unordered_set<std::string> node_names;
 | 
			
		||||
        nodes.reserve(node_ids.size());
 | 
			
		||||
        node_names.reserve(node_ids.size());
 | 
			
		||||
        for (auto id : node_ids) {
 | 
			
		||||
            auto node = get_node(user, id);
 | 
			
		||||
            if (!node) return "Invalid node " + std::to_string(id);
 | 
			
		||||
            if (!node_names.emplace(node->name).second)
 | 
			
		||||
                return "Tried to move multiple nodes with the name " + node->name;
 | 
			
		||||
            nodes.push_back(node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const auto &child : parent->children) {
 | 
			
		||||
            if (node_names.contains(child->name))
 | 
			
		||||
                return "Tried to overwrite existing folder/file";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const auto &node : nodes) {
 | 
			
		||||
            node->parent->children.remove(node);
 | 
			
		||||
            node->parent = parent;
 | 
			
		||||
            parent->children.push_back(node);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    save();
 | 
			
		||||
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Server::FS_delete_nodes(std::string &&token, std::vector<std::uint64_t> &&nodes, mrpc::MRPCStream<std::string> &&stream) {
 | 
			
		||||
    check_user() {
 | 
			
		||||
        stream.close();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::thread deleter{[this, nodes = std::move(nodes), user = std::move(user), stream = std::move(stream)](){
 | 
			
		||||
        for (const auto& node : nodes) {
 | 
			
		||||
            if (node == 0)
 | 
			
		||||
                continue;
 | 
			
		||||
            delete_node(user, node, [&stream](const std::string &log){ stream.send(log); });
 | 
			
		||||
        }
 | 
			
		||||
        stream.close();
 | 
			
		||||
        save();
 | 
			
		||||
    }};
 | 
			
		||||
    deleter.detach();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<std::string> Server::FS_download_preview(std::string &&token, std::uint64_t &&node_id) {
 | 
			
		||||
    check_user_response();
 | 
			
		||||
    std::shared_lock lock{user->node_lock};
 | 
			
		||||
    auto node = get_node(user, node_id);
 | 
			
		||||
    if (!node) return { .e = "Invalid node" };
 | 
			
		||||
 | 
			
		||||
    if (!node->preview)
 | 
			
		||||
        return { .e = "No preview" };
 | 
			
		||||
 | 
			
		||||
    std::vector<std::uint8_t> preview;
 | 
			
		||||
 | 
			
		||||
    auto path = (user->user_dir / std::to_string(node_id)).replace_extension("png");
 | 
			
		||||
 | 
			
		||||
    auto size = std::filesystem::file_size(path);
 | 
			
		||||
    preview.resize(size);
 | 
			
		||||
 | 
			
		||||
    std::FILE *f = std::fopen(path.c_str(), "rb");
 | 
			
		||||
    std::fread(preview.data(), sizeof(std::uint8_t), size, f);
 | 
			
		||||
    std::fclose(f);
 | 
			
		||||
 | 
			
		||||
    return { .o = Botan::base64_encode(preview) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::Response<std::string> Server::FS_get_mime(std::string &&token, std::uint64_t &&node_id) {
 | 
			
		||||
    check_user_response();
 | 
			
		||||
    auto node = get_node(user, node_id);
 | 
			
		||||
    if (!node) return { .e = "Invalid node" };
 | 
			
		||||
    if (!node->file) return { .e = "Node is a directory" };
 | 
			
		||||
    return { .o = get_mime_type(node->name) };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										136
									
								
								src/server/mail.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/server/mail.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
			
		||||
#include <asio.hpp>
 | 
			
		||||
#include <botan_all.h>
 | 
			
		||||
#include <botan_asio/asio_stream.h>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
#include "server_internal.hxx"
 | 
			
		||||
 | 
			
		||||
struct SocketData {
 | 
			
		||||
    asio::streambuf socket_buf;
 | 
			
		||||
    std::istream socket_istream{&socket_buf};
 | 
			
		||||
    std::string last_send;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct Exception : public std::exception {
 | 
			
		||||
    explicit Exception(std::string msg) : msg(std::move(msg)) {}
 | 
			
		||||
    [[nodiscard]] const char* what() const noexcept override { return msg.c_str(); }
 | 
			
		||||
    std::string msg;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename Socket>
 | 
			
		||||
void expect_code(SocketData &data, Socket &s, const std::string& code) {
 | 
			
		||||
    std::string line;
 | 
			
		||||
    do {
 | 
			
		||||
        std::string buf;
 | 
			
		||||
        asio::read_until(s, data.socket_buf, "\n");
 | 
			
		||||
        std::getline(data.socket_istream, line, '\n');
 | 
			
		||||
    } while (line[3] == '-');
 | 
			
		||||
    if (std::string_view{line}.substr(0, 3) != code)
 | 
			
		||||
        throw Exception{"Request: '" + data.last_send + "' Expected: " + code + " got: " + line};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename Socket>
 | 
			
		||||
void send(SocketData &data, Socket &s, std::string l) {
 | 
			
		||||
    data.last_send = l.substr(0, l.find_first_of('\r'));
 | 
			
		||||
    l += "\r\n";
 | 
			
		||||
    asio::write(s, asio::buffer(l, l.size()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string get_date() {
 | 
			
		||||
    char buf[80];
 | 
			
		||||
    auto t = std::time(nullptr);
 | 
			
		||||
    auto ti = localtime (&t);
 | 
			
		||||
    std::strftime(buf, sizeof(buf), "%a, %d %B %Y %T %z", ti);//Format time as string
 | 
			
		||||
    return std::string{buf};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string get_hostname() {
 | 
			
		||||
    try {
 | 
			
		||||
        return asio::ip::host_name();
 | 
			
		||||
    } catch (std::exception &_) {
 | 
			
		||||
        return "";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct CredMan : public Botan::Credentials_Manager {
 | 
			
		||||
    Botan::System_Certificate_Store str;
 | 
			
		||||
    CredMan() = default;
 | 
			
		||||
    std::vector<Botan::Certificate_Store*> trusted_certificate_authorities(const std::string&, const std::string&) override {
 | 
			
		||||
        return {&str};
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct Policy : public Botan::TLS::Strict_Policy {
 | 
			
		||||
    [[nodiscard]] bool require_cert_revocation_info() const override { return false; }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void Server::send_mail(const std::string &email, const std::string &title, const std::string &body) {
 | 
			
		||||
    static std::string host_name = get_hostname();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        std::string msg = fmt::format(
 | 
			
		||||
            "Date: {}\r\n"
 | 
			
		||||
            "To: <{}>\r\n"
 | 
			
		||||
            "From: MFileserver <{}>\r\n"
 | 
			
		||||
            "Subject: {}\r\n"
 | 
			
		||||
            "\r\n"
 | 
			
		||||
            "{}\r\n.",
 | 
			
		||||
            get_date(), email, config.smtp_from, title, body
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        asio::io_service ctx;
 | 
			
		||||
        auto ssl_ctx = std::make_shared<Botan::TLS::Context>(
 | 
			
		||||
            std::make_shared<CredMan>(),
 | 
			
		||||
            std::make_shared<Botan::AutoSeeded_RNG>(),
 | 
			
		||||
            std::make_shared<Botan::TLS::Session_Manager_Noop>(),
 | 
			
		||||
            std::make_shared<Policy>()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        asio::ip::tcp::socket s{ctx};
 | 
			
		||||
        asio::ip::tcp::resolver res{ctx};
 | 
			
		||||
 | 
			
		||||
        SocketData data;
 | 
			
		||||
 | 
			
		||||
        asio::connect(s, res.resolve(config.smtp_host, std::to_string(config.smtp_port)));
 | 
			
		||||
        expect_code(data, s, "220");
 | 
			
		||||
 | 
			
		||||
        send(data, s, "EHLO " + host_name);
 | 
			
		||||
        expect_code(data, s, "250");
 | 
			
		||||
 | 
			
		||||
        send(data, s, "STARTTLS");
 | 
			
		||||
        expect_code(data, s, "220");
 | 
			
		||||
 | 
			
		||||
        // switch_to_ssl
 | 
			
		||||
        Botan::TLS::Stream<asio::ip::tcp::socket> ss(std::move(s), ssl_ctx);
 | 
			
		||||
        ss.handshake(Botan::TLS::Connection_Side::Client);
 | 
			
		||||
 | 
			
		||||
        send(data, ss, "EHLO " + host_name);
 | 
			
		||||
        expect_code(data, ss, "250");
 | 
			
		||||
 | 
			
		||||
        send(data, ss, "AUTH LOGIN");
 | 
			
		||||
        expect_code(data, ss, "334");
 | 
			
		||||
 | 
			
		||||
        send(data, ss, Botan::base64_encode((std::uint8_t*)config.smtp_user.data(), config.smtp_user.size()));
 | 
			
		||||
        expect_code(data, ss, "334");
 | 
			
		||||
 | 
			
		||||
        send(data, ss, Botan::base64_encode((std::uint8_t*)config.smtp_pass.data(), config.smtp_pass.size()));
 | 
			
		||||
        expect_code(data, ss, "235");
 | 
			
		||||
 | 
			
		||||
        send(data, ss, "MAIL FROM:<" + config.smtp_from + ">");
 | 
			
		||||
        expect_code(data, ss, "250");
 | 
			
		||||
 | 
			
		||||
        send(data, ss, "RCPT TO:<" + email + ">");
 | 
			
		||||
        expect_code(data, ss, "250");
 | 
			
		||||
 | 
			
		||||
        send(data, ss, "DATA");
 | 
			
		||||
        expect_code(data, ss, "354");
 | 
			
		||||
 | 
			
		||||
        send(data, ss, msg);
 | 
			
		||||
        expect_code(data, ss, "250");
 | 
			
		||||
 | 
			
		||||
        send(data, ss, "QUIT");
 | 
			
		||||
        expect_code(data, ss, "221");
 | 
			
		||||
    } catch (const std::exception &e) {
 | 
			
		||||
        spdlog::error("Failed to send mail");
 | 
			
		||||
        spdlog::error(e.what());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										618
									
								
								src/server/mrpc/fileserver.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										618
									
								
								src/server/mrpc/fileserver.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,618 @@
 | 
			
		||||
#include "fileserver.hxx"
 | 
			
		||||
#include <corvusoft/restbed/session.hpp>
 | 
			
		||||
#include <corvusoft/restbed/resource.hpp>
 | 
			
		||||
#include <corvusoft/restbed/request.hpp>
 | 
			
		||||
 | 
			
		||||
using namespace mrpc;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
inline std::string& operator<<(std::string &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsString())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetString();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::string &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.String(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::int8_t& operator<<(std::int8_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsInt())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetInt();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::int8_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Int(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::int16_t& operator<<(std::int16_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsInt())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetInt();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::int16_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Int(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::int32_t& operator<<(std::int32_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsInt())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetInt();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::int32_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Int(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::int64_t& operator<<(std::int64_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsInt64())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetInt64();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::int64_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Int64(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::uint8_t& operator<<(std::uint8_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsUint())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetUint();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::uint8_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Uint(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::uint16_t& operator<<(std::uint16_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsUint())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetUint();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::uint16_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Uint(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::uint32_t& operator<<(std::uint32_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsUint())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetUint();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::uint32_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Uint(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::uint64_t& operator<<(std::uint64_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsUint64())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetUint64();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::uint64_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Uint64(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline bool& operator<<(bool &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsBool())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetBool();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const bool &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Bool(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::float_t& operator<<(std::float_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsDouble())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetDouble();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::float_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Double(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
inline std::double_t& operator<<(std::double_t &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsDouble())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    v = j.GetDouble();
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::double_t &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Double(v);
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::nullptr_t &, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.Null();
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
inline std::vector<T>& operator<<(std::vector<T> &v, const rapidjson::Value &j);
 | 
			
		||||
template<typename T>
 | 
			
		||||
inline std::optional<T>& operator<<(std::optional<T> &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (j.IsNull())
 | 
			
		||||
        v = std::nullopt;
 | 
			
		||||
    else {
 | 
			
		||||
        T t;
 | 
			
		||||
        t << j;
 | 
			
		||||
        v = std::move(t);
 | 
			
		||||
    }
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
inline std::vector<T>& operator<<(std::vector<T> &v, const rapidjson::Value &j) {
 | 
			
		||||
    if (!j.IsArray())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    for (const auto &e : j.GetArray()) {
 | 
			
		||||
        T t;
 | 
			
		||||
        t << e;
 | 
			
		||||
        v.push_back(std::move(t));
 | 
			
		||||
    }
 | 
			
		||||
    return v;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::vector<T> &v, mrpc::MRPCJWriter &w);
 | 
			
		||||
template<typename T>
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::optional<T> &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    if (v.has_value())
 | 
			
		||||
        v.value() >> w;
 | 
			
		||||
    else
 | 
			
		||||
        w.Null();
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
inline mrpc::MRPCJWriter& operator>>(const std::vector<T> &v, mrpc::MRPCJWriter &w) {
 | 
			
		||||
    w.StartArray();
 | 
			
		||||
    for (const auto &e : v)
 | 
			
		||||
        e >> w;
 | 
			
		||||
    w.EndArray();
 | 
			
		||||
    return w;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline const rapidjson::Value& json_get(const rapidjson::Value &j, const char *key) {
 | 
			
		||||
    auto member = j.FindMember(key);
 | 
			
		||||
    if (member == j.MemberEnd())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    return member->value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace mrpc {
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
MRPCJWriter& Response<T>::operator>>(MRPCJWriter &__w) const {
 | 
			
		||||
    __w.StartObject();
 | 
			
		||||
    __w.Key("e", 1);
 | 
			
		||||
    e >> __w;
 | 
			
		||||
    __w.Key("o", 1);
 | 
			
		||||
    o >> __w;
 | 
			
		||||
    __w.EndObject();
 | 
			
		||||
    return __w;
 | 
			
		||||
}
 | 
			
		||||
template<typename T>
 | 
			
		||||
Response<T>& Response<T>::operator<<(const rapidjson::Value &__j) {
 | 
			
		||||
    using namespace mrpc;
 | 
			
		||||
    e << json_get(__j, "e");
 | 
			
		||||
    o << json_get(__j, "o");
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MRPCJWriter& LoginResponse::operator>>(MRPCJWriter &__w) const {
 | 
			
		||||
    __w.StartObject();
 | 
			
		||||
    __w.Key("otp_needed", 10);
 | 
			
		||||
    otp_needed >> __w;
 | 
			
		||||
    __w.Key("token", 5);
 | 
			
		||||
    token >> __w;
 | 
			
		||||
    __w.EndObject();
 | 
			
		||||
    return __w;
 | 
			
		||||
}
 | 
			
		||||
LoginResponse& LoginResponse::operator<<(const rapidjson::Value &__j) {
 | 
			
		||||
    using namespace mrpc;
 | 
			
		||||
    otp_needed << json_get(__j, "otp_needed");
 | 
			
		||||
    token << json_get(__j, "token");
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MRPCJWriter& Session::operator>>(MRPCJWriter &__w) const {
 | 
			
		||||
    __w.StartObject();
 | 
			
		||||
    __w.Key("name", 4);
 | 
			
		||||
    name >> __w;
 | 
			
		||||
    __w.Key("tfa_enabled", 11);
 | 
			
		||||
    tfa_enabled >> __w;
 | 
			
		||||
    __w.Key("admin", 5);
 | 
			
		||||
    admin >> __w;
 | 
			
		||||
    __w.Key("sudo", 4);
 | 
			
		||||
    sudo >> __w;
 | 
			
		||||
    __w.EndObject();
 | 
			
		||||
    return __w;
 | 
			
		||||
}
 | 
			
		||||
Session& Session::operator<<(const rapidjson::Value &__j) {
 | 
			
		||||
    using namespace mrpc;
 | 
			
		||||
    name << json_get(__j, "name");
 | 
			
		||||
    tfa_enabled << json_get(__j, "tfa_enabled");
 | 
			
		||||
    admin << json_get(__j, "admin");
 | 
			
		||||
    sudo << json_get(__j, "sudo");
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MRPCJWriter& UserInfo::operator>>(MRPCJWriter &__w) const {
 | 
			
		||||
    __w.StartObject();
 | 
			
		||||
    __w.Key("id", 2);
 | 
			
		||||
    id >> __w;
 | 
			
		||||
    __w.Key("name", 4);
 | 
			
		||||
    name >> __w;
 | 
			
		||||
    __w.Key("tfa", 3);
 | 
			
		||||
    tfa >> __w;
 | 
			
		||||
    __w.Key("admin", 5);
 | 
			
		||||
    admin >> __w;
 | 
			
		||||
    __w.Key("enabled", 7);
 | 
			
		||||
    enabled >> __w;
 | 
			
		||||
    __w.EndObject();
 | 
			
		||||
    return __w;
 | 
			
		||||
}
 | 
			
		||||
UserInfo& UserInfo::operator<<(const rapidjson::Value &__j) {
 | 
			
		||||
    using namespace mrpc;
 | 
			
		||||
    id << json_get(__j, "id");
 | 
			
		||||
    name << json_get(__j, "name");
 | 
			
		||||
    tfa << json_get(__j, "tfa");
 | 
			
		||||
    admin << json_get(__j, "admin");
 | 
			
		||||
    enabled << json_get(__j, "enabled");
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MRPCJWriter& Node::operator>>(MRPCJWriter &__w) const {
 | 
			
		||||
    __w.StartObject();
 | 
			
		||||
    __w.Key("id", 2);
 | 
			
		||||
    id >> __w;
 | 
			
		||||
    __w.Key("name", 4);
 | 
			
		||||
    name >> __w;
 | 
			
		||||
    __w.Key("file", 4);
 | 
			
		||||
    file >> __w;
 | 
			
		||||
    __w.Key("preview", 7);
 | 
			
		||||
    preview >> __w;
 | 
			
		||||
    __w.Key("parent", 6);
 | 
			
		||||
    parent >> __w;
 | 
			
		||||
    __w.Key("size", 4);
 | 
			
		||||
    size >> __w;
 | 
			
		||||
    __w.Key("children", 8);
 | 
			
		||||
    children >> __w;
 | 
			
		||||
    __w.EndObject();
 | 
			
		||||
    return __w;
 | 
			
		||||
}
 | 
			
		||||
Node& Node::operator<<(const rapidjson::Value &__j) {
 | 
			
		||||
    using namespace mrpc;
 | 
			
		||||
    id << json_get(__j, "id");
 | 
			
		||||
    name << json_get(__j, "name");
 | 
			
		||||
    file << json_get(__j, "file");
 | 
			
		||||
    preview << json_get(__j, "preview");
 | 
			
		||||
    parent << json_get(__j, "parent");
 | 
			
		||||
    size << json_get(__j, "size");
 | 
			
		||||
    children << json_get(__j, "children");
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MRPCJWriter& CreateNodeInfo::operator>>(MRPCJWriter &__w) const {
 | 
			
		||||
    __w.StartObject();
 | 
			
		||||
    __w.Key("id", 2);
 | 
			
		||||
    id >> __w;
 | 
			
		||||
    __w.Key("exists", 6);
 | 
			
		||||
    exists >> __w;
 | 
			
		||||
    __w.Key("file", 4);
 | 
			
		||||
    file >> __w;
 | 
			
		||||
    __w.EndObject();
 | 
			
		||||
    return __w;
 | 
			
		||||
}
 | 
			
		||||
CreateNodeInfo& CreateNodeInfo::operator<<(const rapidjson::Value &__j) {
 | 
			
		||||
    using namespace mrpc;
 | 
			
		||||
    id << json_get(__j, "id");
 | 
			
		||||
    exists << json_get(__j, "exists");
 | 
			
		||||
    file << json_get(__j, "file");
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MRPCJWriter& ZipInfo::operator>>(MRPCJWriter &__w) const {
 | 
			
		||||
    __w.StartObject();
 | 
			
		||||
    __w.Key("done", 4);
 | 
			
		||||
    done >> __w;
 | 
			
		||||
    __w.Key("progress", 8);
 | 
			
		||||
    progress >> __w;
 | 
			
		||||
    __w.Key("total", 5);
 | 
			
		||||
    total >> __w;
 | 
			
		||||
    __w.EndObject();
 | 
			
		||||
    return __w;
 | 
			
		||||
}
 | 
			
		||||
ZipInfo& ZipInfo::operator<<(const rapidjson::Value &__j) {
 | 
			
		||||
    using namespace mrpc;
 | 
			
		||||
    done << json_get(__j, "done");
 | 
			
		||||
    progress << json_get(__j, "progress");
 | 
			
		||||
    total << json_get(__j, "total");
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MRPCJWriter& PathSegment::operator>>(MRPCJWriter &__w) const {
 | 
			
		||||
    __w.StartObject();
 | 
			
		||||
    __w.Key("name", 4);
 | 
			
		||||
    name >> __w;
 | 
			
		||||
    __w.Key("id", 2);
 | 
			
		||||
    id >> __w;
 | 
			
		||||
    __w.EndObject();
 | 
			
		||||
    return __w;
 | 
			
		||||
}
 | 
			
		||||
PathSegment& PathSegment::operator<<(const rapidjson::Value &__j) {
 | 
			
		||||
    using namespace mrpc;
 | 
			
		||||
    name << json_get(__j, "name");
 | 
			
		||||
    id << json_get(__j, "id");
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
void send_msg(const std::shared_ptr<restbed::Session> &c, const T &v) {
 | 
			
		||||
    if (c->is_closed())
 | 
			
		||||
        return;
 | 
			
		||||
    rapidjson::StringBuffer s;
 | 
			
		||||
    mrpc::MRPCJWriter writer{s};
 | 
			
		||||
    v >> writer;
 | 
			
		||||
    const auto body_ptr = s.GetString();
 | 
			
		||||
    const auto body = restbed::Bytes{body_ptr, body_ptr+s.GetLength()};
 | 
			
		||||
    c->yield(
 | 
			
		||||
        200,
 | 
			
		||||
        body,
 | 
			
		||||
        std::multimap<std::string, std::string>{
 | 
			
		||||
            {"Content-Type", "application/json"},
 | 
			
		||||
            {"Content-Length", std::to_string(body.size())}
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
void send_sse_msg(const std::shared_ptr<restbed::Session> &c, const T &v) {
 | 
			
		||||
    if (c->is_closed())
 | 
			
		||||
        return;
 | 
			
		||||
    rapidjson::StringBuffer s;
 | 
			
		||||
    std::memcpy(s.Push(5), "data:", 5);
 | 
			
		||||
    mrpc::MRPCJWriter writer{s};
 | 
			
		||||
    v >> writer;
 | 
			
		||||
    std::memcpy(s.Push(2), "\n\n", 2);
 | 
			
		||||
    const auto body_ptr = s.GetString();
 | 
			
		||||
    const auto body = restbed::Bytes{body_ptr, body_ptr+s.GetLength()};
 | 
			
		||||
    c->yield(body);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mrpc::MRPCStreamImpl::MRPCStreamImpl(const std::shared_ptr<restbed::Session> &conn) : conn(conn) {
 | 
			
		||||
    conn->yield(
 | 
			
		||||
        200,
 | 
			
		||||
        std::multimap<std::string, std::string>{
 | 
			
		||||
		    {"Cache-Control", "no-cache"},
 | 
			
		||||
		    {"Content-Type", "text/event-stream"}
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void mrpc::MRPCStreamImpl::close() const noexcept { conn->close("data:null\n\n"); }
 | 
			
		||||
bool mrpc::MRPCStreamImpl::is_open() const noexcept { return conn->is_open(); }
 | 
			
		||||
template<> void MRPCStream<std::string>::send(const std::string &v) const noexcept { send_sse_msg(conn, v); }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
mrpc::MRPCServer::MRPCServer(std::shared_ptr<restbed::Resource> &r) {
 | 
			
		||||
    r->set_method_handler("POST", [this](const std::shared_ptr<restbed::Session>& s) {
 | 
			
		||||
        const auto req = s->get_request();
 | 
			
		||||
        const auto body_len = req->get_header("Content-Length", 0);
 | 
			
		||||
        s->fetch(body_len, [this](const std::shared_ptr<restbed::Session> &s, auto &&body) {
 | 
			
		||||
            try { msg_handler(s, body); }
 | 
			
		||||
            catch (const std::exception &_) { s->close(400); }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void mrpc::MRPCServer::msg_handler(const std::shared_ptr<restbed::Session> __c, const restbed::Bytes &__msg) {
 | 
			
		||||
    rapidjson::Document __j;
 | 
			
		||||
    __j.Parse((const char*)__msg.data(), __msg.size());
 | 
			
		||||
    if (__j.HasParseError())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    std::string __service, __method;
 | 
			
		||||
    __service << json_get(__j, "service");
 | 
			
		||||
    __method << json_get(__j, "method");
 | 
			
		||||
    auto __data_member = __j.FindMember("data");
 | 
			
		||||
    if (__data_member == __j.MemberEnd() || !__data_member->value.IsObject())
 | 
			
		||||
        throw std::exception{};
 | 
			
		||||
    auto &__data = __data_member->value;
 | 
			
		||||
    if (__service == "Auth") {
 | 
			
		||||
        if (__method == "signup") {
 | 
			
		||||
            
 | 
			
		||||
            std::string username; username << json_get(__data, "username");
 | 
			
		||||
                        std::string password; password << json_get(__data, "password");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_signup(std::move(username), std::move(password)));
 | 
			
		||||
        } else if (__method == "login") {
 | 
			
		||||
            
 | 
			
		||||
            std::string username; username << json_get(__data, "username");
 | 
			
		||||
                        std::string password; password << json_get(__data, "password");
 | 
			
		||||
                        std::optional<std::string> otp; otp << json_get(__data, "otp");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_login(std::move(username), std::move(password), std::move(otp)));
 | 
			
		||||
        } else if (__method == "send_recovery_key") {
 | 
			
		||||
            
 | 
			
		||||
            std::string username; username << json_get(__data, "username");
 | 
			
		||||
            
 | 
			
		||||
            Auth_send_recovery_key(std::move(username)); send_msg(__c, nullptr);
 | 
			
		||||
        } else if (__method == "reset_password") {
 | 
			
		||||
            
 | 
			
		||||
            std::string key; key << json_get(__data, "key");
 | 
			
		||||
                        std::string password; password << json_get(__data, "password");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_reset_password(std::move(key), std::move(password)));
 | 
			
		||||
        } else if (__method == "change_password") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::string old_pw; old_pw << json_get(__data, "old_pw");
 | 
			
		||||
                        std::string new_pw; new_pw << json_get(__data, "new_pw");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_change_password(std::move(token), std::move(old_pw), std::move(new_pw)));
 | 
			
		||||
        } else if (__method == "logout") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            Auth_logout(std::move(token)); send_msg(__c, nullptr);
 | 
			
		||||
        } else if (__method == "logout_all") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_logout_all(std::move(token)));
 | 
			
		||||
        } else if (__method == "tfa_setup_mail") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_tfa_setup_mail(std::move(token)));
 | 
			
		||||
        } else if (__method == "tfa_setup_totp") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_tfa_setup_totp(std::move(token)));
 | 
			
		||||
        } else if (__method == "tfa_complete") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::string otp; otp << json_get(__data, "otp");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_tfa_complete(std::move(token), std::move(otp)));
 | 
			
		||||
        } else if (__method == "tfa_disable") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_tfa_disable(std::move(token)));
 | 
			
		||||
        } else if (__method == "delete_user") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_delete_user(std::move(token)));
 | 
			
		||||
        } else if (__method == "session_info") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Auth_session_info(std::move(token)));
 | 
			
		||||
        }
 | 
			
		||||
        else { throw std::exception{}; }
 | 
			
		||||
    } else if (__service == "Admin") {
 | 
			
		||||
        if (__method == "list_users") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_list_users(std::move(token)));
 | 
			
		||||
        } else if (__method == "delete_user") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t user; user << json_get(__data, "user");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_delete_user(std::move(token), std::move(user)));
 | 
			
		||||
        } else if (__method == "logout") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t user; user << json_get(__data, "user");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_logout(std::move(token), std::move(user)));
 | 
			
		||||
        } else if (__method == "disable_tfa") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t user; user << json_get(__data, "user");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_disable_tfa(std::move(token), std::move(user)));
 | 
			
		||||
        } else if (__method == "set_admin") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t user; user << json_get(__data, "user");
 | 
			
		||||
                        bool admin; admin << json_get(__data, "admin");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_set_admin(std::move(token), std::move(user), std::move(admin)));
 | 
			
		||||
        } else if (__method == "set_enabled") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t user; user << json_get(__data, "user");
 | 
			
		||||
                        bool enabled; enabled << json_get(__data, "enabled");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_set_enabled(std::move(token), std::move(user), std::move(enabled)));
 | 
			
		||||
        } else if (__method == "sudo") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t user; user << json_get(__data, "user");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_sudo(std::move(token), std::move(user)));
 | 
			
		||||
        } else if (__method == "unsudo") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_unsudo(std::move(token)));
 | 
			
		||||
        } else if (__method == "shutdown") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, Admin_shutdown(std::move(token)));
 | 
			
		||||
        }
 | 
			
		||||
        else { throw std::exception{}; }
 | 
			
		||||
    } else if (__service == "FS") {
 | 
			
		||||
        if (__method == "get_node") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t node; node << json_get(__data, "node");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, FS_get_node(std::move(token), std::move(node)));
 | 
			
		||||
        } else if (__method == "get_path") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t node; node << json_get(__data, "node");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, FS_get_path(std::move(token), std::move(node)));
 | 
			
		||||
        } else if (__method == "get_nodes_size") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::vector<std::uint64_t> nodes; nodes << json_get(__data, "nodes");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, FS_get_nodes_size(std::move(token), std::move(nodes)));
 | 
			
		||||
        } else if (__method == "create_node") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        bool file; file << json_get(__data, "file");
 | 
			
		||||
                        std::uint64_t parent; parent << json_get(__data, "parent");
 | 
			
		||||
                        std::string name; name << json_get(__data, "name");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, FS_create_node(std::move(token), std::move(file), std::move(parent), std::move(name)));
 | 
			
		||||
        } else if (__method == "move_nodes") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::vector<std::uint64_t> nodes; nodes << json_get(__data, "nodes");
 | 
			
		||||
                        std::uint64_t parent; parent << json_get(__data, "parent");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, FS_move_nodes(std::move(token), std::move(nodes), std::move(parent)));
 | 
			
		||||
        } else if (__method == "delete_nodes") {
 | 
			
		||||
            auto __stream = MRPCStream<std::string>{__c};
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::vector<std::uint64_t> nodes; nodes << json_get(__data, "nodes");
 | 
			
		||||
            
 | 
			
		||||
            FS_delete_nodes(std::move(token), std::move(nodes), std::move(__stream));
 | 
			
		||||
        } else if (__method == "download_preview") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t node; node << json_get(__data, "node");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, FS_download_preview(std::move(token), std::move(node)));
 | 
			
		||||
        } else if (__method == "get_mime") {
 | 
			
		||||
            
 | 
			
		||||
            std::string token; token << json_get(__data, "token");
 | 
			
		||||
                        std::uint64_t node; node << json_get(__data, "node");
 | 
			
		||||
            
 | 
			
		||||
            send_msg(__c, FS_get_mime(std::move(token), std::move(node)));
 | 
			
		||||
        }
 | 
			
		||||
        else { throw std::exception{}; }
 | 
			
		||||
    }
 | 
			
		||||
    else { throw std::exception{}; }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										161
									
								
								src/server/mrpc/fileserver.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/server/mrpc/fileserver.hxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#ifndef MRPC_GEN_H
 | 
			
		||||
#define MRPC_GEN_H
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include <corvusoft/restbed/byte.hpp>
 | 
			
		||||
#define RAPIDJSON_HAS_STDSTRING 1
 | 
			
		||||
#include <rapidjson/stringbuffer.h>
 | 
			
		||||
#include <rapidjson/writer.h>
 | 
			
		||||
#include <rapidjson/document.h>
 | 
			
		||||
 | 
			
		||||
namespace restbed {
 | 
			
		||||
    class Resource;
 | 
			
		||||
    class Session;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace mrpc {
 | 
			
		||||
using MRPCJWriter = rapidjson::Writer<rapidjson::StringBuffer>;
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
struct Response;
 | 
			
		||||
struct LoginResponse;
 | 
			
		||||
struct Session;
 | 
			
		||||
struct UserInfo;
 | 
			
		||||
struct Node;
 | 
			
		||||
struct CreateNodeInfo;
 | 
			
		||||
struct ZipInfo;
 | 
			
		||||
struct PathSegment;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
struct Response {
 | 
			
		||||
    std::optional<std::string> e;
 | 
			
		||||
    std::optional<T> o;
 | 
			
		||||
 | 
			
		||||
    MRPCJWriter& operator >>(MRPCJWriter&) const;
 | 
			
		||||
    Response& operator <<(const rapidjson::Value&);
 | 
			
		||||
};
 | 
			
		||||
struct LoginResponse {
 | 
			
		||||
    bool otp_needed;
 | 
			
		||||
    std::optional<std::string> token;
 | 
			
		||||
 | 
			
		||||
    MRPCJWriter& operator >>(MRPCJWriter&) const;
 | 
			
		||||
    LoginResponse& operator <<(const rapidjson::Value&);
 | 
			
		||||
};
 | 
			
		||||
struct Session {
 | 
			
		||||
    std::string name;
 | 
			
		||||
    bool tfa_enabled;
 | 
			
		||||
    bool admin;
 | 
			
		||||
    bool sudo;
 | 
			
		||||
 | 
			
		||||
    MRPCJWriter& operator >>(MRPCJWriter&) const;
 | 
			
		||||
    Session& operator <<(const rapidjson::Value&);
 | 
			
		||||
};
 | 
			
		||||
struct UserInfo {
 | 
			
		||||
    std::uint64_t id;
 | 
			
		||||
    std::string name;
 | 
			
		||||
    bool tfa;
 | 
			
		||||
    bool admin;
 | 
			
		||||
    bool enabled;
 | 
			
		||||
 | 
			
		||||
    MRPCJWriter& operator >>(MRPCJWriter&) const;
 | 
			
		||||
    UserInfo& operator <<(const rapidjson::Value&);
 | 
			
		||||
};
 | 
			
		||||
struct Node {
 | 
			
		||||
    std::uint64_t id;
 | 
			
		||||
    std::string name;
 | 
			
		||||
    bool file;
 | 
			
		||||
    bool preview;
 | 
			
		||||
    std::optional<std::uint64_t> parent;
 | 
			
		||||
    std::optional<std::uint64_t> size;
 | 
			
		||||
    std::optional<std::vector<Node>> children;
 | 
			
		||||
 | 
			
		||||
    MRPCJWriter& operator >>(MRPCJWriter&) const;
 | 
			
		||||
    Node& operator <<(const rapidjson::Value&);
 | 
			
		||||
};
 | 
			
		||||
struct CreateNodeInfo {
 | 
			
		||||
    std::uint64_t id;
 | 
			
		||||
    bool exists;
 | 
			
		||||
    bool file;
 | 
			
		||||
 | 
			
		||||
    MRPCJWriter& operator >>(MRPCJWriter&) const;
 | 
			
		||||
    CreateNodeInfo& operator <<(const rapidjson::Value&);
 | 
			
		||||
};
 | 
			
		||||
struct ZipInfo {
 | 
			
		||||
    bool done;
 | 
			
		||||
    std::uint64_t progress;
 | 
			
		||||
    std::uint64_t total;
 | 
			
		||||
 | 
			
		||||
    MRPCJWriter& operator >>(MRPCJWriter&) const;
 | 
			
		||||
    ZipInfo& operator <<(const rapidjson::Value&);
 | 
			
		||||
};
 | 
			
		||||
struct PathSegment {
 | 
			
		||||
    std::string name;
 | 
			
		||||
    std::optional<std::uint64_t> id;
 | 
			
		||||
 | 
			
		||||
    MRPCJWriter& operator >>(MRPCJWriter&) const;
 | 
			
		||||
    PathSegment& operator <<(const rapidjson::Value&);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct MRPCStreamImpl {
 | 
			
		||||
    void close() const noexcept;
 | 
			
		||||
    bool is_open() const noexcept;
 | 
			
		||||
protected:
 | 
			
		||||
    explicit MRPCStreamImpl(const std::shared_ptr<restbed::Session> &conn);
 | 
			
		||||
    std::shared_ptr<restbed::Session> conn;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename T>
 | 
			
		||||
struct MRPCStream final : MRPCStreamImpl {
 | 
			
		||||
    explicit MRPCStream(const std::shared_ptr<restbed::Session> &conn) : MRPCStreamImpl(conn) {}
 | 
			
		||||
    void send(const T &v) const noexcept;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template struct MRPCStream<std::string>;
 | 
			
		||||
 | 
			
		||||
struct MRPCServer {
 | 
			
		||||
    MRPCServer() = delete;
 | 
			
		||||
    explicit MRPCServer(std::shared_ptr<restbed::Resource>&);
 | 
			
		||||
private:
 | 
			
		||||
    virtual std::optional<std::string> Auth_signup(std::string &&username, std::string &&password) = 0;
 | 
			
		||||
    virtual Response<LoginResponse> Auth_login(std::string &&username, std::string &&password, std::optional<std::string> &&otp) = 0;
 | 
			
		||||
    virtual void Auth_send_recovery_key(std::string &&username) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Auth_reset_password(std::string &&key, std::string &&password) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Auth_change_password(std::string &&token, std::string &&old_pw, std::string &&new_pw) = 0;
 | 
			
		||||
    virtual void Auth_logout(std::string &&token) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Auth_logout_all(std::string &&token) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Auth_tfa_setup_mail(std::string &&token) = 0;
 | 
			
		||||
    virtual Response<std::string> Auth_tfa_setup_totp(std::string &&token) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Auth_tfa_complete(std::string &&token, std::string &&otp) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Auth_tfa_disable(std::string &&token) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Auth_delete_user(std::string &&token) = 0;
 | 
			
		||||
    virtual Response<Session> Auth_session_info(std::string &&token) = 0;
 | 
			
		||||
    virtual Response<std::vector<UserInfo>> Admin_list_users(std::string &&token) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Admin_delete_user(std::string &&token, std::uint64_t &&user) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Admin_logout(std::string &&token, std::uint64_t &&user) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Admin_disable_tfa(std::string &&token, std::uint64_t &&user) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Admin_set_admin(std::string &&token, std::uint64_t &&user, bool &&admin) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Admin_set_enabled(std::string &&token, std::uint64_t &&user, bool &&enabled) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Admin_sudo(std::string &&token, std::uint64_t &&user) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Admin_unsudo(std::string &&token) = 0;
 | 
			
		||||
    virtual std::optional<std::string> Admin_shutdown(std::string &&token) = 0;
 | 
			
		||||
    virtual Response<Node> FS_get_node(std::string &&token, std::uint64_t &&node) = 0;
 | 
			
		||||
    virtual Response<std::vector<PathSegment>> FS_get_path(std::string &&token, std::uint64_t &&node) = 0;
 | 
			
		||||
    virtual Response<std::uint64_t> FS_get_nodes_size(std::string &&token, std::vector<std::uint64_t> &&nodes) = 0;
 | 
			
		||||
    virtual Response<CreateNodeInfo> FS_create_node(std::string &&token, bool &&file, std::uint64_t &&parent, std::string &&name) = 0;
 | 
			
		||||
    virtual std::optional<std::string> FS_move_nodes(std::string &&token, std::vector<std::uint64_t> &&nodes, std::uint64_t &&parent) = 0;
 | 
			
		||||
    virtual void FS_delete_nodes(std::string &&token, std::vector<std::uint64_t> &&nodes, MRPCStream<std::string>&&) = 0;
 | 
			
		||||
    virtual Response<std::string> FS_download_preview(std::string &&token, std::uint64_t &&node) = 0;
 | 
			
		||||
    virtual Response<std::string> FS_get_mime(std::string &&token, std::uint64_t &&node) = 0;
 | 
			
		||||
 | 
			
		||||
    virtual void msg_handler(std::shared_ptr<restbed::Session>, const restbed::Bytes&) final;
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif // MRPC_GEN_H
 | 
			
		||||
							
								
								
									
										87
									
								
								src/server/server.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/server/server.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
#include "server_internal.hxx"
 | 
			
		||||
 | 
			
		||||
std::shared_ptr<Token> Server::get_token(const std::string &token) {
 | 
			
		||||
    std::shared_lock lock{token_lock};
 | 
			
		||||
    const auto &entry = tokens.find(token);
 | 
			
		||||
    if (entry == tokens.end())
 | 
			
		||||
        return nullptr;
 | 
			
		||||
    return entry->second;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::shared_ptr<User> Server::get_user(std::uint64_t id) {
 | 
			
		||||
    std::shared_lock lock{user_lock};
 | 
			
		||||
    const auto &entry = users.find(id);
 | 
			
		||||
    if (entry == users.end())
 | 
			
		||||
        return nullptr;
 | 
			
		||||
    return entry->second;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::shared_ptr<User> Server::is_token_valid(const std::string &token) {
 | 
			
		||||
    auto t = get_token(token);
 | 
			
		||||
    if (!t)
 | 
			
		||||
        return nullptr;
 | 
			
		||||
    if (Token::clock::now() <= t->expire) {
 | 
			
		||||
        t->refresh();
 | 
			
		||||
        return t->user;
 | 
			
		||||
    }
 | 
			
		||||
    {
 | 
			
		||||
        std::unique_lock lock{token_lock};
 | 
			
		||||
        tokens.erase(token);
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Server::logout_user(std::uint64_t id) {
 | 
			
		||||
    std::unique_lock lock{token_lock};
 | 
			
		||||
    for (auto it = tokens.begin(); it != tokens.end();) {
 | 
			
		||||
        if (it->second->user->id == id)
 | 
			
		||||
            tokens.erase(it++);
 | 
			
		||||
        else
 | 
			
		||||
            ++it;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Server::delete_user(const std::shared_ptr<User> &user) {
 | 
			
		||||
    std::unique_lock lock{user_lock};
 | 
			
		||||
    logout_user(user->id);
 | 
			
		||||
    delete_node(user, 0, [](const std::string&){});
 | 
			
		||||
    users.erase(user->id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Server::send_tfa_mail(const std::shared_ptr<User> &user) {
 | 
			
		||||
    std::lock_guard lock{mail_otp_lock};
 | 
			
		||||
    std::string code; code.reserve(10);
 | 
			
		||||
    for (int i = 0; i < 10; ++i) {
 | 
			
		||||
        auto j = auth_rng->next_byte();
 | 
			
		||||
        while (j > 249) j = auth_rng->next_byte();
 | 
			
		||||
        code.push_back('0' + (j%10));
 | 
			
		||||
    }
 | 
			
		||||
    mail_otp.emplace(code, std::make_pair(user->id, Token::clock::now() + std::chrono::minutes{10}));
 | 
			
		||||
    send_mail(user->name, "MFileserver - TFA code", "Your code is: " + code + "\r\nIt is valid for 10 minutes");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Server::check_mail_code(const std::shared_ptr<User> &user, const std::string &code) {
 | 
			
		||||
    std::lock_guard lock{mail_otp_lock};
 | 
			
		||||
    auto now = Token::clock::now();
 | 
			
		||||
    for (auto it = mail_otp.begin(); it != mail_otp.end();) {
 | 
			
		||||
        if (now >= it->second.second)
 | 
			
		||||
            mail_otp.erase(it++);
 | 
			
		||||
        else
 | 
			
		||||
            ++it;
 | 
			
		||||
    }
 | 
			
		||||
    const auto &entry = mail_otp.find(code);
 | 
			
		||||
    bool ok = entry != mail_otp.end() && entry->second.first == user->id;
 | 
			
		||||
    if (ok)
 | 
			
		||||
        mail_otp.erase(code);
 | 
			
		||||
    return ok;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Server::check_tfa_code(const std::shared_ptr<User> &user, const std::string &code_str) {
 | 
			
		||||
    Botan::OctetString secret{user->tfa_secret};
 | 
			
		||||
    Botan::TOTP totp{secret};
 | 
			
		||||
    try {
 | 
			
		||||
        std::uint32_t code = std::stoul(code_str);
 | 
			
		||||
        return totp.verify_totp(code, std::chrono::system_clock::now(), 1);
 | 
			
		||||
    } catch (std::exception &_) {}
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/server/server.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/server/server.hxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
#ifndef FILESERVER_SERVER_HXX
 | 
			
		||||
#define FILESERVER_SERVER_HXX
 | 
			
		||||
 | 
			
		||||
#include <corvusoft/restbed/service.hpp>
 | 
			
		||||
#include "mrpc/fileserver.hxx"
 | 
			
		||||
#include "../data/data.hxx"
 | 
			
		||||
 | 
			
		||||
extern std::shared_ptr<restbed::Service> g_service;
 | 
			
		||||
 | 
			
		||||
struct Server final : public mrpc::MRPCServer, public Data {
 | 
			
		||||
    explicit Server(std::shared_ptr<restbed::Resource> &ptr) : MRPCServer(ptr), Data() {}
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<Token> get_token(const std::string&);
 | 
			
		||||
    std::shared_ptr<User> is_token_valid(const std::string&);
 | 
			
		||||
    std::shared_ptr<User> get_user(std::uint64_t id);
 | 
			
		||||
 | 
			
		||||
    static void delete_node(const std::shared_ptr<User> &user, std::uint64_t id, const std::function<void(std::string)>& log);
 | 
			
		||||
    void logout_user(std::uint64_t id);
 | 
			
		||||
    void delete_user(const std::shared_ptr<User> &user);
 | 
			
		||||
    void send_tfa_mail(const std::shared_ptr<User> &user);
 | 
			
		||||
    static bool check_tfa_code(const std::shared_ptr<User> &user, const std::string &code);
 | 
			
		||||
    bool check_mail_code(const std::shared_ptr<User> &user, const std::string &code);
 | 
			
		||||
    void send_mail(const std::string& email, const std::string& title, const std::string& body);
 | 
			
		||||
    std::uint64_t nodes_size(const std::shared_ptr<User> &user, const std::vector<std::uint64_t> &ids);
 | 
			
		||||
 | 
			
		||||
    void download(const std::shared_ptr<restbed::Session>&);
 | 
			
		||||
    void download_multi(const std::shared_ptr<restbed::Session>&);
 | 
			
		||||
    void upload(const std::shared_ptr<restbed::Session>&);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    std::optional<std::string> Auth_signup(std::string &&username, std::string &&password) override;
 | 
			
		||||
    mrpc::Response<mrpc::LoginResponse> Auth_login(std::string &&username, std::string &&password, std::optional<std::string> &&otp) override;
 | 
			
		||||
    void Auth_send_recovery_key(std::string &&username) override;
 | 
			
		||||
    std::optional<std::string> Auth_reset_password(std::string &&key, std::string &&password) override;
 | 
			
		||||
    std::optional<std::string> Auth_change_password(std::string &&token, std::string &&old_pw, std::string &&new_pw) override;
 | 
			
		||||
    void Auth_logout(std::string &&token) override;
 | 
			
		||||
    std::optional<std::string> Auth_logout_all(std::string &&token) override;
 | 
			
		||||
    std::optional<std::string> Auth_tfa_setup_mail(std::string &&token) override;
 | 
			
		||||
    mrpc::Response<std::string> Auth_tfa_setup_totp(std::string &&token) override;
 | 
			
		||||
    std::optional<std::string> Auth_tfa_complete(std::string &&token, std::string &&otp) override;
 | 
			
		||||
    std::optional<std::string> Auth_tfa_disable(std::string &&token) override;
 | 
			
		||||
    std::optional<std::string> Auth_delete_user(std::string &&token) override;
 | 
			
		||||
    mrpc::Response<mrpc::Session> Auth_session_info(std::string &&token) override;
 | 
			
		||||
 | 
			
		||||
    mrpc::Response<std::vector<mrpc::UserInfo>> Admin_list_users(std::string &&token) override;
 | 
			
		||||
    std::optional<std::string> Admin_delete_user(std::string &&token, std::uint64_t &&user) override;
 | 
			
		||||
    std::optional<std::string> Admin_logout(std::string &&token, std::uint64_t &&user) override;
 | 
			
		||||
    std::optional<std::string> Admin_disable_tfa(std::string &&token, std::uint64_t &&user) override;
 | 
			
		||||
    std::optional<std::string> Admin_set_admin(std::string &&token, std::uint64_t &&user, bool &&admin) override;
 | 
			
		||||
    std::optional<std::string> Admin_set_enabled(std::string &&token, std::uint64_t &&user, bool &&enabled) override;
 | 
			
		||||
    std::optional<std::string> Admin_sudo(std::string &&token, std::uint64_t &&user) override;
 | 
			
		||||
    std::optional<std::string> Admin_unsudo(std::string &&token) override;
 | 
			
		||||
    std::optional<std::string> Admin_shutdown(std::string &&token) override;
 | 
			
		||||
 | 
			
		||||
    mrpc::Response<mrpc::Node> FS_get_node(std::string &&token, std::uint64_t &&node) override;
 | 
			
		||||
    mrpc::Response<std::vector<mrpc::PathSegment>> FS_get_path(std::string &&token, std::uint64_t &&node) override;
 | 
			
		||||
    mrpc::Response<std::uint64_t> FS_get_nodes_size(std::string &&token, std::vector<std::uint64_t> &&nodes) override;
 | 
			
		||||
    mrpc::Response<mrpc::CreateNodeInfo> FS_create_node(std::string &&token, bool &&file, std::uint64_t &&parent, std::string &&name) override;
 | 
			
		||||
    std::optional<std::string> FS_move_nodes(std::string &&token, std::vector<std::uint64_t> &&nodes, std::uint64_t &&parent) override;
 | 
			
		||||
    void FS_delete_nodes(std::string &&token, std::vector<std::uint64_t> &&nodes, mrpc::MRPCStream<std::string> &&stream) override;
 | 
			
		||||
    mrpc::Response<std::string> FS_download_preview(std::string &&token, std::uint64_t &&node) override;
 | 
			
		||||
    mrpc::Response<std::string> FS_get_mime(std::string &&token, std::uint64_t &&node) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#endif //FILESERVER_SERVER_HXX
 | 
			
		||||
							
								
								
									
										70
									
								
								src/server/server_internal.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/server/server_internal.hxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
#ifndef FILESERVER_SERVER_INTERNAL_HXX
 | 
			
		||||
#define FILESERVER_SERVER_INTERNAL_HXX
 | 
			
		||||
 | 
			
		||||
#include <botan_all.h>
 | 
			
		||||
#include "server.hxx"
 | 
			
		||||
 | 
			
		||||
// TODO log user action with __FUNC__
 | 
			
		||||
#define check_user() auto user = is_token_valid(token); if (!user || !user->enabled)
 | 
			
		||||
#define check_user_response() check_user() return { .e = "Unauthorized" }
 | 
			
		||||
#define check_user_optional() check_user() return "Unauthorized"
 | 
			
		||||
 | 
			
		||||
#if defined(BOTAN_HAS_SYSTEM_RNG)
 | 
			
		||||
    static std::unique_ptr<Botan::RNG> auth_rng = std::make_unique<Botan::System_RNG>();
 | 
			
		||||
#else
 | 
			
		||||
    static std::unique_ptr<Botan::RNG> auth_rng = std::make_unique<Botan::AutoSeeded_RNG>();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#common_image_file_types
 | 
			
		||||
static const std::unordered_map<std::string, std::string> mime_type_map = {
 | 
			
		||||
        {".apng" , "image/apng"},
 | 
			
		||||
        {".avif" , "image/avif"},
 | 
			
		||||
        {".bmp"  , "image/bmp"},
 | 
			
		||||
        {".gif"  , "image/gif"},
 | 
			
		||||
        {".jpg"  , "image/jpeg"},
 | 
			
		||||
        {".jpeg" , "image/jpeg"},
 | 
			
		||||
        {".jfif" , "image/jpeg"},
 | 
			
		||||
        {".pjpeg", "image/jpeg"},
 | 
			
		||||
        {".pjp"  , "image/jpeg"},
 | 
			
		||||
        {".png"  , "image/png"},
 | 
			
		||||
        {".svg"  , "image/svg"},
 | 
			
		||||
        {".webp" , "image/webp"},
 | 
			
		||||
 | 
			
		||||
        {".aac"  , "audio/aac"},
 | 
			
		||||
        {".flac" , "audio/flac"},
 | 
			
		||||
        {".mp3"  , "audio/mp3"},
 | 
			
		||||
        {".m4a"  , "audio/mp4"},
 | 
			
		||||
        {".oga"  , "audio/ogg"},
 | 
			
		||||
        {".ogg"  , "audio/ogg"},
 | 
			
		||||
        {".wav"  , "audio/wav"},
 | 
			
		||||
 | 
			
		||||
        {".3gp"  , "video/3gpp"},
 | 
			
		||||
        {".mpg"  , "video/mpeg"},
 | 
			
		||||
        {".mpeg" , "video/mpeg"},
 | 
			
		||||
        {".mp4"  , "video/mp4"},
 | 
			
		||||
        {".m4v"  , "video/mp4"},
 | 
			
		||||
        {".m4p"  , "video/mp4"},
 | 
			
		||||
        {".ogv"  , "video/ogg"},
 | 
			
		||||
        {".mov"  , "video/quicktime"},
 | 
			
		||||
        {".webm" , "video/webm"},
 | 
			
		||||
        {".mkv"  , "video/x-matroska"},
 | 
			
		||||
        {".mk3d" , "video/x-matroska"},
 | 
			
		||||
        {".mks"  , "video/x-matroska"},
 | 
			
		||||
 | 
			
		||||
        {".pdf"  , "application/pdf"}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
static const std::string& get_mime_type(const std::filesystem::path &filename) {
 | 
			
		||||
    static const std::string octet = "application/octet-stream";
 | 
			
		||||
 | 
			
		||||
    const auto &entry = mime_type_map.find(filename.extension());
 | 
			
		||||
    if (entry != mime_type_map.end())
 | 
			
		||||
        return entry->second;
 | 
			
		||||
    else
 | 
			
		||||
        return octet;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::shared_ptr<Node> get_node(const std::shared_ptr<User>& user, std::uint64_t id);
 | 
			
		||||
 | 
			
		||||
#endif //FILESERVER_SERVER_INTERNAL_HXX
 | 
			
		||||
							
								
								
									
										113
									
								
								src/server/upload.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/server/upload.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
#include <fstream>
 | 
			
		||||
#include <corvusoft/restbed/session.hpp>
 | 
			
		||||
#include <corvusoft/restbed/request.hpp>
 | 
			
		||||
#include <corvusoft/restbed/response.hpp>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
#include <stb_image.h>
 | 
			
		||||
#include <stb_image_resize2.h>
 | 
			
		||||
#include <stb_image_write.h>
 | 
			
		||||
#include "server_internal.hxx"
 | 
			
		||||
 | 
			
		||||
static constexpr std::size_t chunk_size = 1024*1024, max_image_size = 1024*1024*50, preview_size=480;
 | 
			
		||||
static const std::set<std::string> image_extension = {".png", ".jpg", ".jpeg", ".tga", ".bmp", ".psd", ".gif", ".jfif", ".pjpeg", ".pjp"};
 | 
			
		||||
 | 
			
		||||
struct UploadInfo {
 | 
			
		||||
    Server *server;
 | 
			
		||||
    std::shared_lock<std::shared_mutex> node_lock;
 | 
			
		||||
    std::size_t to_read;
 | 
			
		||||
    std::filesystem::path path;
 | 
			
		||||
    std::ofstream file;
 | 
			
		||||
    std::shared_ptr<Node> node;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void make_preview(const std::shared_ptr<UploadInfo>& info) {
 | 
			
		||||
    int x, y, channels;
 | 
			
		||||
    auto img = std::unique_ptr<stbi_uc, decltype(&free)>
 | 
			
		||||
        {stbi_load(info->path.c_str(), &x, &y, &channels, 0), &free};
 | 
			
		||||
    if (!img)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    float x_ration = (float)preview_size / (float)x, y_ration = (float)preview_size / (float)y;
 | 
			
		||||
    float ratio = std::min(x_ration, y_ration);
 | 
			
		||||
    int new_x = (int)((float)(x)*ratio), new_y = (int)((float)(y)*ratio);
 | 
			
		||||
    stbir_pixel_layout layout;
 | 
			
		||||
    switch (channels) {
 | 
			
		||||
        case 1: layout = STBIR_1CHANNEL; break;
 | 
			
		||||
        case 2: layout = STBIR_2CHANNEL; break;
 | 
			
		||||
        case 3: layout = STBIR_RGB; break;
 | 
			
		||||
        case 4: layout = STBIR_RGBA; break;
 | 
			
		||||
        default: return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto rimg = std::unique_ptr<unsigned char, decltype(&free)>
 | 
			
		||||
        {stbir_resize_uint8_linear(img.get(), x, y, 0, nullptr, new_x, new_y, 0, layout), &free};
 | 
			
		||||
    if (!rimg)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    auto png_path = info->path.replace_extension("png");
 | 
			
		||||
    if (!stbi_write_png(png_path.c_str(), new_x, new_y, channels, rimg.get(), 0))
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    info->node->preview = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void fetch_handler(const std::shared_ptr<restbed::Session> &s, const restbed::Bytes &bytes) {
 | 
			
		||||
    std::shared_ptr<UploadInfo> info = s->get("upload");
 | 
			
		||||
 | 
			
		||||
    std::size_t read = bytes.size();
 | 
			
		||||
    info->to_read -= std::min(read, info->to_read);
 | 
			
		||||
    info->file.write((char*)bytes.data(), bytes.size());
 | 
			
		||||
    if (info->to_read > 0)
 | 
			
		||||
        return s->fetch(std::min(info->to_read, chunk_size), fetch_handler);
 | 
			
		||||
    info->file.close();
 | 
			
		||||
    s->close(200);
 | 
			
		||||
 | 
			
		||||
    std::size_t real_size = std::filesystem::file_size(info->path);
 | 
			
		||||
    info->node->size = real_size;
 | 
			
		||||
    auto ext = std::filesystem::path{info->node->name}.extension().string();
 | 
			
		||||
    if (real_size < max_image_size && image_extension.contains(ext))
 | 
			
		||||
        make_preview(info);
 | 
			
		||||
 | 
			
		||||
    info->node_lock.unlock();
 | 
			
		||||
    info->server->save();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Server::upload(const std::shared_ptr<restbed::Session> &s) {
 | 
			
		||||
    const auto req = s->get_request();
 | 
			
		||||
    if (!req->has_header("X-Node"))
 | 
			
		||||
        return s->close(400, "Missing node");
 | 
			
		||||
    if (!req->has_header("X-Token"))
 | 
			
		||||
        return s->close(400, "Missing token");
 | 
			
		||||
 | 
			
		||||
    if (req->get_header("Transfer-Encoding") == "chunked") {
 | 
			
		||||
        spdlog::error("Encountered a chunked upload!");
 | 
			
		||||
        return s->close(500, "Sorry but your browser is not supported yet");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::uint64_t node_id = req->get_header("X-Node", 0);
 | 
			
		||||
    std::string token = req->get_header("X-Token");
 | 
			
		||||
 | 
			
		||||
    check_user() return s->close(400, "Invalid user");
 | 
			
		||||
    {
 | 
			
		||||
        std::shared_lock lock{user->node_lock};
 | 
			
		||||
        auto node = get_node(user, node_id);
 | 
			
		||||
        if (!node) return s->close(400, "Invalid node");
 | 
			
		||||
        if (!node->file) return s->close(400, "Can't upload to a directory");
 | 
			
		||||
        std::size_t to_read = req->get_header("Content-Length", 0);
 | 
			
		||||
        auto path = user->user_dir / std::to_string(node->id);
 | 
			
		||||
        if (node->preview) {
 | 
			
		||||
            node->preview = false;
 | 
			
		||||
            std::filesystem::remove(path.replace_extension("png"));
 | 
			
		||||
        }
 | 
			
		||||
        std::shared_ptr<UploadInfo> info{new UploadInfo{
 | 
			
		||||
            .server = this,
 | 
			
		||||
            .node_lock = std::shared_lock{user->node_lock},
 | 
			
		||||
            .to_read = to_read,
 | 
			
		||||
            .path = path,
 | 
			
		||||
            .file = std::ofstream{path, std::ios_base::out|std::ios_base::trunc|std::ios_base::binary},
 | 
			
		||||
            .node = node
 | 
			
		||||
        }};
 | 
			
		||||
        s->set("upload", info);
 | 
			
		||||
        s->fetch(std::min(to_read, chunk_size), fetch_handler);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/util/crash.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/util/crash.hxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
#ifndef FILESERVER_CRASH_HXX
 | 
			
		||||
#define FILESERVER_CRASH_HXX
 | 
			
		||||
 | 
			
		||||
#include <source_location>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
 | 
			
		||||
// TODO implement backtrace
 | 
			
		||||
[[noreturn]]
 | 
			
		||||
static void crash(std::source_location loc = std::source_location::current()) {
 | 
			
		||||
    spdlog::critical("crash called from: {}:{} `{}`", loc.file_name(), loc.line(), loc.function_name());
 | 
			
		||||
    spdlog::shutdown();
 | 
			
		||||
    std::abort();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif //FILESERVER_CRASH_HXX
 | 
			
		||||
							
								
								
									
										60
									
								
								src/util/logging.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/util/logging.hxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
#ifndef FILESERVER_LOGGING_HXX
 | 
			
		||||
#define FILESERVER_LOGGING_HXX
 | 
			
		||||
 | 
			
		||||
#include <cstdarg>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
#include <corvusoft/restbed/logger.hpp>
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace logging {
 | 
			
		||||
    struct RestbedLogger : public restbed::Logger {
 | 
			
		||||
        void stop() override {}
 | 
			
		||||
        void start(const std::shared_ptr<const restbed::Settings>&) override {
 | 
			
		||||
            logger = spdlog::default_logger()->clone("restbed");
 | 
			
		||||
        }
 | 
			
		||||
        void log(Level level, const char *format, ...) override {
 | 
			
		||||
            std::va_list args;
 | 
			
		||||
            va_start(args, format);
 | 
			
		||||
            restbed_log(level, format, args);
 | 
			
		||||
            va_end(args);
 | 
			
		||||
        }
 | 
			
		||||
        void log_if(bool expression, Level level, const char *format, ...) override {
 | 
			
		||||
            if (expression) {
 | 
			
		||||
                va_list args;
 | 
			
		||||
                va_start(args, format);
 | 
			
		||||
                restbed_log(level, format, args);
 | 
			
		||||
                va_end(args);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private:
 | 
			
		||||
        std::shared_ptr<spdlog::logger> logger;
 | 
			
		||||
        void restbed_log(const restbed::Logger::Level restbed_level, const char* format, va_list args) {
 | 
			
		||||
            spdlog::level::level_enum level;
 | 
			
		||||
            switch (restbed_level) {
 | 
			
		||||
                case restbed::Logger::DEBUG:   level = spdlog::level::level_enum::debug;    break;
 | 
			
		||||
                case restbed::Logger::INFO:    level = spdlog::level::level_enum::info;     break;
 | 
			
		||||
                case restbed::Logger::WARNING: level = spdlog::level::level_enum::warn;     break;
 | 
			
		||||
                case restbed::Logger::ERROR:   level = spdlog::level::level_enum::err;      break;
 | 
			
		||||
                case restbed::Logger::SECURITY:
 | 
			
		||||
                case restbed::Logger::FATAL:   level = spdlog::level::level_enum::critical; break;
 | 
			
		||||
            }
 | 
			
		||||
            std::string buf;
 | 
			
		||||
            buf.resize(1024);
 | 
			
		||||
            int written = vsnprintf(buf.data(), 1024, format, args);
 | 
			
		||||
            //if (std::string_view{buf.cbegin(), buf.cbegin()+10} == "Incoming '")
 | 
			
		||||
            //    return;
 | 
			
		||||
            if (written >= 1024) {
 | 
			
		||||
                buf.resize(written + 10);
 | 
			
		||||
                written = vsnprintf(buf.data(), written + 10, format, args);
 | 
			
		||||
            }
 | 
			
		||||
            buf.resize(written);
 | 
			
		||||
            logger->log(level, buf);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif //FILESERVER_LOGGING_HXX
 | 
			
		||||
							
								
								
									
										7
									
								
								src/util/stb.cxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/util/stb.cxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
#define STB_IMAGE_IMPLEMENTATION
 | 
			
		||||
#define STB_IMAGE_RESIZE_IMPLEMENTATION
 | 
			
		||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
 | 
			
		||||
 | 
			
		||||
#include <stb_image.h>
 | 
			
		||||
#include <stb_image_resize2.h>
 | 
			
		||||
#include <stb_image_write.h>
 | 
			
		||||
							
								
								
									
										30
									
								
								src/util/timed_mutex.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/util/timed_mutex.hxx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
#ifndef FILESERVER_TIMED_MUTEX_HXX
 | 
			
		||||
#define FILESERVER_TIMED_MUTEX_HXX
 | 
			
		||||
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
#include <shared_mutex>
 | 
			
		||||
 | 
			
		||||
struct TimedSharedMutex {
 | 
			
		||||
    std::shared_mutex m;
 | 
			
		||||
 | 
			
		||||
    using clock = std::chrono::high_resolution_clock;
 | 
			
		||||
 | 
			
		||||
    void lock_shared() {
 | 
			
		||||
        auto start = clock::now();
 | 
			
		||||
        m.lock_shared();
 | 
			
		||||
        auto end = clock::now();
 | 
			
		||||
        auto d = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
 | 
			
		||||
        spdlog::info("Lock s took {} ms", d.count());
 | 
			
		||||
    }
 | 
			
		||||
    void unlock_shared() { m.unlock_shared(); spdlog::info("Unlock s"); }
 | 
			
		||||
    void lock() {
 | 
			
		||||
        auto start = clock::now();
 | 
			
		||||
        m.lock();
 | 
			
		||||
        auto end = clock::now();
 | 
			
		||||
        auto d = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
 | 
			
		||||
        spdlog::info("Lock took {} ms", d.count());
 | 
			
		||||
    }
 | 
			
		||||
    void unlock() { m.unlock(); spdlog::info("Unlock"); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#endif //FILESERVER_TIMED_MUTEX_HXX
 | 
			
		||||
		Reference in New Issue
	
	Block a user