paivana

HTTP paywall reverse proxy
Log | Files | Refs | README | LICENSE

commit fa5ffd38bccd26f42466080ad9d073a092fa80eb
parent e035546a0bc48131bffded1229f4f0bcf6413774
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu, 27 Nov 2025 00:24:35 +0100

basic paywall logic

Diffstat:
Msrc/backend/paivana-httpd.c | 294++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/backend/test.conf | 6++++++
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