diff --git a/README.md b/README.md index 8145739..ba38858 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,24 @@ Log timestamps come from the modification time in the listing when the vehicle s Downloads are staged in a temporary directory and only moved next to the finished logs once the transferred size matches the listing, so a partial file is never mistaken for a complete one. +`.BIN` files are not accepted by review.px4.io, so on ArduPilot leave `upload_enabled = false` or point `remote_server` somewhere that understands dataflash logs. + ### Upgrading from a pre-FTP logloader Older versions identified logs by the timestamp `LOG_ENTRY` reported, which MAVLink FTP cannot reproduce. On first start the existing `logs` table is renamed to `logs_legacy` and its rows are matched against the FTP listing by size, so logs already downloaded and uploaded are not fetched or uploaded a second time. Nothing is deleted; `logs_legacy` stays behind for inspection. +### Configuration +| Key | Default | Description | +| --- | --- | --- | +| `connection_url` | `udp://:14551` | MAVSDK connection string | +| `local_server` | `http://127.0.0.1:5006` | Local upload target | +| `remote_server` | `https://review.px4.io` | Remote upload target | +| `email` | `""` | Email attached to remote uploads | +| `remote_api_key` | `""` | Per-account API key for authenticated Flight Review (`Authorization: Bearer` + `X-API-Key`). Empty = upload without API key headers (open servers). Generate under /account | +| `upload_enabled` | `false` | Upload to the remote server | +| `public_logs` | `false` | Mark remote uploads public | +| `remote_log_directory` | `""` | Override the vehicle log directory | +| `ftp_use_burst` | `true` | Use FTP burst reads | + ### Build Install dependencies ``` diff --git a/config.toml b/config.toml index af7db70..0e34d76 100644 --- a/config.toml +++ b/config.toml @@ -5,6 +5,13 @@ email = "" upload_enabled = false public_logs = false +# Per-account API key for authenticated Flight Review (ARK). +# Generate at https://review.arkelectron.com/account (shown once; format fr_...). +# When set: sent as Authorization: Bearer and X-API-Key on POST /upload. +# When empty: remote uploads are still attempted without API key headers +# (works for open servers; ARK Flight Review will reject with 403). +remote_api_key = "" + # Vehicle log directory. Empty probes "@MAV_LOG" (the virtual log directory from # the MAVLink FTP specification), then "/fs/microsd/log" (PX4) and "/APM/LOGS" # (ArduPilot). Set this when the logs live somewhere else. diff --git a/src/LogLoader.cpp b/src/LogLoader.cpp index 70f8f6e..215c812 100644 --- a/src/LogLoader.cpp +++ b/src/LogLoader.cpp @@ -48,6 +48,7 @@ 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 + .api_key = "", // Local Flight Review is typically open on the companion }; // Setup remote server interface @@ -58,6 +59,7 @@ LogLoader::LogLoader(const LogLoader::Settings& settings) .db_path = _settings.application_directory + "remote_server.db", .upload_enabled = settings.upload_enabled, .public_logs = settings.public_logs, + .api_key = settings.remote_api_key, }; _local_server = std::make_shared(local_server_settings); @@ -405,8 +407,14 @@ void LogLoader::upload_pending_logs(std::shared_ptr server) if (result.success) { LOG("Log upload SUCCESS: " << result.message); - } else if (result.status_code == 400) { - LOG("Log upload failed (" << result.status_code << "): " << result.message); + } else if (result.status_code == 400 || result.status_code == 401 || result.status_code == 403) { + LOG("Log upload rejected (" << result.status_code << "): " << result.message); + + // Account/auth policy applies to every log on this server — stop the batch. + if (result.status_code == 401 || result.status_code == 403) { + LOG("Stopping remote uploads for this cycle (server requires login/approval)"); + return; + } } else if (result.status_code == 503) { // Server down (e.g. local flight-review not running). Already logged once diff --git a/src/LogLoader.hpp b/src/LogLoader.hpp index 67edbb3..b70b715 100644 --- a/src/LogLoader.hpp +++ b/src/LogLoader.hpp @@ -16,6 +16,8 @@ class LogLoader std::string email; std::string local_server; std::string remote_server; + // API key for remote_server (ARK Flight Review account key). Empty = none. + std::string remote_api_key; std::string mavsdk_connection_url; std::string application_directory; bool upload_enabled; diff --git a/src/ServerInterface.cpp b/src/ServerInterface.cpp index bd1e6c3..dede21e 100644 --- a/src/ServerInterface.cpp +++ b/src/ServerInterface.cpp @@ -300,8 +300,11 @@ ServerInterface::UploadResult ServerInterface::upload_log(const std::string& uui } } else if (result.status_code == 400) { - // Permanent failure - add to blacklist - add_to_blacklist(uuid, "HTTP 400: Bad Request"); + // The server will not take this log whoever asks - do not keep retrying it. + // 401/403 are not blacklisted: they say the account is not authorized yet, + // which is fixed by setting remote_api_key, and every pending log would + // otherwise be thrown away one per cycle while the key is missing. + add_to_blacklist(uuid, "HTTP 400: " + result.message); } return result; @@ -445,25 +448,64 @@ ServerInterface::UploadResult ServerInterface::upload(const std::string& filepat std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); items.push_back({"filearg", content, filepath, "application/octet-stream"}); - LOG("Uploading " << fs::path(filepath).filename().string() << " to " << _settings.server_url); + // API-key headers only when a non-empty key is configured. Never send + // Authorization/X-API-Key with an empty value. + httplib::Headers headers; + const bool use_api_key = !_settings.api_key.empty(); + + if (use_api_key) { + // ARK Flight Review (flight_review api_key.py): + // Authorization: Bearer + // X-API-Key: + // (Query ?api_key= also works server-side; form fields cannot — auth + // runs in prepare() before the body is available.) + headers.emplace("Authorization", "Bearer " + _settings.api_key); + headers.emplace("X-API-Key", _settings.api_key); + } + + LOG("Uploading " << fs::path(filepath).filename().string() << " to " << _settings.server_url + << (use_api_key ? " (with API key)" : "")); // Post multi-part form httplib::Result res; if (_protocol == Protocol::Https) { httplib::SSLClient cli(_settings.server_url); - res = cli.Post("/upload", items); + cli.set_connection_timeout(30, 0); + cli.set_read_timeout(120, 0); + res = use_api_key ? cli.Post("/upload", headers, items) + : cli.Post("/upload", items); } else { httplib::Client cli(_settings.server_url); - res = cli.Post("/upload", items); + cli.set_connection_timeout(30, 0); + cli.set_read_timeout(120, 0); + res = use_api_key ? cli.Post("/upload", headers, items) + : cli.Post("/upload", items); } if (res && res->status == 302) { return {true, 302, "Success: " + _settings.server_url + res->get_header_value("Location")}; - } else if (res && res->status == 400) { - return {false, 400, "Bad Request - Will not retry"}; + } else if (res && (res->status == 400 || res->status == 401 || res->status == 403)) { + // Permanent client/auth errors: do not spin on the same log forever. + // review.arkelectron.com returns 403 when the account is not logged in / + // not approved for automated uploads. + std::string detail = res->body; + + // Collapse HTML / whitespace for a one-line log message. + for (char& c : detail) { + if (c == '\n' || c == '\r' || c == '\t') { + c = ' '; + } + } + + if (detail.size() > 200) { + detail.resize(200); + detail += "..."; + } + + return {false, res->status, detail.empty() ? "Rejected by server (will not retry)" : detail}; } else { return {false, res ? res->status : 0, "Will retry later"}; @@ -483,18 +525,24 @@ bool ServerInterface::server_reachable() if (_protocol == Protocol::Https) { httplib::SSLClient cli(_settings.server_url); - cli.set_connection_timeout(2, 0); - cli.set_read_timeout(2, 0); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(5, 0); + // Flight Review often redirects "/" (302). We only need proof the host answers. + cli.set_follow_location(false); res = cli.Get("/"); } else { httplib::Client cli(_settings.server_url); - cli.set_connection_timeout(2, 0); - cli.set_read_timeout(2, 0); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(5, 0); + cli.set_follow_location(false); res = cli.Get("/"); } - const bool success = res && res->status == 200; + // Any HTTP response means the server is up. Flight Review's home page returns + // 302, so requiring status == 200 falsely marked review.arkelectron.com dead. + // Connection / TLS failures leave res empty. + const bool success = static_cast(res); if (!success) { _unreachable_until = now + kUnreachableCooldown; diff --git a/src/ServerInterface.hpp b/src/ServerInterface.hpp index 3d8e014..36c8e87 100644 --- a/src/ServerInterface.hpp +++ b/src/ServerInterface.hpp @@ -16,6 +16,12 @@ class ServerInterface std::string db_path; // Path to this server's database bool upload_enabled {}; bool public_logs {}; + // Per-account API key for authenticated Flight Review instances + // (e.g. review.arkelectron.com). Sent as: + // Authorization: Bearer + // X-API-Key: + // Matches ARK flight_review api_key.py. Leave empty for open servers. + std::string api_key; }; struct UploadResult { diff --git a/src/main.cpp b/src/main.cpp index 24f6af5..542aab4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,6 +57,7 @@ int main(int argc, char** argv) .email = config["email"].value_or(""), .local_server = config["local_server"].value_or("http://127.0.0.1:5006"), .remote_server = config["remote_server"].value_or("https://logs.px4.io"), + .remote_api_key = config["remote_api_key"].value_or(""), .mavsdk_connection_url = config["connection_url"].value_or("0.0.0"), .application_directory = config["application_directory"].value_or(data_dir.string() + "/"), .upload_enabled = config["upload_enabled"].value_or(false), @@ -65,6 +66,27 @@ int main(int argc, char** argv) .ftp_use_burst = config["ftp_use_burst"].value_or(true) }; + // Trim whitespace-only keys so they count as "not set" (no empty auth headers). + auto trim = [](std::string s) { + const auto start = s.find_first_not_of(" \t\r\n"); + + if (start == std::string::npos) { + return std::string{}; + } + + const auto end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); + }; + settings.remote_api_key = trim(std::move(settings.remote_api_key)); + + // Still attempt remote uploads without a key (open servers like logs.px4.io). + // Authenticated ARK Flight Review will 403 until remote_api_key is set; we + // never send empty Authorization / X-API-Key headers (see ServerInterface). + if (settings.upload_enabled && settings.remote_api_key.empty()) { + LOG("upload_enabled is true but remote_api_key is empty — remote uploads will " + "proceed without an API key (open servers only; ARK Flight Review needs a key)"); + } + _log_loader = std::make_shared(settings); bool connected = false;