diff --git a/.gitmodules b/.gitmodules index 5b8b83f..e7c62b0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "third_party/tomlplusplus"] path = third_party/tomlplusplus url = https://github.com/marzer/tomlplusplus.git +[submodule "third_party/nlohmann_json"] + path = third_party/nlohmann_json + url = https://github.com/nlohmann/json.git \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 6529639..4aad746 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,11 +28,13 @@ find_package(MAVSDK REQUIRED) include_directories(third_party/cpp-httplib/) include_directories(third_party/tomlplusplus/) +include_directories(third_party/nlohmann_json/single_include) include_directories(${SQLite3_INCLUDE_DIRS}) add_executable(${PROJECT_NAME} src/main.cpp src/ServerInterface.cpp + src/MealaServerInterface.cpp src/LogLoader.cpp) target_link_libraries(${PROJECT_NAME} diff --git a/config.toml b/config.toml index ff208c2..2e4cdee 100644 --- a/config.toml +++ b/config.toml @@ -1,6 +1,8 @@ connection_url = "udp://:14551" local_server = "http://127.0.0.1:5006" +upload_service = 0 remote_server = "https://review.px4.io" +credentials_file = "" email = "" upload_enabled = false public_logs = false diff --git a/src/LogLoader.cpp b/src/LogLoader.cpp index 357b0fe..b062dcc 100644 --- a/src/LogLoader.cpp +++ b/src/LogLoader.cpp @@ -5,6 +5,7 @@ #include #include #include +#include "ServerInterface.hpp" namespace fs = std::filesystem; @@ -27,6 +28,9 @@ LogLoader::LogLoader(const LogLoader::Settings& settings) .db_path = _settings.application_directory + "local_server.db", .upload_enabled = true, // Always upload to local server .public_logs = true, // Public required true for searching using Web UI + .upload_service = UploadService::FlightReview, // Local server is always FlightReview, + .credentials_file = "" // FlightReview does not need credentials + }; // Setup remote server interface @@ -37,10 +41,21 @@ LogLoader::LogLoader(const LogLoader::Settings& settings) .db_path = _settings.application_directory + "remote_server.db", .upload_enabled = settings.upload_enabled, .public_logs = settings.public_logs, + .upload_service = settings.upload_service, + .credentials_file = settings.credentials_file }; _local_server = std::make_shared(local_server_settings); - _remote_server = std::make_shared(remote_server_settings); + + // _remote_server = std::make_shared(remote_server_settings); + if (_settings.upload_service == UploadService::Meala) { + MealaCredentials creds; + creds.credentials_file = _settings.credentials_file; + _remote_server = std::make_shared(remote_server_settings, creds); + + } else { + _remote_server = std::make_shared(remote_server_settings); + } std::cout << std::fixed << std::setprecision(8); diff --git a/src/LogLoader.hpp b/src/LogLoader.hpp index 0f2df34..08f9ff3 100644 --- a/src/LogLoader.hpp +++ b/src/LogLoader.hpp @@ -11,6 +11,7 @@ class LogLoader { public: + struct Settings { std::string email; std::string local_server; @@ -19,6 +20,8 @@ class LogLoader std::string application_directory; bool upload_enabled; bool public_logs; + UploadService upload_service; + std::string credentials_file; }; LogLoader(const Settings& settings); diff --git a/src/MealaServerInterface.cpp b/src/MealaServerInterface.cpp new file mode 100644 index 0000000..572f416 --- /dev/null +++ b/src/MealaServerInterface.cpp @@ -0,0 +1,138 @@ +#include "ServerInterface.hpp" +#include +#include +#include +#include +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include +#include +#include "Log.hpp" + +namespace fs = std::filesystem; +using json = nlohmann::json; + +MealaServerInterface::MealaServerInterface(const Settings& settings, const MealaCredentials& creds) + : ServerInterface(settings), _creds(creds) +{} + +bool MealaServerInterface::login() +{ + std::string username, password, token; + + if (!_creds.credentials_file.empty() && fs::exists(_creds.credentials_file)) { + std::ifstream f(_creds.credentials_file); + json creds_json; + f >> creds_json; + username = creds_json.value("username", ""); + token = creds_json.value("token", ""); + + } else { + username = _creds.username; + password = _creds.password; + } + + httplib::Params params; + params.emplace("username", username); + + if (!token.empty()) { + params.emplace("token", token); + + } else { + params.emplace("password", password); + } + + std::string url_path = "/login"; + httplib::Result res; + + if (_protocol == Protocol::Https) { + httplib::SSLClient cli(_settings.server_url); + res = cli.Post(url_path.c_str(), params); + + } else { + httplib::Client cli(_settings.server_url); + res = cli.Post(url_path.c_str(), params); + } + + if (res && res->status == 200) { + auto it = res->headers.find("Set-Cookie"); + + if (it != res->headers.end()) { + _session_cookie = it->second; + _logged_in = true; + return true; + } + } + + _logged_in = false; + return false; +} + +ServerInterface::UploadResult MealaServerInterface::upload(const std::string& filepath) +{ + if (!_logged_in && !login()) { + return {false, 401, "Login to Meala failed"}; + } + + constexpr size_t chunk_size = 5 * 1024 * 1024; // 5 MB + + if (!fs::exists(filepath)) { + return {false, 404, "Log file does not exist: " + filepath}; + } + + size_t file_size = fs::file_size(filepath); + size_t total_chunks = (file_size / chunk_size) + (file_size % chunk_size ? 1 : 0); + std::string filename = fs::path(filepath).filename().string(); + + std::ifstream file(filepath, std::ios::binary); + + if (!file) { + return {false, 0, "Failed to open file"}; + } + + for (size_t chunk_index = 0; chunk_index < total_chunks; ++chunk_index) { + size_t offset = chunk_index * chunk_size; + size_t this_chunk_size = std::min(chunk_size, file_size - offset); + std::vector buffer(this_chunk_size); + file.read(buffer.data(), this_chunk_size); + + httplib::MultipartFormDataItems items = { + {"comments", "Log uploaded from C++ API", "", ""}, + {"battery", "", "", ""}, + {"pic", "", "", ""}, + {"gso", "", "", ""}, + {"vehicle_id", "", "", ""}, + {"dzchunkbyteoffset", std::to_string(offset), "", ""}, + {"dzchunkindex", std::to_string(chunk_index), "", ""}, + {"dztotalchunkcount", std::to_string(total_chunks), "", ""}, + {"files", std::string(buffer.begin(), buffer.end()), filename, "application/octet-stream"} + }; + + std::string url_path = "/upload/api"; + httplib::Headers headers; + + if (!_session_cookie.empty()) { + headers.emplace("Cookie", _session_cookie); + } + + LOG("Uploading " << filename << " to " << _settings.server_url); + + httplib::Result res; + + if (_protocol == Protocol::Https) { + httplib::SSLClient cli(_settings.server_url); + res = cli.Post(url_path.c_str(), headers, items); + + } else { + httplib::Client cli(_settings.server_url); + res = cli.Post(url_path.c_str(), headers, items); + } + + if (!res || res->status != 200) { + LOG("Upload failed on chunk " << (chunk_index + 1) << ": " + << (res ? std::to_string(res->status) : "No response")); + return {false, res ? res->status : 0, "No response from server"}; + } + } + + return {true, 200, "Upload completed successfully."}; +} \ No newline at end of file diff --git a/src/ServerInterface.cpp b/src/ServerInterface.cpp index 554893b..493ae21 100644 --- a/src/ServerInterface.cpp +++ b/src/ServerInterface.cpp @@ -11,6 +11,7 @@ #include #define CPPHTTPLIB_OPENSSL_SUPPORT #include +#include namespace fs = std::filesystem; @@ -597,4 +598,4 @@ ServerInterface::DatabaseEntry ServerInterface::row_to_db_entry(sqlite3_stmt* st entry.downloaded = sqlite3_column_int(stmt, 4) != 0; return entry; -} +} \ No newline at end of file diff --git a/src/ServerInterface.hpp b/src/ServerInterface.hpp index ea47559..c9b3ba2 100644 --- a/src/ServerInterface.hpp +++ b/src/ServerInterface.hpp @@ -4,6 +4,12 @@ #include #include #include +#include + +enum class UploadService { + FlightReview = 0, + Meala = 1 +}; class ServerInterface { @@ -15,6 +21,8 @@ class ServerInterface std::string db_path; // Path to this server's database bool upload_enabled {}; bool public_logs {}; + UploadService upload_service; + std::string credentials_file; // For Meala credentials }; struct UploadResult { @@ -59,14 +67,17 @@ class ServerInterface void start(); void stop(); -private: +protected: enum class Protocol { Http, Https }; + virtual UploadResult upload(const std::string& filepath); + Protocol _protocol {Protocol::Https}; + Settings _settings; +private: void sanitize_url_and_determine_protocol(); - UploadResult upload(const std::string& filepath); bool server_reachable(); // Database operations @@ -74,8 +85,27 @@ class ServerInterface bool add_to_blacklist(const std::string& uuid, const std::string& reason); DatabaseEntry row_to_db_entry(sqlite3_stmt* stmt); - Settings _settings; - Protocol _protocol {Protocol::Https}; bool _should_exit = false; sqlite3* _db = nullptr; }; + +struct MealaCredentials { + std::string username; + std::string password; + std::string token; + std::string credentials_file; +}; + +class MealaServerInterface : public ServerInterface +{ +public: + MealaServerInterface(const Settings& settings, const MealaCredentials& creds); + + bool login(); + UploadResult upload(const std::string& filepath) override; + +private: + MealaCredentials _creds; + std::string _session_cookie; + bool _logged_in = false; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6000d2b..774e4ef 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -37,7 +37,9 @@ int main() .mavsdk_connection_url = config["connection_url"].value_or("0.0.0"), .application_directory = std::string(getenv("HOME")) + "/.local/share/logloader/", .upload_enabled = config["upload_enabled"].value_or(false), - .public_logs = config["public_logs"].value_or(false) + .public_logs = config["public_logs"].value_or(false), + .upload_service = static_cast(config["upload_service"].value_or(UploadService::FlightReview)), // 0 for FlightReview, 1 for Meala + .credentials_file = config["credentials_file"].value_or(std::string(getenv("HOME")) + "/.local/share/logloader/meala_creds.cert") }; _log_loader = std::make_shared(settings); diff --git a/third_party/nlohmann_json b/third_party/nlohmann_json new file mode 160000 index 0000000..52d3f6e --- /dev/null +++ b/third_party/nlohmann_json @@ -0,0 +1 @@ +Subproject commit 52d3f6e034504b4097741b2da5b644a38ee273ff