commit fa5ffd38bccd26f42466080ad9d073a092fa80eb
parent e035546a0bc48131bffded1229f4f0bcf6413774
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 27 Nov 2025 00:24:35 +0100
basic paywall logic
Diffstat:
2 files changed, 298 insertions(+), 2 deletions(-)
diff --git a/src/backend/paivana-httpd.c b/src/backend/paivana-httpd.c
@@ -263,6 +263,11 @@ static CURLM *curl_multi;
static struct MHD_Daemon *mhd_daemon;
/**
+ * Static paywall response.
+ */
+static struct MHD_Response *paywall;
+
+/**
* The task ID
*/
static struct GNUNET_SCHEDULER_Task *httpd_task;
@@ -278,11 +283,147 @@ static struct MHD_Response *curl_failure_response;
static const struct GNUNET_CONFIGURATION_Handle *cfg;
/**
+ * Disable paywall check.
+ */
+static int no_check;
+
+/**
* Destination to which HTTP server we forward requests to.
* Of the format "http://servername:PORT"
*/
static char *target_server_base_url;
+/**
+ * Merchant backend base URL.
+ */
+static char *merchant_base_url;
+
+/**
+ * Merchant backend access token.
+ */
+static char *merchant_access_token;
+
+/**
+ * Secret for the cookie generation.
+ */
+static struct GNUNET_HashCode paivana_secret;
+
+
+/* ********************* Cookie handling ****************** */
+
+/**
+ * Compute access cookie hash for the given @a expiration and @a ca.
+ *
+ * @param expiration expiration time of the cookie
+ * @param ca_len number of bytes in @a ca
+ * @param ca client address
+ * @param[out] c set to the cookie hash
+ */
+static void
+compute_cookie_hash (struct GNUNET_TIME_Absolute expiration,
+ size_t ca_len,
+ const void *ca,
+ struct GNUNET_HashCode *c)
+{
+ struct GNUNET_TIME_AbsoluteNBO e;
+
+ e = GNUNET_TIME_absolute_hton (expiration);
+ GNUNET_assert (GNUNET_YES ==
+ GNUNET_CRYPTO_kdf (c,
+ sizeof (c),
+ &e,
+ sizeof (e),
+ &paivana_secret,
+ sizeof (paivana_secret),
+ ca,
+ ca_len,
+ NULL,
+ 0));
+}
+
+
+/**
+ * Check if the given cookie currently grants access.
+ *
+ * @param cookie the cookie
+ * @param ca_len number of bytes in @a ca
+ * @param ca client address
+ * @return true if the cookie is OK
+ */
+static bool
+check_cookie (const char *cookie,
+ size_t ca_len,
+ const void *ca)
+{
+ const char *dash;
+ unsigned long long u;
+ struct GNUNET_HashCode h;
+ struct GNUNET_HashCode c;
+ struct GNUNET_TIME_Absolute a;
+
+ dash = strchr (cookie,
+ '-');
+ if (NULL == dash)
+ return false;
+ dash++;
+ if (1 !=
+ sscanf (cookie,
+ "%llu-",
+ &u))
+ return false;
+ a.abs_value_us = u;
+ if (GNUNET_TIME_absolute_is_past (a))
+ return false;
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (dash,
+ strlen (dash),
+ &c,
+ sizeof (c)))
+ return false;
+ compute_cookie_hash (a,
+ ca_len,
+ ca,
+ &h);
+ return (0 ==
+ GNUNET_memcmp (&c,
+ &h));
+}
+
+
+/**
+ * Compute access cookie hash for the given @a expiration and @a ca.
+ *
+ * @param expiration expiration time of the cookie
+ * @param ca_len number of bytes in @a ca
+ * @param ca client address
+ * @param[out] c set to the cookie hash
+ */
+static char *
+compute_cookie (struct GNUNET_TIME_Absolute expiration,
+ size_t ca_len,
+ const void *ca)
+{
+ struct GNUNET_HashCode h;
+ char *end;
+ char cstr[128];
+ char *res;
+
+ compute_cookie_hash (expiration,
+ ca_len,
+ ca,
+ &h);
+ end = GNUNET_STRINGS_data_to_string (&h,
+ sizeof (h),
+ cstr,
+ sizeof (cstr));
+ *end = '\0';
+ GNUNET_asprintf (&res,
+ "%llu-%s",
+ (unsigned long long) expiration.abs_value_us,
+ cstr);
+ return res;
+}
+
/* ********************* Global helpers ****************** */
@@ -904,6 +1045,48 @@ create_response (void *cls,
if (REQUEST_STATE_WITH_MHD == hr->state)
{
+ const char *cookie;
+ bool ok = (0 == no_check);
+
+ cookie = MHD_lookup_connection_value (con,
+ MHD_COOKIE_KIND,
+ "Paivana-Cookie");
+ if (NULL != cookie)
+ {
+ const union MHD_ConnectionInfo *ci;
+ const struct sockaddr *ca;
+ socklen_t ca_len;
+
+ ci = MHD_get_connection_info (con,
+ MHD_CONNECTION_INFO_CLIENT_ADDRESS);
+ GNUNET_assert (NULL != ci);
+ ca = ci->client_addr;
+ switch (ca->sa_family)
+ {
+ case AF_INET:
+ ca_len = sizeof (struct sockaddr_in);
+ break;
+ case AF_INET6:
+ ca_len = sizeof (struct sockaddr_in6);
+ break;
+ default:
+ GNUNET_break (0);
+ ca_len = 0;
+ break;
+ }
+ ok = check_cookie (cookie,
+ ca_len,
+ ca);
+ if (! ok)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Request denied\n");
+ return MHD_queue_response (con,
+ MHD_HTTP_PAYMENT_REQUIRED,
+ paywall);
+ }
+ }
+
hr->state = REQUEST_STATE_CLIENT_UPLOAD_STARTED;
/* TODO: hacks for 100 continue suppression would go here! */
return MHD_YES;
@@ -1637,6 +1820,54 @@ parse_serving_mean (const struct GNUNET_CONFIGURATION_Handle *ccfg,
}
+static bool
+load_paywall ()
+{
+ char *tpath;
+ char *fn;
+ int fd;
+ struct stat sb;
+
+ tpath = GNUNET_OS_installation_get_path (PAIVANA_project_data (),
+ GNUNET_OS_IPK_DATADIR);
+ GNUNET_asprintf (&fn,
+ "%s/paywall.html",
+ tpath);
+ GNUNET_free (tpath);
+ fd = open (fn,
+ O_RDONLY);
+ if (-1 == fd)
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
+ "open",
+ fn);
+ GNUNET_free (fn);
+ return false;
+ }
+ if (0 !=
+ fstat (fd,
+ &sb))
+ {
+ GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
+ "stat",
+ fn);
+ GNUNET_free (fn);
+ GNUNET_break (0 == close (fd));
+ return false;
+ }
+ GNUNET_free (fn);
+ paywall = MHD_create_response_from_fd (sb.st_size,
+ fd);
+ if (NULL == paywall)
+ return false;
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (paywall,
+ MHD_HTTP_HEADER_CONTENT_TYPE,
+ "text/html"));
+ return true;
+}
+
+
/**
* Main function that will be run. Main tasks are (1) init. the
* curl infrastructure (curl_global_init() / curl_multi_init()),
@@ -1658,6 +1889,7 @@ run (void *cls,
int fh = -1;
char *serve_unixpath = NULL;
mode_t serve_unixmode = 0;
+ char *secret;
(void) cls;
(void) args;
@@ -1671,6 +1903,11 @@ run (void *cls,
GNUNET_SCHEDULER_shutdown ();
return;
}
+ if (! load_paywall ())
+ {
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
if (NULL == (curl_multi = curl_multi_init ()))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
@@ -1681,8 +1918,8 @@ run (void *cls,
/* No need to check return value. If given, we take,
* otherwise it stays zero. */
if (GNUNET_OK !=
- GNUNET_CONFIGURATION_get_value_string
- (c,
+ GNUNET_CONFIGURATION_get_value_string (
+ c,
"paivana",
"DESTINATION_BASE_URL",
&target_server_base_url))
@@ -1693,6 +1930,53 @@ run (void *cls,
GNUNET_SCHEDULER_shutdown ();
return;
}
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_string (
+ c,
+ "paivana",
+ "MERCHANT_BACKEND_URL",
+ &merchant_base_url))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "paivana",
+ "MERCHANT_BACKEND_URL");
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_string (
+ c,
+ "paivana",
+ "MERCHANT_ACCESS_TOKEN",
+ &merchant_access_token))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "paivana",
+ "MERCHANT_ACCESS_TOKEN");
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_string (
+ c,
+ "paivana",
+ "SECRET",
+ &secret))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
+ "paivana",
+ "SECRET");
+ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
+ &paivana_secret,
+ sizeof (paivana_secret));
+ }
+ else
+ {
+ GNUNET_CRYPTO_hash (secret,
+ strlen (secret),
+ &paivana_secret);
+ GNUNET_free (secret);
+ }
if (GNUNET_SYSERR ==
parse_serving_mean (c,
@@ -1746,6 +2030,12 @@ main (int argc,
char *const *argv)
{
struct GNUNET_GETOPT_CommandLineOption options[] = {
+ GNUNET_GETOPT_option_flag (
+ 'n',
+ "no-payment",
+ gettext_noop (
+ "disables payment, useful for testing reverse-proxy only"),
+ &no_check),
GNUNET_GETOPT_OPTION_END
};
diff --git a/src/backend/test.conf b/src/backend/test.conf
@@ -2,3 +2,8 @@
DESTINATION_BASE_URL = https://grothoff.org/
SERVE = tcp
HTTP_PORT = 8888
+
+MERCHANT_BACKEND_URL = https://backend.demo.taler.net/instances/sandbox/
+MERCHANT_ACCESS_TOKEN = secret-token:sandbox
+
+SECRET = 42
+\ No newline at end of file