Package: release.debian.org Severity: normal User: release.debian....@packages.debian.org Usertags: unblock X-Debbugs-Cc: megato...@packages.debian.org Control: affects -1 + src:megatools
Please unblock package megatools [ Reason ] megatools is a command-line client for the Mega cloud storage service. Mega has recently started using Hashcash to (presumably) prevent DoS attacks, so HTTP requests from clients that don't include a valid X-Hashcash header can be rejected with an HTTP 402 code. With megatools 1.15.3-1 (the current version in trixie) the result looks like this: $ megals -l ERROR: Can't login to mega.nz: API call 'us' failed: HTTP POST failed: Server returned 402 After updating megatools to 1.15.5-1 the request works as expected. Mega seems to allow connections from the same IP for some time after a valid request has been made, so if the user doesn't have the latest megatools installed one possible workaround is to log in first using a web browser before using the command-line client. [ Impact ] If the package is not updated users won't be able to use megatools normally unless they log in with the browser first as explained in the previous paragraph. This is cumbersome in general, and very hard or impossible in scenarios where megatools is used remotely over ssh or similar. [ Tests ] I tested manually that Mega returns HTTP 402 for every request until I updated the package to the version in sid. [ Risks ] This package comes with two changes (see the attached diff): - Add the X-Hashcash header if the first request fails with HTTP 402. I believe that the risk here is very low since this works exactly as before and the new code path is only used if access is denied. - Identify as Firefox in the User-Agent string (and other headers), in order to minimize the risk of the original user agent being blocked. I think that the risk is also very low, and in any case it would not make things worse than they are now. https://salsa.debian.org/berto/megatools/-/blob/debian/1.11.5-1/lib/http.c?ref_type=tags#L176 [ Checklist ] [x] all changes are documented in the d/changelog [x] I reviewed all changes and I approve them [x] attach debdiff against the package in testing [ Other info ] I'm requesting this earlier because as of today the package in sid is "too young, only 10 of 20 days old", but I wanted to do it before the deadline for unblock requests on the 30th of July. unblock megatools/1.11.5-1
diff --git a/NEWS b/NEWS index 6718be6..4635920 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,12 @@ +megatools 1.11.5 - 2025-07-06 +============================= + +This release implements mega.nz X-Hashcash to handle 402 status responses +as suggested by a user. + +This should handle 402 errors returned by mega.nz. + + megatools 1.11.3 - 2025-02-03 ============================= diff --git a/debian/changelog b/debian/changelog index f9a80f9..5c7e4c7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +megatools (1.11.5-1) unstable; urgency=medium + + * New upstream release + + -- Alberto Garcia <be...@igalia.com> Fri, 11 Jul 2025 15:09:54 +0200 + +megatools (1.11.4-1) unstable; urgency=medium + + * New upstream release. + * debian/control: + - Update Standards-Version to 4.7.2 (no changes). + + -- Alberto Garcia <be...@igalia.com> Wed, 07 May 2025 16:26:31 +0200 + megatools (1.11.3-1) unstable; urgency=medium * New upstream release. diff --git a/debian/control b/debian/control index 21dd60b..3de48a7 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Build-Depends: debhelper-compat (= 13), libssl-dev, libglib2.0-dev, meson -Standards-Version: 4.7.0 +Standards-Version: 4.7.2 Rules-Requires-Root: no Homepage: https://xff.cz/megatools/ Vcs-Browser: https://salsa.debian.org/berto/megatools diff --git a/lib/http.c b/lib/http.c index e31ff4e..de6efb0 100644 --- a/lib/http.c +++ b/lib/http.c @@ -20,8 +20,11 @@ #include "http.h" #include "mega.h" #include "config.h" +#include "alloc.h" + #include <curl/curl.h> #include <string.h> +#include <openssl/evp.h> char* http_netif = NULL; int http_ipproto = HTTP_IPPROTO_ANY; @@ -34,7 +37,7 @@ int http_ipproto = HTTP_IPPROTO_ANY; #define CURL_AT_LEAST_VERSION(x, y, z) (LIBCURL_VERSION_NUM >= CURL_VERSION_BITS(x, y, z)) #endif -//#define STEALTH_MODE +#define STEALTH_MODE 1 #if CURL_AT_LEAST_VERSION(7, 12, 0) static CURLSH *http_share; @@ -172,7 +175,7 @@ struct http *http_new(void) #if STEALTH_MODE // we are Firefox! - http_set_header(h, "User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0"); + http_set_header(h, "User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:137.0) Gecko/20100101 Firefox/137.0"); http_set_header(h, "Referer", "https://mega.nz/"); http_set_header(h, "Origin", "https://mega.nz"); http_set_header(h, "Accept", "*/*"); @@ -323,6 +326,97 @@ static gboolean to_error(struct http* h, CURLcode res, GError** err) return TRUE; } +gchar *base64urlencode(const guchar *data, gsize len); +guchar *base64urldecode(const gchar *str, gsize *len); + +static uint32_t gencash(const char token[48], uint32_t easiness) +{ + uint8_t *buf; + const size_t buf_size = 4 + 262144 * 48; + uint32_t ret = 0xffffffffu; + uint32_t threshold = ((easiness & 63) << 1) + 1 << (easiness >> 6) * 7 + 3; + + buf = g_malloc(buf_size); + for (int i = 0; i < 262144; i++) + memcpy(buf + 4 + i * 48, token, 48); + memset(buf, 0, 4); + uint32_t *prefix = (void*)buf; + + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + while (1) { + *prefix += 1; + + if (!EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) + goto out; + + if (!EVP_DigestUpdate(ctx, buf, buf_size)) + goto out; + + unsigned char hash[EVP_MAX_MD_SIZE]; + unsigned int hash_size = 0; + + if (!EVP_DigestFinal_ex(ctx, hash, &hash_size)) + goto out; + + // BE 32 bit number at start of hash must be <= threshold + if (GUINT32_FROM_BE(*(uint32_t*)hash) <= threshold) { + ret = *prefix; + goto out; + } + } + +out: + EVP_MD_CTX_free(ctx); + g_free(buf); + return ret; +} + +static gboolean http_handle_402(struct http *h) +{ + glong http_status = 0; + + if (curl_easy_getinfo(h->curl, CURLINFO_RESPONSE_CODE, &http_status) != CURLE_OK) + return FALSE; + if (http_status != 402) + return FALSE; + + struct curl_header *hdr = NULL; + CURLHcode hh = curl_easy_header(h->curl, "X-Hashcash", 0, CURLH_HEADER, -1, &hdr); + if (hh != CURLHE_OK) + return FALSE; + + gc_strfreev gchar** parts = g_strsplit(hdr->value, ":", -1); + if (g_strv_length(parts) != 4) + return FALSE; + + if (strcmp(parts[0], "1")) + return FALSE; + + gchar* np = NULL; + gint64 v = g_ascii_strtoll(parts[1], &np, 10); + if (v == 0 && np == parts[1]) + return FALSE; + if (v < 0 || v > 255) + return FALSE; + + gsize token_size; + gc_free guchar* token = base64urldecode(parts[3], &token_size); + if (token_size != 48) + return FALSE; + + uint32_t ret = gencash(token, v); + if (ret == 0xffffffffu) + return FALSE; + + gc_free gchar* str = base64urlencode((gchar*)&ret, 4); + + gc_free gchar* res = g_strdup_printf("1:%s:%s", parts[3], str); + http_set_header(h, "X-Hashcash", res); + + return TRUE; +} + GString *http_post(struct http *h, const gchar *url, const gchar *body, gssize body_len, GError **err) { struct curl_slist *headers = NULL; @@ -355,11 +449,26 @@ GString *http_post(struct http *h, const gchar *url, const gchar *body, gssize b curl_easy_setopt(h->curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)append_gstring); curl_easy_setopt(h->curl, CURLOPT_WRITEDATA, response); - // perform HTTP request - res = curl_easy_perform(h->curl); - if (to_error(h, res, err)) { - g_string_free(response, TRUE); - response = NULL; + int tries = 0; + while (tries++ < 4) { + // perform HTTP request + res = curl_easy_perform(h->curl); + + if (res == CURLE_OK && http_handle_402(h)) { + // re-create request headers list + curl_slist_free_all(headers); + headers = NULL; + g_hash_table_foreach(h->headers, (GHFunc)add_header, &headers); + curl_easy_setopt(h->curl, CURLOPT_HTTPHEADER, headers); + continue; + } + + if (to_error(h, res, err)) { + g_string_free(response, TRUE); + response = NULL; + } + + break; } curl_easy_setopt(h->curl, CURLOPT_HTTPHEADER, NULL); diff --git a/lib/mega.c b/lib/mega.c index 619ce38..8721990 100644 --- a/lib/mega.c +++ b/lib/mega.c @@ -279,7 +279,7 @@ static void s_json_gen_member_rsa_key(SJsonGen *gen, const gchar *name, struct r // }}} // {{{ base64urlencode -static gchar *base64urlencode(const guchar *data, gsize len) +gchar *base64urlencode(const guchar *data, gsize len) { gint i, shl; gchar *she, *p; @@ -311,7 +311,7 @@ static gchar *base64urlencode(const guchar *data, gsize len) // }}} // {{{ base64urldecode -static guchar *base64urldecode(const gchar *str, gsize *len) +guchar *base64urldecode(const gchar *str, gsize *len) { gint i; diff --git a/meson.build b/meson.build index 69db3f9..290ba00 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('megatools', 'c', - version: '1.11.3', + version: '1.11.5', default_options: [ 'c_std=gnu99', ],