taler-exchange-httpd_get-purses-PURSE_PUB-merge.c (13598B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file taler-exchange-httpd_get-purses-PURSE_PUB-merge.c 18 * @brief Handle GET /purses/$PID/$TARGET requests 19 * @author Christian Grothoff 20 */ 21 #include <gnunet/gnunet_util_lib.h> 22 #include <jansson.h> 23 #include <microhttpd.h> 24 #include "taler/taler_mhd_lib.h" 25 #include "taler/taler_dbevents.h" 26 #include "taler-exchange-httpd_get-keys.h" 27 #include "taler-exchange-httpd_get-purses-PURSE_PUB-merge.h" 28 #include "taler-exchange-httpd_mhd.h" 29 #include "taler-exchange-httpd_responses.h" 30 #include "exchange-database/select_purse.h" 31 #include "exchange-database/event_listen.h" 32 #include "exchange-database/event_listen_cancel.h" 33 34 35 /** 36 * Information about an ongoing /purses GET operation. 37 */ 38 struct GetContext 39 { 40 /** 41 * Kept in a DLL. 42 */ 43 struct GetContext *next; 44 45 /** 46 * Kept in a DLL. 47 */ 48 struct GetContext *prev; 49 50 /** 51 * Connection we are handling. 52 */ 53 struct MHD_Connection *connection; 54 55 /** 56 * Subscription for the database event we are 57 * waiting for. 58 */ 59 struct GNUNET_DB_EventHandler *eh; 60 61 /** 62 * Subscription for refund event we are 63 * waiting for. 64 */ 65 struct GNUNET_DB_EventHandler *ehr; 66 67 /** 68 * Public key of our purse. 69 */ 70 struct TALER_PurseContractPublicKeyP purse_pub; 71 72 /** 73 * When does this purse expire? 74 */ 75 struct GNUNET_TIME_Timestamp purse_expiration; 76 77 /** 78 * When was this purse merged? 79 */ 80 struct GNUNET_TIME_Timestamp merge_timestamp; 81 82 /** 83 * How much is the purse (supposed) to be worth? 84 */ 85 struct TALER_Amount amount; 86 87 /** 88 * How much was deposited into the purse so far? 89 */ 90 struct TALER_Amount deposited; 91 92 /** 93 * Hash over the contract of the purse. 94 */ 95 struct TALER_PrivateContractHashP h_contract; 96 97 /** 98 * When will this request time out? 99 */ 100 struct GNUNET_TIME_Absolute timeout; 101 102 /** 103 * true to wait for merge, false to wait for deposit. 104 */ 105 bool wait_for_merge; 106 107 /** 108 * True if we are still suspended. 109 */ 110 bool suspended; 111 }; 112 113 114 /** 115 * Head of DLL of suspended GET requests. 116 */ 117 static struct GetContext *gc_head; 118 119 /** 120 * Tail of DLL of suspended GET requests. 121 */ 122 static struct GetContext *gc_tail; 123 124 125 void 126 TEH_purses_get_cleanup () 127 { 128 struct GetContext *gc; 129 130 while (NULL != (gc = gc_head)) 131 { 132 GNUNET_CONTAINER_DLL_remove (gc_head, 133 gc_tail, 134 gc); 135 if (gc->suspended) 136 { 137 gc->suspended = false; 138 MHD_resume_connection (gc->connection); 139 } 140 } 141 } 142 143 144 /** 145 * Function called once a connection is done to 146 * clean up the `struct GetContext` state. 147 * 148 * @param rc context to clean up for 149 */ 150 static void 151 gc_cleanup (struct TEH_RequestContext *rc) 152 { 153 struct GetContext *gc = rc->rh_ctx; 154 155 GNUNET_assert (! gc->suspended); 156 if (NULL != gc->eh) 157 { 158 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 159 "Cancelling DB event listening\n"); 160 TALER_TALER_EXCHANGEDB_event_listen_cancel (TEH_pg, 161 gc->eh); 162 gc->eh = NULL; 163 } 164 if (NULL != gc->ehr) 165 { 166 TALER_TALER_EXCHANGEDB_event_listen_cancel (TEH_pg, 167 gc->ehr); 168 gc->ehr = NULL; 169 } 170 GNUNET_free (gc); 171 } 172 173 174 /** 175 * Function called on events received from Postgres. 176 * Wakes up long pollers. 177 * 178 * @param cls the `struct TEH_RequestContext *` 179 * @param extra additional event data provided 180 * @param extra_size number of bytes in @a extra 181 */ 182 static void 183 db_event_cb (void *cls, 184 const void *extra, 185 size_t extra_size) 186 { 187 struct TEH_RequestContext *rc = cls; 188 struct GetContext *gc = rc->rh_ctx; 189 struct GNUNET_AsyncScopeSave old_scope; 190 191 (void) extra; 192 (void) extra_size; 193 if (NULL == gc) 194 return; /* event triggered while main transaction 195 was still running */ 196 if (! gc->suspended) 197 return; /* might get multiple wake-up events */ 198 gc->suspended = false; 199 GNUNET_async_scope_enter (&rc->async_scope_id, 200 &old_scope); 201 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 202 "Waking up on %p - %p - %s\n", 203 rc, 204 gc, 205 gc->suspended ? "suspended" : "active"); 206 TEH_check_invariants (); 207 GNUNET_CONTAINER_DLL_remove (gc_head, 208 gc_tail, 209 gc); 210 MHD_resume_connection (gc->connection); 211 TALER_MHD_daemon_trigger (); 212 TEH_check_invariants (); 213 GNUNET_async_scope_restore (&old_scope); 214 } 215 216 217 MHD_RESULT 218 TEH_handler_purses_get (struct TEH_RequestContext *rc, 219 const char *const args[2]) 220 { 221 struct GetContext *gc = rc->rh_ctx; 222 bool purse_deleted; 223 bool purse_refunded; 224 MHD_RESULT res; 225 226 if (NULL == gc) 227 { 228 gc = GNUNET_new (struct GetContext); 229 rc->rh_ctx = gc; 230 rc->rh_cleaner = &gc_cleanup; 231 gc->connection = rc->connection; 232 if (GNUNET_OK != 233 GNUNET_STRINGS_string_to_data (args[0], 234 strlen (args[0]), 235 &gc->purse_pub, 236 sizeof (gc->purse_pub))) 237 { 238 GNUNET_break_op (0); 239 return TALER_MHD_reply_with_error (rc->connection, 240 MHD_HTTP_BAD_REQUEST, 241 TALER_EC_EXCHANGE_GENERIC_PURSE_PUB_MALFORMED, 242 args[0]); 243 } 244 if (0 == strcmp (args[1], 245 "merge")) 246 gc->wait_for_merge = true; 247 else if (0 == strcmp (args[1], 248 "deposit")) 249 gc->wait_for_merge = false; 250 else 251 { 252 GNUNET_break_op (0); 253 return TALER_MHD_reply_with_error (rc->connection, 254 MHD_HTTP_BAD_REQUEST, 255 TALER_EC_EXCHANGE_PURSES_INVALID_WAIT_TARGET, 256 args[1]); 257 } 258 259 TALER_MHD_parse_request_timeout (rc->connection, 260 &gc->timeout); 261 if ( (GNUNET_TIME_absolute_is_future (gc->timeout)) && 262 (NULL == gc->eh) ) 263 { 264 struct TALER_EXCHANGEDB_PurseEventP rep = { 265 .header.size = htons (sizeof (rep)), 266 .header.type = htons ( 267 gc->wait_for_merge 268 ? TALER_DBEVENT_EXCHANGE_PURSE_MERGED 269 : TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), 270 .purse_pub = gc->purse_pub 271 }; 272 273 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 274 "Starting DB event listening on purse %s (%s)\n", 275 TALER_B2S (&gc->purse_pub), 276 gc->wait_for_merge 277 ? "waiting for merge" 278 : "waiting for deposit"); 279 gc->eh = TALER_EXCHANGEDB_event_listen ( 280 TEH_pg, 281 GNUNET_TIME_absolute_get_remaining (gc->timeout), 282 &rep.header, 283 &db_event_cb, 284 rc); 285 if (NULL == gc->eh) 286 { 287 GNUNET_break (0); 288 gc->timeout = GNUNET_TIME_UNIT_ZERO_ABS; 289 } 290 else 291 { 292 struct GNUNET_DB_EventHeaderP repr = { 293 .size = htons (sizeof (repr)), 294 .type = htons (TALER_DBEVENT_EXCHANGE_PURSE_REFUNDED), 295 }; 296 297 gc->ehr = TALER_EXCHANGEDB_event_listen ( 298 TEH_pg, 299 GNUNET_TIME_absolute_get_remaining (gc->timeout), 300 &repr, 301 &db_event_cb, 302 rc); 303 } 304 } 305 } /* end first-time initialization */ 306 307 { 308 enum GNUNET_DB_QueryStatus qs; 309 struct GNUNET_TIME_Timestamp create_timestamp; 310 311 qs = TALER_EXCHANGEDB_select_purse (TEH_pg, 312 &gc->purse_pub, 313 &create_timestamp, 314 &gc->purse_expiration, 315 &gc->amount, 316 &gc->deposited, 317 &gc->h_contract, 318 &gc->merge_timestamp, 319 &purse_deleted, 320 &purse_refunded); 321 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 322 "select_purse %s returned %d (%s)\n", 323 args[0], 324 (int) qs, 325 GNUNET_TIME_timestamp2s (gc->merge_timestamp)); 326 switch (qs) 327 { 328 case GNUNET_DB_STATUS_HARD_ERROR: 329 GNUNET_break (0); 330 return TALER_MHD_reply_with_error (rc->connection, 331 MHD_HTTP_INTERNAL_SERVER_ERROR, 332 TALER_EC_GENERIC_DB_FETCH_FAILED, 333 "select_purse"); 334 case GNUNET_DB_STATUS_SOFT_ERROR: 335 GNUNET_break (0); 336 return TALER_MHD_reply_with_error (rc->connection, 337 MHD_HTTP_INTERNAL_SERVER_ERROR, 338 TALER_EC_GENERIC_DB_FETCH_FAILED, 339 "select_purse"); 340 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 341 return TALER_MHD_reply_with_error (rc->connection, 342 MHD_HTTP_NOT_FOUND, 343 TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, 344 NULL); 345 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 346 break; /* handled below */ 347 } 348 } 349 if (purse_refunded || 350 purse_deleted) 351 { 352 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 353 "Purse refunded or deleted\n"); 354 return TALER_MHD_reply_with_error (rc->connection, 355 MHD_HTTP_GONE, 356 purse_deleted 357 ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED 358 : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED 359 , 360 GNUNET_TIME_timestamp2s ( 361 gc->purse_expiration)); 362 } 363 364 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 365 "Deposited amount is %s (%d/%d/%d)\n", 366 TALER_amount2s (&gc->deposited), 367 GNUNET_TIME_absolute_is_future (gc->timeout), 368 GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time), 369 (0 < 370 TALER_amount_cmp (&gc->amount, 371 &gc->deposited))); 372 if (GNUNET_TIME_absolute_is_future (gc->timeout) && 373 ( ((gc->wait_for_merge) && 374 GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time)) || 375 ((! gc->wait_for_merge) && 376 (0 < 377 TALER_amount_cmp (&gc->amount, 378 &gc->deposited))) ) ) 379 { 380 gc->suspended = true; 381 GNUNET_CONTAINER_DLL_insert (gc_head, 382 gc_tail, 383 gc); 384 MHD_suspend_connection (gc->connection); 385 return MHD_YES; 386 } 387 388 { 389 struct GNUNET_TIME_Timestamp dt = GNUNET_TIME_timestamp_get (); 390 struct TALER_ExchangePublicKeyP exchange_pub; 391 struct TALER_ExchangeSignatureP exchange_sig; 392 enum TALER_ErrorCode ec; 393 394 if (GNUNET_TIME_timestamp_cmp (dt, 395 >, 396 gc->purse_expiration)) 397 dt = gc->purse_expiration; 398 if (0 < 399 TALER_amount_cmp (&gc->amount, 400 &gc->deposited)) 401 { 402 /* amount > deposited: not yet fully paid */ 403 dt = GNUNET_TIME_UNIT_ZERO_TS; 404 } 405 if (TALER_EC_NONE != 406 (ec = TALER_exchange_online_purse_status_sign ( 407 &TEH_keys_exchange_sign_, 408 gc->merge_timestamp, 409 dt, 410 &gc->deposited, 411 &exchange_pub, 412 &exchange_sig))) 413 { 414 res = TALER_MHD_reply_with_ec (rc->connection, 415 ec, 416 NULL); 417 } 418 else 419 { 420 /* Make sure merge_timestamp is omitted if not yet merged */ 421 if (GNUNET_TIME_absolute_is_never (gc->merge_timestamp.abs_time)) 422 gc->merge_timestamp = GNUNET_TIME_UNIT_ZERO_TS; 423 res = TALER_MHD_REPLY_JSON_PACK ( 424 rc->connection, 425 MHD_HTTP_OK, 426 TALER_JSON_pack_amount ("balance", 427 &gc->deposited), 428 GNUNET_JSON_pack_data_auto ("exchange_sig", 429 &exchange_sig), 430 GNUNET_JSON_pack_data_auto ("exchange_pub", 431 &exchange_pub), 432 GNUNET_JSON_pack_timestamp ("purse_expiration", 433 gc->purse_expiration), 434 GNUNET_JSON_pack_allow_null ( 435 GNUNET_JSON_pack_timestamp ("merge_timestamp", 436 gc->merge_timestamp)), 437 GNUNET_JSON_pack_allow_null ( 438 GNUNET_JSON_pack_timestamp ("deposit_timestamp", 439 dt)) 440 ); 441 } 442 } 443 return res; 444 } 445 446 447 /* end of taler-exchange-httpd_purses_get.c */