paivana-httpd_daemon.c (11271B)
1 /* 2 This file is part of GNUnet. 3 Copyright (C) 2026 Taler Systems SA 4 5 Paivana is free software; you can redistribute it and/or 6 modify it under the terms of the GNU General Public License 7 as published by the Free Software Foundation; either version 8 3, or (at your option) any later version. 9 10 Paivana is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty 12 of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 13 the GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public 16 License along with Paivana; see the file COPYING. If not, 17 write to the Free Software Foundation, Inc., 51 Franklin 18 Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 */ 20 21 /** 22 * @author Christian Grothoff 23 * @file paivana-httpd_daemon.c 24 * @brief daemon functions 25 */ 26 27 #include "platform.h" 28 #include <curl/curl.h> 29 #include <gnunet/gnunet_util_lib.h> 30 #include <gnunet/gnunet_curl_lib.h> 31 #include <taler/taler_mhd_lib.h> 32 #include "paivana-httpd_cookie.h" 33 #include "paivana-httpd_daemon.h" 34 #include "paivana-httpd_helper.h" 35 #include "paivana-httpd_pay.h" 36 #include "paivana-httpd_reverse.h" 37 #include "paivana-httpd_templates.h" 38 39 40 struct RequestContext 41 { 42 43 /** 44 * HTTP connection to the client. 45 */ 46 struct MHD_Connection *connection; 47 48 /** 49 * Handle for request forwarding as reverse proxy. 50 */ 51 struct HttpRequest *hr; 52 53 /** 54 * Handle for processing actual payment. 55 */ 56 struct PayRequest *hp; 57 58 /** 59 * Full request URL. 60 */ 61 char *url; 62 63 /** 64 * True if this is a POST to the .well-known/paivana endpoint. 65 */ 66 bool is_paivana; 67 68 /** 69 * We are past the paywall, forward to client. 70 */ 71 bool do_forward; 72 }; 73 74 75 /** 76 * Set to true if we started a daemon. 77 */ 78 static bool have_daemons; 79 80 81 /** 82 * Main MHD callback for handling requests. 83 * 84 * @param cls unused 85 * @param con MHD connection handle 86 * @param url the url in the request 87 * @param meth the HTTP method used ("GET", "PUT", etc.) 88 * @param ver the HTTP version string (i.e. "HTTP/1.1") 89 * @param upload_data the data being uploaded (excluding HEADERS, 90 * for a POST that fits into memory and that is encoded 91 * with a supported encoding, the POST data will NOT be 92 * given in upload_data and is instead available as 93 * part of MHD_get_connection_values; very large POST 94 * data *will* be made available incrementally in 95 * upload_data) 96 * @param upload_data_size set initially to the size of the 97 * @a upload_data provided; the method must update this 98 * value to the number of bytes NOT processed; 99 * @param con_cls pointer to location where we store the 100 * 'struct Request' 101 * @return #MHD_YES if the connection was handled successfully, 102 * #MHD_NO if the socket must be closed due to a serious 103 * error while handling the request 104 */ 105 static enum MHD_Result 106 create_response (void *cls, 107 struct MHD_Connection *con, 108 const char *url, 109 const char *meth, 110 const char *ver, 111 const char *upload_data, 112 size_t *upload_data_size, 113 void **con_cls) 114 { 115 struct RequestContext *rc = *con_cls; 116 const char *cookie; 117 bool ok = false; 118 struct GNUNET_Buffer buf; 119 char *website; 120 121 (void) cls; 122 memset (&buf, 123 0, 124 sizeof (buf)); 125 if ( (! rc->is_paivana) && 126 (0 == strcmp (url, 127 "/.well-known/paivana")) && 128 (0 == strcasecmp (meth, 129 MHD_HTTP_METHOD_POST)) ) 130 { 131 rc->is_paivana = true; 132 } 133 if (rc->is_paivana) 134 { 135 if (PH_no_check) 136 { 137 /* paywall disabled, 501 */ 138 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 139 "Paywall disabled, refusing to respond to %s\n", 140 url); 141 return TALER_MHD_reply_with_error (rc->connection, 142 MHD_HTTP_NOT_IMPLEMENTED, 143 TALER_EC_PAIVANA_PAYWALL_DISABLED, 144 NULL); 145 } 146 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 147 "Client POSTed payment, checking validity\n"); 148 if (NULL == rc->hp) 149 rc->hp = PAIVANA_HTTPD_payment_create (rc->connection); 150 return PAIVANA_HTTPD_payment_handle (rc->hp, 151 upload_data, 152 upload_data_size); 153 } 154 155 if (PH_have_whitelist_ex && (! rc->do_forward)) 156 { 157 rc->do_forward = (0 == 158 regexec (&PH_whitelist_ex, 159 url, 160 0, 161 NULL, 162 0)); 163 } 164 165 if (rc->do_forward) 166 goto do_forward; 167 168 if ( (0 == strncmp (url, 169 "/.well-known/paivana/templates/", 170 strlen ("/.well-known/paivana/templates/"))) && 171 ( (0 == strcasecmp (meth, 172 MHD_HTTP_METHOD_GET)) || 173 /* HEAD must be answered like GET (MHD suppresses the body); 174 otherwise it falls through to the paywall check below, 175 which redirects to this very URL again -- forever. */ 176 (0 == strcasecmp (meth, 177 MHD_HTTP_METHOD_HEAD)) ) ) 178 { 179 const char *id = &url[strlen ("/.well-known/paivana/templates/")]; 180 181 return PAIVANA_HTTPD_return_template (rc->connection, 182 id); 183 } 184 185 if (! PAIVANA_HTTPD_get_base_url (con, 186 &buf)) 187 { 188 GNUNET_break (0); 189 return TALER_MHD_reply_with_error ( 190 con, 191 MHD_HTTP_BAD_REQUEST, 192 TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, 193 "Host or X-Forwarded-Host required"); 194 } 195 GNUNET_buffer_write_str (&buf, 196 url); 197 website = GNUNET_buffer_reap_str (&buf); 198 cookie = MHD_lookup_connection_value (con, 199 MHD_COOKIE_KIND, 200 "Paivana-Cookie"); 201 if (NULL != cookie) 202 { 203 void *ca = NULL; 204 size_t ca_len = 0; 205 206 /* If we cannot get the client address, we just 207 use 0/NULL and log an error. */ 208 GNUNET_break (PAIVANA_HTTPD_get_client_address (con, 209 &ca, 210 &ca_len)); 211 ok = PAIVANA_HTTPD_check_cookie (cookie, 212 website, 213 ca_len, 214 ca); 215 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 216 "Client sent cookie %s for %s: %s\n", 217 cookie, 218 website, 219 ok ? "good" : "invalid"); 220 GNUNET_free (ca); 221 } 222 if (! ok) 223 { 224 enum GNUNET_GenericReturnValue ret; 225 226 ret = PAIVANA_HTTPD_search_templates (con, 227 website); 228 if (GNUNET_SYSERR != ret) 229 { 230 GNUNET_free (website); 231 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 232 "Payment required, sending paywall page %s\n", 233 (GNUNET_OK == ret) ? "ok" : "failed"); 234 return (GNUNET_OK == ret) ? MHD_YES : MHD_NO; 235 } 236 } 237 GNUNET_free (website); 238 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 239 "Request OK, no paywall applies!\n"); 240 rc->do_forward = true; 241 do_forward: 242 if (NULL == rc->hr) 243 rc->hr = PAIVANA_HTTPD_reverse_create (rc->connection, 244 rc->url); 245 return PAIVANA_HTTPD_reverse (rc->hr, 246 con, 247 url, 248 meth, 249 ver, 250 upload_data, 251 upload_data_size); 252 } 253 254 255 /** 256 * Function called when MHD decides that we 257 * are done with a request. 258 * 259 * @param cls NULL 260 * @param connection connection handle 261 * @param con_cls value as set by the last call to 262 * the MHD_AccessHandlerCallback, should be 263 * our `struct RequestContext *` (created in `mhd_log_callback()`) 264 * @param toe reason for request termination (ignored) 265 */ 266 static void 267 mhd_completed_cb (void *cls, 268 struct MHD_Connection *connection, 269 void **con_cls, 270 enum MHD_RequestTerminationCode toe) 271 { 272 struct RequestContext *rc = *con_cls; 273 274 (void) cls; 275 (void) connection; 276 if (NULL == rc) 277 return; 278 if (MHD_REQUEST_TERMINATED_COMPLETED_OK != toe) 279 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 280 "MHD encountered error handling request to %s: %d\n", 281 rc->url, 282 toe); 283 if (NULL != rc->hr) 284 PAIVANA_HTTPD_reverse_cleanup (rc->hr); 285 if (NULL != rc->hp) 286 PAIVANA_HTTPD_payment_destroy (rc->hp); 287 GNUNET_free (rc->url); 288 GNUNET_free (rc); 289 *con_cls = NULL; 290 } 291 292 293 /** 294 * Function called when MHD first processes an incoming connection. 295 * Gives us the respective URI information. 296 * 297 * We use this to associate the `struct MHD_Connection` with our 298 * internal `struct HttpRequest` data structure (by checking 299 * for matching sockets). 300 * 301 * @param cls the HTTP server handle (a `struct MhdHttpList`) 302 * @param url the URL that is being requested 303 * @param connection MHD connection object for the request 304 * @return the `struct RequestContext` that this @a connection is for 305 */ 306 static void * 307 mhd_log_callback (void *cls, 308 const char *url, 309 struct MHD_Connection *connection) 310 { 311 struct RequestContext *rc; 312 313 rc = GNUNET_new (struct RequestContext); 314 rc->connection = connection; 315 rc->url = GNUNET_strdup (url); 316 rc->do_forward = (1 == PH_no_check); 317 return rc; 318 } 319 320 321 /** 322 * Callback invoked on every listen socket to start the 323 * respective MHD HTTP daemon. 324 * 325 * @param cls unused 326 * @param lsock the listen socket 327 */ 328 static void 329 start_daemon (void *cls, 330 int lsock) 331 { 332 struct MHD_Daemon *mhd; 333 334 (void) cls; 335 GNUNET_assert (-1 != lsock); 336 mhd = MHD_start_daemon ( 337 MHD_USE_DEBUG 338 | MHD_ALLOW_SUSPEND_RESUME 339 | MHD_USE_DUAL_STACK, 340 0, 341 NULL, NULL, 342 &create_response, NULL, 343 MHD_OPTION_LISTEN_SOCKET, 344 lsock, 345 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 16, 346 MHD_OPTION_NOTIFY_COMPLETED, &mhd_completed_cb, NULL, 347 MHD_OPTION_URI_LOG_CALLBACK, &mhd_log_callback, NULL, 348 MHD_OPTION_END); 349 350 if (NULL == mhd) 351 { 352 GNUNET_break (0); 353 GNUNET_SCHEDULER_shutdown (); 354 return; 355 } 356 have_daemons = true; 357 TALER_MHD_daemon_start (mhd); 358 } 359 360 361 void 362 PAIVANA_HTTPD_serve_requests () 363 { 364 enum GNUNET_GenericReturnValue ret; 365 366 ret = TALER_MHD_listen_bind (PH_cfg, 367 "paivana", 368 &start_daemon, 369 NULL); 370 switch (ret) 371 { 372 case GNUNET_SYSERR: 373 PH_global_ret = EXIT_NOTCONFIGURED; 374 GNUNET_SCHEDULER_shutdown (); 375 return; 376 case GNUNET_NO: 377 if (! have_daemons) 378 { 379 PH_global_ret = EXIT_NOTCONFIGURED; 380 GNUNET_SCHEDULER_shutdown (); 381 return; 382 } 383 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 384 "Could not open all configured listen sockets\n"); 385 break; 386 case GNUNET_OK: 387 break; 388 } 389 }