sync

Backup service to store encrypted wallet databases (experimental)
Log | Files | Refs | Submodules | README | LICENSE

commit e838c2881998fe7dd9b186b5fa06af433541bc32
parent f33f0927a2cb0ce0b2e7843fe1664cfc895a4ec0
Author: Iván Ávalos <avalos@disroot.org>
Date:   Tue, 26 May 2026 17:35:13 +0200

new sync server implementation (DD92)

Diffstat:
Mmeson.build | 23+++++++++++++++--------
Msrc/include/sync/meson.build | 1+
Asrc/include/sync/sync-database/append_block_TR.h | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/sync/sync-database/common.h | 15++++++++++-----
Msrc/include/sync/sync-database/create_tables.h | 2+-
Asrc/include/sync/sync-database/delete_block_TR.h | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/sync/sync-database/drop_tables.h | 2+-
Msrc/include/sync/sync-database/gc.h | 24++++++++++++++----------
Msrc/include/sync/sync-database/increment_lifetime_TR.h | 2+-
Msrc/include/sync/sync-database/lookup_account_TR.h | 33+++++++++++++++++++++------------
Dsrc/include/sync/sync-database/lookup_backup_TR.h | 48------------------------------------------------
Asrc/include/sync/sync-database/lookup_block_TR.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/sync/sync-database/lookup_block_range_TR.h | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/sync/sync-database/lookup_object.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/sync/sync-database/lookup_object_uid.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/sync/sync-database/lookup_pending_payments_by_account.h | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/include/sync/sync-database/lookup_pending_payments_by_account_TR.h | 60------------------------------------------------------------
Msrc/include/sync/sync-database/meson.build | 18+++++++++++-------
Msrc/include/sync/sync-database/preflight.h | 2+-
Dsrc/include/sync/sync-database/store_backup_TR.h | 48------------------------------------------------
Asrc/include/sync/sync-database/store_object_TR.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/sync/sync-database/store_payment_TR.h | 6++++--
Dsrc/include/sync/sync-database/update_backup_TR.h | 48------------------------------------------------
Asrc/include/sync/sync-database/update_block_TR.h | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/sync/sync_database_lib.h | 38++++++++++++++++++++++++++------------
Msrc/include/sync/sync_service.h | 569+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Asrc/include/sync/sync_signatures.h | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/sync/sync_testing_lib.h | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/include/sync/sync_util.h | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/lib/meson.build | 18++++++++++--------
Asrc/lib/sync_api_block_append.c | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/sync_api_block_delete.c | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/sync_api_block_list.c | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/sync_api_block_operation_common.c | 518+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/sync_api_block_operation_common.h | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/sync_api_block_update.c | 45+++++++++++++++++++++++++++++++++++++++++++++
Dsrc/lib/sync_api_download.c | 307-------------------------------------------------------------------------------
Asrc/lib/sync_api_object_fetch.c | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/sync_api_object_upload.c | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/lib/sync_api_upload.c | 441-------------------------------------------------------------------------------
Msrc/sync/meson.build | 47++++++++++++-----------------------------------
Dsrc/sync/sync-httpd.c | 655-------------------------------------------------------------------------------
Dsrc/sync/sync-httpd.h | 173-------------------------------------------------------------------------------
Msrc/sync/sync-httpd2.c | 446+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/sync/sync-httpd2.h | 48++++++++++++++++++++++++++++++++++++++++++++++--
Dsrc/sync/sync-httpd2_backup.c | 237-------------------------------------------------------------------------------
Dsrc/sync/sync-httpd2_backup.h | 74--------------------------------------------------------------------------
Dsrc/sync/sync-httpd2_backup_post.c | 1013-------------------------------------------------------------------------------
Msrc/sync/sync-httpd2_config.c | 9+++++++--
Msrc/sync/sync-httpd2_config.h | 4++--
Asrc/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.c | 701+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_get-backups-KEY-blocks.c | 334+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_get-backups-KEY-blocks.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_get-backups-KEY-objects-UID.c | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_get-backups-KEY-objects-UID.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_get-backups-KEY.c | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_get-backups-KEY.h | 38++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_payments.c | 491+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_payments.h | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.c | 985+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_post-backups-KEY-objects-UID.c | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sync/sync-httpd2_post-backups-KEY-objects-UID.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/sync/sync-httpd_backup.c | 263-------------------------------------------------------------------------------
Dsrc/sync/sync-httpd_backup.h | 79-------------------------------------------------------------------------------
Dsrc/sync/sync-httpd_backup_post.c | 1055-------------------------------------------------------------------------------
Dsrc/sync/sync-httpd_config.c | 68--------------------------------------------------------------------
Dsrc/sync/sync-httpd_config.h | 45---------------------------------------------
Dsrc/sync/sync-httpd_mhd.c | 66------------------------------------------------------------------
Dsrc/sync/sync-httpd_mhd.h | 69---------------------------------------------------------------------
Msrc/syncdb/Makefile.sql | 4+++-
Msrc/syncdb/meson.build | 15++++++++++-----
Msrc/syncdb/procedures.sql.in | 13++++++++++---
Msrc/syncdb/sync-0001.sql | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/syncdb/sync-dbinit.c | 16++++++++++++----
Dsrc/syncdb/sync_db_plugin.c | 87-------------------------------------------------------------------------------
Asrc/syncdb/syncdb_account_check.sql | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_append_block_TR.c | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_append_block_TR.sql | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/syncdb_create_tables.c | 37++++++++++++++++++++-----------------
Asrc/syncdb/syncdb_delete_block_TR.c | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_delete_block_TR.sql | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/syncdb_drop_tables.c | 21++-------------------
Msrc/syncdb/syncdb_gc.c | 47+++++++++++++++++++++--------------------------
Asrc/syncdb/syncdb_gc.sql | 40++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/syncdb_increment_lifetime_TR.c | 6++++++
Msrc/syncdb/syncdb_increment_lifetime_TR.sql | 31++++++++++---------------------
Msrc/syncdb/syncdb_lookup_account_TR.c | 51+++++++++++++++++++++++++++------------------------
Dsrc/syncdb/syncdb_lookup_account_TR.sql | 54------------------------------------------------------
Dsrc/syncdb/syncdb_lookup_backup_TR.c | 84-------------------------------------------------------------------------------
Asrc/syncdb/syncdb_lookup_block_TR.c | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_lookup_block_TR.sql | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_lookup_block_range_TR.c | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_lookup_block_range_TR.sql | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_lookup_object.c | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_lookup_object_uid.c | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_lookup_pending_payments_by_account.c | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/syncdb/syncdb_lookup_pending_payments_by_account_TR.c | 142-------------------------------------------------------------------------------
Asrc/syncdb/syncdb_object_refcounts.sql | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/syncdb_pg.c | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/syncdb/syncdb_pg.h | 5+++--
Msrc/syncdb/syncdb_preflight.c | 62++------------------------------------------------------------
Dsrc/syncdb/syncdb_store_backup_TR.c | 100-------------------------------------------------------------------------------
Dsrc/syncdb/syncdb_store_backup_TR.sql | 93-------------------------------------------------------------------------------
Asrc/syncdb/syncdb_store_object_TR.c | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_store_object_TR.sql | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/syncdb_store_payment_TR.c | 53+++++++++++++++++++++++++++++++----------------------
Asrc/syncdb/syncdb_store_payment_TR.sql | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/syncdb/syncdb_update_backup_TR.c | 108-------------------------------------------------------------------------------
Dsrc/syncdb/syncdb_update_backup_TR.sql | 97-------------------------------------------------------------------------------
Asrc/syncdb/syncdb_update_block_TR.c | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/syncdb/syncdb_update_block_TR.sql | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/test_sync_db.c | 972++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/testing/meson.build | 12++++++++++--
Msrc/testing/test_sync_api.c | 588+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/testing/test_sync_api.conf | 3+++
Dsrc/testing/testing_api_cmd_backup_download.c | 283-------------------------------------------------------------------------------
Dsrc/testing/testing_api_cmd_backup_upload.c | 458-------------------------------------------------------------------------------
Asrc/testing/testing_api_cmd_block_append.c | 241+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_block_common.c | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_block_common.h | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_block_delete.c | 442+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_block_list.c | 445+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_block_update.c | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_object_fetch.c | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testing/testing_api_cmd_object_upload.c | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/meson.build | 34++++++++++++++++++++++++++++++++--
Msrc/util/os_installation.c | 2+-
Asrc/util/sync_block.c | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/sync_object.c | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/sync_signatures.c | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/test_sync_signatures.c | 413+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
133 files changed, 14195 insertions(+), 7178 deletions(-)

diff --git a/meson.build b/meson.build @@ -7,7 +7,11 @@ project( ) cc = meson.get_compiler('c') -incdir = include_directories('src/include', 'src/include/sync', 'src/include/sync/sync-database') +incdir = include_directories( + 'src/include', + 'src/include/sync', + 'src/include/sync/sync-database', +) # Used to populate gnunet_private_config.h private_config = configuration_data() @@ -69,9 +73,10 @@ if not get_option('only-doc') mhd_dep = cc.find_library('microhttpd', required: true) endif + # sync-httpd2 is the only daemon we build, so MHD2 is mandatory. mhd2_dep = dependency('libmicrohttpd2', required: false) if not mhd2_dep.found() - mhd2_dep = cc.find_library('microhttpd2', required: false) + mhd2_dep = cc.find_library('microhttpd2', required: true) endif json_dep = dependency('jansson', required: false) @@ -256,10 +261,7 @@ if not get_option('only-doc') 'taler/taler_pq_lib.h', 'TALER_PQ_query_param_array_blinded_denom_sig', required: true, - dependencies: [ - talerpq_dep, - pq_dep, - ], + dependencies: [talerpq_dep, pq_dep], ) private_config.set10('HAVE_TALERPQ', talerpq_dep.found()) @@ -346,11 +348,16 @@ if not get_option('only-doc') subdir('doc') endif - taler_prefix = get_option('prefix') / get_option('libdir') + # The variable name must match GNUNET_OS_ProjectData.env_varname in + # src/util/os_installation.c. Otherwise GNUnet falls back to locating + # $DATADIR relative to libsyncutil, which under the build-tree + # LD_LIBRARY_PATH that meson injects resolves into the build directory, + # and the tests fail to find the installed SQL files. + sync_prefix = get_option('prefix') / get_option('libdir') add_test_setup( 'default', - env: ['TALER_SYNC_PREFIX=' + taler_prefix], + env: ['SYNC_PREFIX=' + sync_prefix], exclude_suites: ['perf', 'installcheck', 'integrationtests'], is_default: true, ) diff --git a/src/include/sync/meson.build b/src/include/sync/meson.build @@ -3,6 +3,7 @@ subdir('sync-database') install_data( 'sync_database_lib.h', 'sync_service.h', + 'sync_signatures.h', 'sync_testing_lib.h', 'sync_util.h', install_dir: get_option('includedir') / 'sync', diff --git a/src/include/sync/sync-database/append_block_TR.h b/src/include/sync/sync-database/append_block_TR.h @@ -0,0 +1,73 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/append_block_TR.h + * @brief append block at the end of DLL + * @author Iván Ávalos + */ +#ifndef SYNC_DATABASE_APPEND_BLOCK_TR_H +#define SYNC_DATABASE_APPEND_BLOCK_TR_H + +#include <gnunet/gnunet_util_lib.h> +#include "sync/sync_service.h" + +/** + * Append block at the end of DLL. + * + * The append relinks the previous tail, so @a prev_upload_sig is the + * tail's new signature (covering its new next link). That signature was + * verified against the tail as it was read outside the transaction, so + * the transaction re-checks the stored tail against @a prev_hash and + * @a prev_prev_nonce before storing it. + * + * @param account_pub account to store block under + * @param block_nonce nonce of the block to store + * @param prev_nonce nonce of the last block in the backup + * @param data_hash hash of @a data + * @param data_size number of bytes in @a data + * @param data raw data of the block to store + * @param refs_length number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by the block + * @param object_incs array of refcount increments for @a object_uids + * @param upload_sig verified signature of the new block + * @param refs_hash hash over the object references + * @param prev_upload_sig new signature of the tail, NULL if the block is + * the first in the backup + * @param prev_hash current hash of the tail, NULL if the block is the + * first in the backup + * @param prev_prev_nonce nonce preceding the tail as covered by + * @a prev_upload_sig, NULL if the tail is the first block or if + * the block being appended is + * @return transaction status + */ +enum SYNC_DB_QueryStatus +SYNCDB_append_block_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *block_nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct GNUNET_HashCode *data_hash, + size_t data_size, + const void *data, + unsigned int refs_length, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *refs_hash, + const struct SYNC_AccountSignatureP *prev_upload_sig, + const struct GNUNET_HashCode *prev_hash, + const struct SYNC_BlockNonce *prev_prev_nonce); + +#endif diff --git a/src/include/sync/sync-database/common.h b/src/include/sync/sync-database/common.h @@ -14,7 +14,7 @@ Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_common.h + * @file include/sync/sync-database/common.h * @brief common sync database definitions * @author Christian Grothoff */ @@ -34,14 +34,19 @@ enum SYNC_DB_QueryStatus { /** - * Client claimed to be updating an existing backup, but we have none. + * Backup is in an inconsistent state. */ - SYNC_DB_OLD_BACKUP_MISSING = -5, + SYNC_DB_INCONSISTENT_BACKUP = -7, /** - * Update failed because the old backup hash does not match what we previously had in the DB. + * Client attempted to write an outdated copy of the backup. */ - SYNC_DB_OLD_BACKUP_MISMATCH = -4, + SYNC_DB_OUTDATED_WRITE = -6, + + /** + * Client attempted to write a resource that does not exist on the server. + */ + SYNC_DB_TARGET_MISSING = -5, /** * Account is unpaid / does not exist. diff --git a/src/include/sync/sync-database/create_tables.h b/src/include/sync/sync-database/create_tables.h @@ -14,7 +14,7 @@ Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_create_tables.h + * @file include/sync/sync-database/create_tables.h * @brief create sync database tables * @author Christian Grothoff */ diff --git a/src/include/sync/sync-database/delete_block_TR.h b/src/include/sync/sync-database/delete_block_TR.h @@ -0,0 +1,72 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/delete_block_TR.h + * @brief delete a block in the DLL + * @author Iván Ávalos + */ +#ifndef SYNC_DATABASE_DELETE_BLOCK_TR_H +#define SYNC_DATABASE_DELETE_BLOCK_TR_H + +#include <gnunet/gnunet_util_lib.h> +#include "sync/sync_service.h" + +/** + * Delete a block in the DLL. + * + * The deletion relinks both neighbours, so @a prev_upload_sig and + * @a next_upload_sig are the neighbours' new signatures (covering their + * new links). Those signatures were verified against the neighbours as + * they were read outside the transaction, so the transaction re-checks + * the stored neighbours against @a prev_hash and @a prev_prev_nonce, + * resp. @a next_hash and @a next_next_nonce, before storing them. + * + * @param account_pub account to store block under + * @param block_nonce nonce of the block to delete + * @param prev_nonce nonce of the preceding block to @a block_nonce + * @param next_nonce nonce of the succeeding block to @a block_nonce + * @param block_hash hash of the block to delete + * @param refs_length number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by the block + * @param object_incs array of refcount increments for @a object_uids + * @param prev_upload_sig new signature of the preceding block, NULL if none + * @param prev_hash current hash of the preceding block, NULL if none + * @param prev_prev_nonce nonce preceding the preceding block as covered + * by @a prev_upload_sig, NULL if there is none + * @param next_upload_sig new signature of the succeeding block, NULL if none + * @param next_hash current hash of the succeeding block, NULL if none + * @param next_next_nonce nonce succeeding the succeeding block as covered + * by @a next_upload_sig, NULL if there is none + * @return transaction status + */ +enum SYNC_DB_QueryStatus +SYNCDB_delete_block_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *block_nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *block_hash, + unsigned int refs_length, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_AccountSignatureP *prev_upload_sig, + const struct GNUNET_HashCode *prev_hash, + const struct SYNC_BlockNonce *prev_prev_nonce, + const struct SYNC_AccountSignatureP *next_upload_sig, + const struct GNUNET_HashCode *next_hash, + const struct SYNC_BlockNonce *next_next_nonce); + +#endif diff --git a/src/include/sync/sync-database/drop_tables.h b/src/include/sync/sync-database/drop_tables.h @@ -14,7 +14,7 @@ Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_drop_tables.h + * @file include/sync/sync-database/drop_tables.h * @brief drop sync tables * @author Christian Grothoff */ diff --git a/src/include/sync/sync-database/gc.h b/src/include/sync/sync-database/gc.h @@ -14,7 +14,7 @@ Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_gc.h + * @file include/sync/sync-database/gc.h * @brief garbage collection for sync database * @author Christian Grothoff */ @@ -27,17 +27,21 @@ /** * Function called to perform "garbage collection" on the * database, expiring records we no longer require. Deletes - * all user records that are not paid up (and by cascade deletes - * the associated recovery documents). Also deletes expired - * truth and financial records older than @a fin_expire. + * accounts that are no longer paid up (and by cascade their + * blocks and objects), stale pending payments and unreferenced + * expired objects. * - * @param expire_backups backups older than the given time stamp should be garbage collected + * @param expire_backups backups older than the given time stamp + * should be garbage collected * @param expire_pending_payments payments still pending from since before - * this value should be garbage collected - * @return transaction status + * this value should be garbage collected + * @param expire_objects unreferenced objects that expired before this + * value should be garbage collected + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure */ -enum GNUNET_DB_QueryStatus -SYNCDB_gc (struct GNUNET_TIME_Absolute expire, - struct GNUNET_TIME_Absolute expire_pending_payments); +enum GNUNET_GenericReturnValue +SYNCDB_gc (struct GNUNET_TIME_Absolute expire_backups, + struct GNUNET_TIME_Absolute expire_pending_payments, + struct GNUNET_TIME_Absolute expire_objects); #endif diff --git a/src/include/sync/sync-database/increment_lifetime_TR.h b/src/include/sync/sync-database/increment_lifetime_TR.h @@ -14,7 +14,7 @@ Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_increment_lifetime_TR.h + * @file include/sync/sync-database/increment_lifetime_TR.h * @brief increment account lifetime after payment * @author Christian Grothoff */ diff --git a/src/include/sync/sync-database/lookup_account_TR.h b/src/include/sync/sync-database/lookup_account_TR.h @@ -1,21 +1,21 @@ /* - This file is part of GNU Taler - Copyright (C) 2019, 2021 Taler Systems SA + This file is part of TALER + (C) 2026 Taler Systems SA - Taler is free software; you can redistribute it and/or modify it under the + TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. - Taler is distributed in the hope that it will be useful, but WITHOUT ANY + TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with - Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_lookup_account_TR.h - * @brief lookup account and backup metadata + * @file include/sync/sync-database/lookup_account_TR.h + * @brief look up the status of an account * @author Christian Grothoff */ #ifndef SYNC_DATABASE_LOOKUP_ACCOUNT_TR_H @@ -23,18 +23,27 @@ #include <gnunet/gnunet_util_lib.h> #include "sync/sync_service.h" -#include "sync/sync-database/common.h" /** - * Lookup an account and associated backup meta data. + * Look up the status of an account. + * + * Unlike every other entry point, this one does not fail an expired + * account: telling the client *when* the account expired is the whole + * point, so the caller gets the stored date and decides. Only an + * account that does not exist at all is #SYNC_DB_TARGET_MISSING. * * @param account_pub account to look up - * @param[out] backup_hash set to hash of backup - * @return transaction status + * @param[out] expiration_date when the account expires (or expired) + * @param[out] storage_used_bytes total size of the account's blocks + * @param[out] block_count number of blocks in the account's backup + * @return transaction status, #SYNC_DB_TARGET_MISSING if there is no + * such account */ enum SYNC_DB_QueryStatus SYNCDB_lookup_account_TR ( const struct SYNC_AccountPublicKeyP *account_pub, - struct GNUNET_HashCode *backup_hash); + struct GNUNET_TIME_Timestamp *expiration_date, + uint64_t *storage_used_bytes, + uint64_t *block_count); #endif diff --git a/src/include/sync/sync-database/lookup_backup_TR.h b/src/include/sync/sync-database/lookup_backup_TR.h @@ -1,48 +0,0 @@ -/* - This file is part of GNU Taler - Copyright (C) 2019, 2021 Taler Systems SA - - Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file include/sync_database_lookup_backup_TR.h - * @brief obtain backup data - * @author Christian Grothoff - */ -#ifndef SYNC_DATABASE_LOOKUP_BACKUP_TR_H -#define SYNC_DATABASE_LOOKUP_BACKUP_TR_H - -#include <gnunet/gnunet_util_lib.h> -#include "sync/sync_service.h" -#include "sync/sync-database/common.h" - -/** - * Obtain backup. - * - * @param account_pub account to obtain backup for - * @param[out] account_sig set to signature affirming storage request - * @param[out] prev_hash set to hash of the previous backup (all zeros if none) - * @param[out] backup_hash set to hash of backup - * @param[out] backup_size set to number of bytes in backup - * @param[out] backup set to raw data to backup, caller MUST FREE - * @return transaction status - */ -enum SYNC_DB_QueryStatus -SYNCDB_lookup_backup_TR ( - const struct SYNC_AccountPublicKeyP *account_pub, - struct SYNC_AccountSignatureP *account_sig, - struct GNUNET_HashCode *prev_hash, - struct GNUNET_HashCode *backup_hash, - size_t *backup_size, - void **backup); - -#endif diff --git a/src/include/sync/sync-database/lookup_block_TR.h b/src/include/sync/sync-database/lookup_block_TR.h @@ -0,0 +1,52 @@ +/* + This file is part of GNU Taler + Copyright (C) 2026 Taler Systems SA + + Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/lookup_block_TR.h + * @brief lookup a single block by nonce + * @author Iván Ávalos + */ +#ifndef SYNC_DATABASE_LOOKUP_BLOCK_TR_H +#define SYNC_DATABASE_LOOKUP_BLOCK_TR_H + +#include "sync/sync_service.h" + + +/** + * Lookup a single block by @a nonce. + * + * @param account_pub account to look up in + * @param nonce nonce of the block to fetch + * @param[out] block_hash set to the block hash (may be NULL) + * @param[out] prev_nonce set to the previous block nonce (may be NULL) + * @param[out] next_nonce set to the next block nonce (may be NULL) + * @param[out] data_size set to the number of bytes in the data (may be NULL) + * @param[out] is_first set to true if this block is the first in the backup + * @param[out] is_last set to true if this block is the last in the backup + * @return query status (see GNUNET_DB_QueryStatus) + */ +enum SYNC_DB_QueryStatus +SYNCDB_lookup_block_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *nonce, + struct GNUNET_HashCode *block_hash, + struct SYNC_BlockNonce *prev_nonce, + struct SYNC_BlockNonce *next_nonce, + size_t *data_size, + struct GNUNET_HashCode *refs_hash, + bool *is_first, + bool *is_last); + +#endif diff --git a/src/include/sync/sync-database/lookup_block_range_TR.h b/src/include/sync/sync-database/lookup_block_range_TR.h @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + Copyright (C) 2026 Taler Systems SA + + Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/lookup_block_range_TR.h + * @brief lookup a limited range of blocks starting at a nonce + * @author Iván Ávalos + */ +#ifndef SYNC_DATABASE_LOOKUP_BLOCK_RANGE_TR_H +#define SYNC_DATABASE_LOOKUP_BLOCK_RANGE_TR_H + +#include "sync/sync_service.h" + + +/** + * Iterator called for every block returned by the query. + * + * @param cls closure pointer + * @param nonce nonce identifying the block + * @param block_hash hash of the block data + * @param prev_nonce nonce of the block preceding @a nonce + * @param next_nonce nonce of the block succeeding @a nonce + * @param data_size number of bytes in @a data + * @param data pointer to block data (may be NULL) + */ +typedef void +(*SYNC_DB_BlockIterator)(void *cls, + const struct SYNC_BlockNonce *nonce, + const struct GNUNET_HashCode *block_hash, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + size_t data_size, + const void *data, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *refs_hash); + + +/** + * Lookup a (limited) range of blocks starting at @a start_nonce. + * + * @param start_nonce start of the range (inclusive) + * @param limit maximum number of blocks to return + * @param it iterator to call for each returned block + * @param it_cls closure for @a it + * @return query status (see GNUNET_DB_QueryStatus) + */ +enum SYNC_DB_QueryStatus +SYNCDB_lookup_block_range_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *start_nonce, + const int16_t limit, + SYNC_DB_BlockIterator it, + void *it_cls); + +#endif diff --git a/src/include/sync/sync-database/lookup_object.h b/src/include/sync/sync-database/lookup_object.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/lookup_object.h + * @brief lookup object procedure wrapper + * @author Iván Ávalos + */ +#ifndef SYNC_DATABASE_LOOKUP_OBJECT_TR_H +#define SYNC_DATABASE_LOOKUP_OBJECT_TR_H + +#include <gnunet/gnunet_util_lib.h> +#include "sync/sync_service.h" + +/** + * Lookup an object blob for an account. + * + * @param account_pub account to look up + * @param object_hash object identifier hash + * @param[out] refcount object reference count + * @param[out] expiration_date expiration timestamp + * @param[out] data_size size of object data + * @param[out] data buffer to receive object data + * @return transaction status + */ +enum SYNC_DB_QueryStatus +SYNCDB_lookup_object ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct GNUNET_HashCode *object_hash, + int64_t *refcount, + struct GNUNET_TIME_Timestamp *expiration_date, + size_t *data_size, + void *data); + +#endif diff --git a/src/include/sync/sync-database/lookup_object_uid.h b/src/include/sync/sync-database/lookup_object_uid.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/lookup_object_uid.h + * @brief lookup object by UID procedure wrapper + * @author Iván Ávalos + */ +#ifndef SYNC_DATABASE_LOOKUP_OBJECT_UID_H +#define SYNC_DATABASE_LOOKUP_OBJECT_UID_H + +#include <gnunet/gnunet_util_lib.h> +#include "sync/sync_service.h" + +/** + * Lookup an object blob for an account by its UID. + * + * @param account_pub account to look up + * @param uid object UID to look up + * @param[out] object_hash hash of the object data + * @param[out] refcount object reference count + * @param[out] data_size size of object data + * @param[out] data buffer to receive object data (allocated by callee) + * @return transaction status + */ +enum SYNC_DB_QueryStatus +SYNCDB_lookup_object_uid ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_ObjectUID *uid, + struct GNUNET_HashCode *object_hash, + int64_t *refcount, + size_t *data_size, + void *data); + +#endif diff --git a/src/include/sync/sync-database/lookup_pending_payments_by_account.h b/src/include/sync/sync-database/lookup_pending_payments_by_account.h @@ -0,0 +1,62 @@ +/* + This file is part of GNU Taler + Copyright (C) 2019, 2021 Taler Systems SA + + Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/lookup_pending_payments_by_account.h + * @brief lookup pending payments by account + * @author Christian Grothoff + */ +#ifndef SYNC_DATABASE_LOOKUP_PENDING_PAYMENTS_BY_ACCOUNT_TR_H +#define SYNC_DATABASE_LOOKUP_PENDING_PAYMENTS_BY_ACCOUNT_TR_H + +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_db_lib.h> +#include <taler/taler_util.h> +#include "sync/sync_service.h" + + +/** + * Function called on all pending payments for an account. + * + * @param cls closure + * @param timestamp for how long have we been waiting + * @param order_id order id in the backend + * @param token claim token, or NULL for none + * @param amount how much is the order for + */ +typedef void +(*SYNC_DB_PaymentPendingIterator)(void *cls, + struct GNUNET_TIME_Timestamp timestamp, + const char *order_id, + const struct TALER_ClaimTokenP *token, + const struct TALER_Amount *amount); + + +/** + * Lookup pending payments by account. + * + * @param account_pub account to look for pending payments under + * @param it iterator to call on all pending payments + * @param it_cls closure for @a it + * @return transaction status, on success the number of + * pending payments found + */ +enum SYNC_DB_QueryStatus +SYNCDB_lookup_pending_payments_by_account_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + SYNC_DB_PaymentPendingIterator it, + void *it_cls); + +#endif diff --git a/src/include/sync/sync-database/lookup_pending_payments_by_account_TR.h b/src/include/sync/sync-database/lookup_pending_payments_by_account_TR.h @@ -1,60 +0,0 @@ -/* - This file is part of GNU Taler - Copyright (C) 2019, 2021 Taler Systems SA - - Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file include/sync_database_lookup_pending_payments_by_account_TR.h - * @brief lookup pending payments by account - * @author Christian Grothoff - */ -#ifndef SYNC_DATABASE_LOOKUP_PENDING_PAYMENTS_BY_ACCOUNT_TR_H -#define SYNC_DATABASE_LOOKUP_PENDING_PAYMENTS_BY_ACCOUNT_TR_H - -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_db_lib.h> -#include "sync/sync_service.h" - - -/** - * Function called on all pending payments for an account. - * - * @param cls closure - * @param timestamp for how long have we been waiting - * @param order_id order id in the backend - * @param token claim token, or NULL for none - * @param amount how much is the order for - */ -typedef void -(*SYNC_DB_PaymentPendingIterator)(void *cls, - struct GNUNET_TIME_Timestamp timestamp, - const char *order_id, - const struct TALER_ClaimTokenP *token, - const struct TALER_Amount *amount); - - -/** - * Lookup pending payments by account. - * - * @param account_pub account to look for pending payments under - * @param it iterator to call on all pending payments - * @param it_cls closure for @a it - * @return transaction status - */ -enum GNUNET_DB_QueryStatus -SYNCDB_lookup_pending_payments_by_account_TR ( - const struct SYNC_AccountPublicKeyP *account_pub, - SYNC_DB_PaymentPendingIterator it, - void *it_cls); - -#endif diff --git a/src/include/sync/sync-database/meson.build b/src/include/sync/sync-database/meson.build @@ -1,15 +1,19 @@ install_data( - 'create_tables.h', 'common.h', 'drop_tables.h', + 'create_tables.h', + 'preflight.h', 'gc.h', + 'append_block_TR.h', + 'delete_block_TR.h', + 'update_block_TR.h', 'increment_lifetime_TR.h', - 'lookup_account_TR.h', - 'lookup_backup_TR.h', - 'lookup_pending_payments_by_account_TR.h', - 'preflight.h', - 'store_backup_TR.h', + 'lookup_block_range_TR.h', + 'lookup_block_TR.h', + 'lookup_object.h', + 'lookup_object_uid.h', + 'lookup_pending_payments_by_account.h', 'store_payment_TR.h', - 'update_backup_TR.h', + 'store_object_TR.h', install_dir: get_option('includedir') / 'sync' / 'sync-database', ) diff --git a/src/include/sync/sync-database/preflight.h b/src/include/sync/sync-database/preflight.h @@ -14,7 +14,7 @@ Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_preflight.h + * @file include/sync/sync-database/preflight.h * @brief preflight check for sync database * @author Christian Grothoff */ diff --git a/src/include/sync/sync-database/store_backup_TR.h b/src/include/sync/sync-database/store_backup_TR.h @@ -1,48 +0,0 @@ -/* - This file is part of GNU Taler - Copyright (C) 2019, 2021 Taler Systems SA - - Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file include/sync_database_store_backup_TR.h - * @brief store first backup - * @author Christian Grothoff - */ -#ifndef SYNC_DATABASE_STORE_BACKUP_TR_H -#define SYNC_DATABASE_STORE_BACKUP_TR_H - -#include <gnunet/gnunet_util_lib.h> -#include "sync/sync_service.h" -#include "sync/sync-database/common.h" - -/** - * Store backup. Only applicable for the FIRST backup under - * an @a account_pub. Use @e SYNCDB_update_backup_TR to update an - * existing backup. - * - * @param account_pub account to store @a backup under - * @param account_sig signature affirming storage request - * @param backup_hash hash of @a backup - * @param backup_size number of bytes in @a backup - * @param backup raw data to backup - * @return transaction status - */ -enum SYNC_DB_QueryStatus -SYNCDB_store_backup_TR ( - const struct SYNC_AccountPublicKeyP *account_pub, - const struct SYNC_AccountSignatureP *account_sig, - const struct GNUNET_HashCode *backup_hash, - size_t backup_size, - const void *backup); - -#endif diff --git a/src/include/sync/sync-database/store_object_TR.h b/src/include/sync/sync-database/store_object_TR.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/store_object_TR.h + * @brief store object procedure wrapper + * @author Iván Ávalos + */ +#ifndef SYNC_DATABASE_STORE_OBJECT_TR_H +#define SYNC_DATABASE_STORE_OBJECT_TR_H + +#include <gnunet/gnunet_util_lib.h> +#include "sync/sync_service.h" + +/** + * Store an object blob for an account. + * + * @param uid unique identifier for the object + * @param account_pub account to store object under + * @param object_hash hash of @a data + * @param lifetime relative lifetime for the object + * @param data_size size of object data + * @param data pointer to object data + * @return transaction status + */ +enum SYNC_DB_QueryStatus +SYNCDB_store_object_TR ( + const struct SYNC_ObjectUID *uid, + const struct SYNC_AccountPublicKeyP *account_pub, + const struct GNUNET_HashCode *object_hash, + struct GNUNET_TIME_Relative lifetime, + size_t data_size, + const void *data); + +#endif diff --git a/src/include/sync/sync-database/store_payment_TR.h b/src/include/sync/sync-database/store_payment_TR.h @@ -14,7 +14,7 @@ Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_store_payment_TR.h + * @file include/sync/sync-database/store_payment_TR.h * @brief store payment for sync database * @author Christian Grothoff */ @@ -35,6 +35,7 @@ * @param order_id order we created * @param token claim token, or NULL for none * @param amount how much we asked for + * @param lifetime how long the account will be valid for * @return transaction status */ enum SYNC_DB_QueryStatus @@ -42,6 +43,7 @@ SYNCDB_store_payment_TR ( const struct SYNC_AccountPublicKeyP *account_pub, const char *order_id, const struct TALER_ClaimTokenP *token, - const struct TALER_Amount *amount); + const struct TALER_Amount *amount, + struct GNUNET_TIME_Relative lifetime); #endif diff --git a/src/include/sync/sync-database/update_backup_TR.h b/src/include/sync/sync-database/update_backup_TR.h @@ -1,48 +0,0 @@ -/* - This file is part of GNU Taler - Copyright (C) 2019, 2021 Taler Systems SA - - Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file include/sync_database_update_backup_TR.h - * @brief update existing backup - * @author Christian Grothoff - */ -#ifndef SYNC_DATABASE_UPDATE_BACKUP_TR_H -#define SYNC_DATABASE_UPDATE_BACKUP_TR_H - -#include <gnunet/gnunet_util_lib.h> -#include "sync/sync_service.h" -#include "sync/sync-database/common.h" - -/** - * Update backup. - * - * @param account_pub account to store @a backup under - * @param old_backup_hash hash of the previous backup (must match) - * @param account_sig signature affirming storage request - * @param backup_hash hash of @a backup - * @param backup_size number of bytes in @a backup - * @param backup raw data to backup - * @return transaction status - */ -enum SYNC_DB_QueryStatus -SYNCDB_update_backup_TR ( - const struct SYNC_AccountPublicKeyP *account_pub, - const struct GNUNET_HashCode *old_backup_hash, - const struct SYNC_AccountSignatureP *account_sig, - const struct GNUNET_HashCode *backup_hash, - size_t backup_size, - const void *backup); - -#endif diff --git a/src/include/sync/sync-database/update_block_TR.h b/src/include/sync/sync-database/update_block_TR.h @@ -0,0 +1,61 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync-database/update_block_TR.h + * @brief update contents of a block in the DLL + * @author Iván Ávalos + */ +#ifndef SYNC_DATABASE_UPDATE_BLOCK_TR_H +#define SYNC_DATABASE_UPDATE_BLOCK_TR_H + +#include <gnunet/gnunet_util_lib.h> +#include "sync/sync_service.h" + +/** + * Update the contents of a block in the DLL. + * + * @param account_pub account under which block is stored + * @param block_nonce nonce of the block to update + * @param prev_nonce nonce of the preceding block to @a block_nonce + * @param next_nonce nonce of the succeeding block to @a block_nonce + * @param old_data_hash hash of the previous raw data + * @param new_data_hash hash of @a data + * @param data_size number of bytes in @a data + * @param data raw data of the block to update + * @param refs_length number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by the block + * @param object_incs array of refcount increments for @a object_uids + * @param upload_sig verified signature of the update + * @param refs_hash hash over the object references + * @return transaction status + */ +enum SYNC_DB_QueryStatus +SYNCDB_update_block_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *block_nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_data_hash, + const struct GNUNET_HashCode *new_data_hash, + size_t data_size, + const void *data, + unsigned int refs_length, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *refs_hash); + +#endif diff --git a/src/include/sync/sync_database_lib.h b/src/include/sync/sync_database_lib.h @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_database_lib.h + * @file include/sync/sync_database_lib.h * @brief sync database library interface */ #ifndef SYNC_DB_LIB_H @@ -25,26 +25,40 @@ #include <sync/sync-database/create_tables.h> #include <sync/sync-database/preflight.h> #include <sync/sync-database/gc.h> -#include <sync/sync-database/store_payment_TR.h> -#include <sync/sync-database/lookup_pending_payments_by_account_TR.h> -#include <sync/sync-database/store_backup_TR.h> -#include <sync/sync-database/update_backup_TR.h> -#include <sync/sync-database/lookup_account_TR.h> -#include <sync/sync-database/lookup_backup_TR.h> +#include <sync/sync-database/append_block_TR.h> +#include <sync/sync-database/delete_block_TR.h> +#include <sync/sync-database/update_block_TR.h> #include <sync/sync-database/increment_lifetime_TR.h> +#include <sync/sync-database/lookup_block_range_TR.h> +#include <sync/sync-database/lookup_account_TR.h> +#include <sync/sync-database/lookup_block_TR.h> +#include <sync/sync-database/lookup_object.h> +#include <sync/sync-database/lookup_object_uid.h> +#include <sync/sync-database/lookup_pending_payments_by_account.h> +#include <sync/sync-database/store_payment_TR.h> +#include <sync/sync-database/store_object_TR.h> /** * Initialize the sync database subsystem. * * @param cfg configuration to use - * @param skip_preflight true if we should skip the usual - * preflight check which assures us that the DB is actually - * operational; only sync-dbinit should use true here. * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure */ enum GNUNET_GenericReturnValue -SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg, - bool skip_preflight); +SYNCDB_init ( + const struct GNUNET_CONFIGURATION_Handle *cfg); + + +/** + * Initialize the sync database subsystem for administration. + * Disables the check that the database schema is current. + * + * @param cfg configuration to use + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ +enum GNUNET_GenericReturnValue +SYNCDB_init_admin ( + const struct GNUNET_CONFIGURATION_Handle *cfg); /** diff --git a/src/include/sync/sync_service.h b/src/include/sync/sync_service.h @@ -14,7 +14,7 @@ Anastasis; see the file COPYING.LIB. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_service.h + * @file include/sync/sync_service.h * @brief C interface of libsync, a C library to use sync's HTTP API * @author Christian Grothoff */ @@ -27,9 +27,6 @@ #include <jansson.h> -GNUNET_NETWORK_STRUCT_BEGIN - - /** * Private key identifying an account. */ @@ -53,356 +50,502 @@ struct SYNC_AccountPublicKeyP struct GNUNET_CRYPTO_EddsaPublicKey eddsa_pub; }; - /** - * Data signed by the account public key of a sync client to - * authorize the upload of the backup. + * Signature made with an account's public key. */ -struct SYNC_UploadSignaturePS +struct SYNC_AccountSignatureP { /** - * Set to #TALER_SIGNATURE_SYNC_BACKUP_UPLOAD. - */ - struct GNUNET_CRYPTO_SignaturePurpose purpose; - - /** - * Hash of the previous backup, all zeros for none. - */ - struct GNUNET_HashCode old_backup_hash GNUNET_PACKED; - - /** - * Hash of the new backup. + * We use EdDSA. */ - struct GNUNET_HashCode new_backup_hash GNUNET_PACKED; - + struct GNUNET_CRYPTO_EddsaSignature eddsa_sig; }; +/** + * Nonce used to identify a block and derive its encryption key. + */ +struct SYNC_BlockNonce +{ + unsigned char nonce[24]; +}; /** - * Signature made with an account's public key. + * Hash of the encryption key used to encrypt the object. */ -struct SYNC_AccountSignatureP +struct SYNC_ObjectUID { - /** - * We use EdDSA. - */ - struct GNUNET_CRYPTO_EddsaSignature eddsa_sig; + unsigned char uid[64]; }; -GNUNET_NETWORK_STRUCT_END + +/* ********************* Block list API *********************** */ /** - * High-level ways how an upload may conclude. + * One entry returned by the block list. */ -enum SYNC_UploadStatus +struct SYNC_BlockListEntry { + + /** + * Nonce identifying this block. + */ + struct SYNC_BlockNonce nonce; + + /** + * Hash of the block data. + */ + struct GNUNET_HashCode hash; + + /** + * Number of bytes in @e data. + */ + size_t data_size; + /** - * Backup was successfully made. + * Block data, allocated by the parser. */ - SYNC_US_SUCCESS = 0, + void *data; /** - * Account expired or payment was explicitly requested - * by the client. + * Nonce of the preceding block (may be all zeros if + * @e have_prev_nonce is false). */ - SYNC_US_PAYMENT_REQUIRED = 1, + struct SYNC_BlockNonce prev_nonce; /** - * Conflicting backup existed on server. Client should - * reconcile and try again with (using the provided - * recovered backup as the previous backup). + * True if @e prev_nonce is set. */ - SYNC_US_CONFLICTING_BACKUP = 2, + bool have_prev_nonce; /** - * HTTP interaction failed, see HTTP status. + * Nonce of the succeeding block (may be all zeros if + * @e have_next_nonce is false). */ - SYNC_US_HTTP_ERROR = 3, + struct SYNC_BlockNonce next_nonce; /** - * We had an internal error (not sure this can happen, - * but reserved for HTTP 400 status codes). + * True if @e next_nonce is set. */ - SYNC_US_CLIENT_ERROR = 4, + bool have_next_nonce; /** - * Server had an internal error. + * Upload signature stored with the block; the wallet verifies the + * backup against it before applying. */ - SYNC_US_SERVER_ERROR = 5 + struct SYNC_AccountSignatureP upload_sig; + + /** + * Hash of the block data this block replaced, part of the signed + * context (all zeros for the first upload of the block). + */ + struct GNUNET_HashCode old_hash; + + /** + * Hash over the object references covered by the stored signature. + */ + struct GNUNET_HashCode refs_hash; + }; /** - * Result of an upload. + * Result of a block list operation. */ -struct SYNC_UploadDetails +struct SYNC_BlockListDetails { /** + * HTTP status code. + */ + unsigned int http_status; + + /** * Taler error code. */ enum TALER_ErrorCode ec; /** - * HTTP status of the request. + * Number of entries in @e entries. */ - unsigned int http_status; + unsigned int num_entries; + + /** + * Array of block list entries (may be freed by the + * caller after copying). + */ + struct SYNC_BlockListEntry *entries; + +}; + + +/** + * Function called with the results of a #SYNC_block_list(). + * + * @param cls closure + * @param ld block list details + */ +typedef void +(*SYNC_BlockListCallback)(void *cls, + const struct SYNC_BlockListDetails *ld); + + +/** + * Handle for a block list operation. + */ +struct SYNC_BlockListOperation; + + +/** + * List blocks for an account, starting from @a start_nonce. + * + * @param ctx for HTTP client request processing + * @param base_url base URL of the Sync server + * @param pub account public key + * @param limit maximum number of blocks to return + * @param start_nonce optional nonce to start listing from, + * NULL to start from the beginning + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle for the operation + */ +struct SYNC_BlockListOperation * +SYNC_block_list (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPublicKeyP *pub, + int16_t limit, + const struct SYNC_BlockNonce *start_nonce, + SYNC_BlockListCallback cb, + void *cb_cls); + + +/** + * Cancel a block list operation. + * + * @param[in] op operation to cancel + */ +void +SYNC_block_list_cancel (struct SYNC_BlockListOperation *op); + + +/* ********************* Block operation API *********************** */ + + +/** + * High-level status of a block operation. + */ +enum SYNC_BlockOperationStatus +{ /** - * High level status of the upload operation. + * Block was successfully uploaded (201 Created). */ - enum SYNC_UploadStatus us; + SYNC_BOS_SUCCESS = 0, /** - * Details depending on @e us. + * Block already exists (304 Not Modified). */ - union - { + SYNC_BOS_NOT_MODIFIED = 1, - /** - * Data returned if @e us is #SYNC_US_SUCCESS. - */ - struct - { + /** + * Payment required (402 Payment Required). + */ + SYNC_BOS_PAYMENT_REQUIRED = 2, + + /** + * HTTP 4xx error (but not 402). + */ + SYNC_BOS_CLIENT_ERROR = 3, - /** - * Hash of the synchronized backup. - */ - const struct GNUNET_HashCode *curr_backup_hash; + /** + * HTTP 5xx error. + */ + SYNC_BOS_SERVER_ERROR = 4, - } success; + /** + * Other HTTP error. + */ + SYNC_BOS_HTTP_ERROR = 5 - /** - * Previous backup. Returned if @e us is - * #SYNC_US_CONFLICTING_BACKUP - */ - struct - { - /** - * Hash over @e existing_backup. - */ - struct GNUNET_HashCode existing_backup_hash; +}; - /** - * Number of bytes in @e existing_backup. - */ - size_t existing_backup_size; - /** - * The backup on the server, which does not match the - * "previous" backup expected by the client and thus - * needs to be decrypted, reconciled and re-uploaded. - */ - const void *existing_backup; +/** + * Result of a block operation. + */ +struct SYNC_BlockOperationDetails +{ - } recovered_backup; + /** + * HTTP status code. + */ + unsigned int http_status; - struct - { - /** - * A taler://pay/-URI with a request to pay the annual fee for - * the service. Returned if @e us is #SYNC_US_PAYMENT_REQUIRED. - */ - const char *payment_request; + /** + * Taler error code. + */ + enum TALER_ErrorCode ec; - } payment_required; + /** + * High level status. + */ + enum SYNC_BlockOperationStatus bos; - } details; + /** + * Payment URI, set if @e bos is #SYNC_BOS_PAYMENT_REQUIRED. + */ + const char *pay_uri; }; /** - * Function called with the results of a #SYNC_upload(). + * Function called with the results of a block operation. * * @param cls closure - * @param ud details about the upload operation + * @param ud details about the operation */ typedef void -(*SYNC_UploadCallback)(void *cls, - const struct SYNC_UploadDetails *ud); +(*SYNC_BlockOperationCallback)(void *cls, + const struct SYNC_BlockOperationDetails *ud); /** - * Handle for an upload operatoin. + * Handle for a block operation. */ -struct SYNC_UploadOperation; +struct SYNC_BlockOperation; + /** - * Options for payment. + * Parameters of a block append/update/delete operation, including the + * relink signatures of the relinked neighbours. */ -enum SYNC_PaymentOptions +struct SYNC_BlockOperationArgs { + /** - * No special options. + * Nonce of the block being written or deleted. */ - SYNC_PO_NONE = 0, + const struct SYNC_BlockNonce *nonce; /** - * Trigger payment even if sync does not require it - * yet (forced payment). + * Nonce of the preceding block, NULL for none. */ - SYNC_PO_FORCE_PAYMENT = 1, + const struct SYNC_BlockNonce *prev_nonce; /** - * Request a fresh order to be created, say because the - * existing one was claimed (but not paid) by another wallet. + * Nonce of the succeeding block, NULL for none. */ - SYNC_PO_FRESH_ORDER = 2 + const struct SYNC_BlockNonce *next_nonce; + + /** + * Hash of the existing block data for an update, NULL for an append. + */ + const struct GNUNET_HashCode *old_data_hash; + + /** + * Hash of the block to delete (delete only; also the If-Match value). + */ + const struct GNUNET_HashCode *block_hash; + + /** + * Number of bytes in @e block_data. + */ + size_t block_data_size; + + /** + * The block data. + */ + const void *block_data; + + /** + * Number of entries in @e object_uids and @e object_incs. + */ + size_t refs_len; + + /** + * Array of object UIDs referenced by this block. + */ + const struct SYNC_ObjectUID *object_uids; + + /** + * Array of reference count adjustments. + */ + const int16_t *object_incs; + + /** + * Entry of the block at @e prev_nonce that is relinked by this + * operation (the tail on an append, the preceding neighbour on a + * delete); NULL if there is no such block. Its relink signature is + * computed from this entry. + */ + const struct SYNC_BlockListEntry *prev_entry; + + /** + * Entry of the block at @e next_nonce that is relinked by a delete; + * NULL if there is no such block (or for appends/updates). + */ + const struct SYNC_BlockListEntry *next_entry; + }; + /** - * Upload a @a backup to a Sync server. Note that @a backup must - * have already been compressed, padded and encrypted by the - * client. - * - * While @a pub is theoretically protected by the HTTPS protocol and - * required to access the backup, it should be assumed that an - * adversary might be able to download the backups from the Sync - * server -- or even run the Sync server. Thus, strong encryption - * is essential and NOT implemented by this function. - * - * The use of Anastasis to safely store the Sync encryption keys and - * @a pub is recommended. Storing @a priv in Anastasis depends on - * your priorities: without @a priv, further updates to the backup are - * not possible, and the user would have to pay for another - * account. OTOH, without @a priv an adversary that compromised - * Anastasis can only read the backups, but not alter or destroy them. + * Append a new block to a Sync server (POST). * * @param ctx for HTTP client request processing * @param base_url base URL of the Sync server - * @param priv private key of an account with the server - * @param prev_backup_hash hash of the previous backup, NULL for the first upload ever - * @param backup_size number of bytes in @a backup - * @param backup the encrypted backup, must remain in - * memory until we are done with the operation! - * @param po payment options - * @param paid_order_id ID of the paid order, NULL if no payment was made so far + * @param priv private key of the account + * @param args parameters of the operation; @a args->prev_entry is the + * tail's entry, from which its relink signature is computed * @param cb function to call with the result * @param cb_cls closure for @a cb * @return handle for the operation */ -struct SYNC_UploadOperation * -SYNC_upload (struct GNUNET_CURL_Context *ctx, - const char *base_url, - struct SYNC_AccountPrivateKeyP *priv, - const struct GNUNET_HashCode *prev_backup_hash, - size_t backup_size, - const void *backup, - enum SYNC_PaymentOptions po, - const char *paid_order_id, - SYNC_UploadCallback cb, - void *cb_cls); +struct SYNC_BlockOperation * +SYNC_block_append (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockOperationArgs *args, + SYNC_BlockOperationCallback cb, + void *cb_cls); /** - * Cancel the upload. Note that aborting an upload does NOT guarantee - * that it did not complete, it is possible that the server did - * receive the full request before the upload is aborted. + * Update an existing block on a Sync server (PUT). * - * @param[in] uo operation to cancel. + * @param ctx for HTTP client request processing + * @param base_url base URL of the Sync server + * @param priv private key of the account + * @param args parameters of the operation; @a args->old_data_hash is + * the hash of the existing block data (mandatory) + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle for the operation */ -void -SYNC_upload_cancel (struct SYNC_UploadOperation *uo); +struct SYNC_BlockOperation * +SYNC_block_update (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockOperationArgs *args, + SYNC_BlockOperationCallback cb, + void *cb_cls); /** - * Detailed results from the successful download. + * Delete a block from a Sync server (DELETE). + * + * @param ctx for HTTP client request processing + * @param base_url base URL of the Sync server + * @param priv private key of the account + * @param args parameters of the operation; @a args->block_hash is the + * hash of the block to delete (for the If-Match header), and + * @a args->prev_entry/@a args->next_entry the entries of the + * neighbours being relinked + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle for the operation */ -struct SYNC_DownloadDetails -{ +struct SYNC_BlockOperation * +SYNC_block_delete (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockOperationArgs *args, + SYNC_BlockOperationCallback cb, + void *cb_cls); - /** - * HTTP status code. - */ - unsigned int http_status; - /** - * Details depending on @e http_status. - */ - union - { +/** + * Cancel a block operation. + * + * @param[in] op operation to cancel + */ +void +SYNC_block_operation_cancel (struct SYNC_BlockOperation *op); - /** - * Details if status is #MHD_HTTP_OK. - */ - struct - { - /** - * Signature (already verified). - */ - struct SYNC_AccountSignatureP sig; +/* ********************* Object API *********************** */ - /** - * Hash of the previous version. - */ - struct GNUNET_HashCode prev_backup_hash; - /** - * Hash over @e backup and @e backup_size. - */ - struct GNUNET_HashCode curr_backup_hash; +/** + * Upload an encrypted object to the Sync server (POST). + * + * @param ctx for HTTP client request processing + * @param base_url base URL of the Sync server + * @param priv private key of the account + * @param uid UID for the object + * @param data_size number of bytes in @a data + * @param data the object data + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle for the operation + */ +struct SYNC_BlockOperation * +SYNC_object_upload (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_ObjectUID *uid, + size_t data_size, + const void *data, + SYNC_BlockOperationCallback cb, + void *cb_cls); - /** - * The backup we downloaded. - */ - const void *backup; - /** - * Number of bytes in @e backup. - */ - size_t backup_size; - } ok; +/* ********************* Object fetch API *********************** */ - } details; -}; +/** + * Handle for an object fetch operation. + */ +struct SYNC_ObjectFetchOperation; /** - * Function called with the results of a #SYNC_download(). + * Function called with the results of an object fetch. * * @param cls closure - * @param dd download details + * @param http_status HTTP status code + * @param ec error code (0 on success) + * @param uid UID of the fetched object + * @param data_size number of bytes in @a data + * @param data object data (valid only during the callback) */ typedef void -(*SYNC_DownloadCallback)(void *cls, - const struct SYNC_DownloadDetails *dd); +(*SYNC_ObjectFetchCallback)(void *cls, + unsigned int http_status, + enum TALER_ErrorCode ec, + const struct SYNC_ObjectUID *uid, + size_t data_size, + const void *data); /** - * Handle for a download operation. - */ -struct SYNC_DownloadOperation; - - -/** - * Download the latest version of a backup for account @a pub. + * Fetch an object from the Sync server (GET). * * @param ctx for HTTP client request processing * @param base_url base URL of the Sync server - * @param pub account public key - * @param cb function to call with the backup + * @param pub public key of the account + * @param uid UID of the object to fetch + * @param cb function to call with the result * @param cb_cls closure for @a cb * @return handle for the operation */ -struct SYNC_DownloadOperation * -SYNC_download (struct GNUNET_CURL_Context *ctx, - const char *base_url, - const struct SYNC_AccountPublicKeyP *pub, - SYNC_DownloadCallback cb, - void *cb_cls); +struct SYNC_ObjectFetchOperation * +SYNC_object_fetch (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPublicKeyP *pub, + const struct SYNC_ObjectUID *uid, + SYNC_ObjectFetchCallback cb, + void *cb_cls); /** - * Cancel the download. + * Cancel an object fetch operation. * - * @param[in] download operation to cancel. + * @param[in] op operation to cancel */ void -SYNC_download_cancel (struct SYNC_DownloadOperation *download); +SYNC_object_fetch_cancel (struct SYNC_ObjectFetchOperation *op); #endif /* SYNC_SERVICE_H */ diff --git a/src/include/sync/sync_signatures.h b/src/include/sync/sync_signatures.h @@ -0,0 +1,157 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file include/sync/sync_signatures.h + * @brief Utility functions for sync signatures + * @author Iván Ávalos + */ +#ifndef SYNC_SIGNATURES_H +#define SYNC_SIGNATURES_H + +#include <gnunet/gnunet_util_lib.h> +#include "sync_service.h" + + +/** + * Sign a block upload (append/update). + * + * @param account_priv private key to sign with + * @param nonce nonce identifying the block + * @param prev_nonce nonce of the preceding block, NULL for none + * @param next_nonce nonce of the succeeding block, NULL for none + * @param old_hash hash of existing block data for an update, + * NULL for an append + * @param new_hash hash of the new block data + * @param refs_hash from #SYNC_object_refs_hash() + * @param[out] upload_sig signature to create + */ +void +SYNC_block_upload_sign ( + const struct SYNC_AccountPrivateKeyP *account_priv, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *new_hash, + const struct GNUNET_HashCode *refs_hash, + struct SYNC_AccountSignatureP *upload_sig); + + +/** + * Verify signature on a block upload (append/update). + * + * @param account_pub public key of the account + * @param nonce nonce identifying the block + * @param prev_nonce nonce of the preceding block, NULL for none + * @param next_nonce nonce of the succeeding block, NULL for none + * @param old_hash hash of existing block data for an update, + * NULL for an append + * @param new_hash hash of the new block data + * @param refs_hash from #SYNC_object_refs_hash() + * @param upload_sig signature to verify + * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR otherwise + */ +enum GNUNET_GenericReturnValue +SYNC_block_upload_verify ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *new_hash, + const struct GNUNET_HashCode *refs_hash, + const struct SYNC_AccountSignatureP *upload_sig); + + +/** + * Sign a block deletion. + * + * @param account_priv private key to sign with + * @param nonce nonce of the block to delete + * @param prev_nonce nonce of the preceding block, NULL for none + * @param next_nonce nonce of the succeeding block, NULL for none + * @param hash hash of the block to delete + * @param refs_hash from #SYNC_object_refs_hash() + * @param[out] delete_sig signature to create + */ +void +SYNC_block_delete_sign ( + const struct SYNC_AccountPrivateKeyP *account_priv, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *hash, + const struct GNUNET_HashCode *refs_hash, + struct SYNC_AccountSignatureP *delete_sig); + + +/** + * Verify signature on a block deletion. + * + * @param account_pub public key of the account + * @param nonce nonce of the block to delete + * @param prev_nonce nonce of the preceding block, NULL for none + * @param next_nonce nonce of the succeeding block, NULL for none + * @param hash hash of the block to delete + * @param refs_hash from #SYNC_object_refs_hash() + * @param delete_sig signature to verify + * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR otherwise + */ +enum GNUNET_GenericReturnValue +SYNC_block_delete_verify ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *hash, + const struct GNUNET_HashCode *refs_hash, + const struct SYNC_AccountSignatureP *delete_sig); + + +/** + * Sign an object upload. + * + * @param account_priv private key to sign with + * @param uid UID of the object to upload + * @param hash hash of the object to upload + * @param[out] upload_sig signature to create + */ +void +SYNC_object_upload_sign ( + const struct SYNC_AccountPrivateKeyP *account_priv, + const struct SYNC_ObjectUID *uid, + const struct GNUNET_HashCode *hash, + struct SYNC_AccountSignatureP *upload_sig); + + +/** + * Verify signature on an object upload. + * + * @param account_pub public key of the account + * @param uid UID of the object + * @param hash hash of the object + * @param upload_sig signature to verify + * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR otherwise + */ +enum GNUNET_GenericReturnValue +SYNC_object_upload_verify ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_ObjectUID *uid, + const struct GNUNET_HashCode *hash, + const struct SYNC_AccountSignatureP *upload_sig); + + +#endif diff --git a/src/include/sync/sync_testing_lib.h b/src/include/sync/sync_testing_lib.h @@ -17,7 +17,7 @@ <http://www.gnu.org/licenses/> */ /** - * @file include/sync_testing_lib.h + * @file include/sync/sync_testing_lib.h * @brief API for writing an interpreter to test SYNC components * @author Christian Grothoff <christian@grothoff.org> */ @@ -26,110 +26,190 @@ #include "sync/sync_service.h" #include <gnunet/gnunet_json_lib.h> +#include <stdbool.h> #include <taler/taler_testing_lib.h> -#include <microhttpd.h> /** - * Make the "backup download" command for a non-existent upload. + * Call @a op on all simple traits. + * + * @param op macro to call + */ +#define SYNC_TESTING_SIMPLE_TRAITS(op) \ + op (sync_account_pub, const struct SYNC_AccountPublicKeyP) \ + op (sync_account_priv, const struct SYNC_AccountPrivateKeyP) \ + op (prev_hash, const struct GNUNET_HashCode) \ + op (curr_hash, const struct GNUNET_HashCode) \ + op (block_nonce, const struct SYNC_BlockNonce) \ + op (prev_nonce, const struct SYNC_BlockNonce) \ + op (next_nonce, const struct SYNC_BlockNonce) \ + op (block_list_result, const struct SYNC_BlockListDetails) \ + op (block_refs_hash, const struct GNUNET_HashCode) + + +/* FIXME: eventually switch to libgnunettesting with the SYNC_ prefix for + the symbols instead of TALER_TESTING_! */ +SYNC_TESTING_SIMPLE_TRAITS (TALER_TESTING_MAKE_DECL_SIMPLE_TRAIT) + + +/** + * Make the "block append" command. * * @param label command label * @param sync_url base URL of the sync serving - * the policy store request. + * @param upload_reference reference to a previous command that + * provides account keys, NULL to generate fresh keys + * @param nonce block nonce to use, NULL to generate a random one + * @param http_status expected HTTP status. + * @param block_data data to upload + * @param block_data_size number of bytes in @a block_data + * @param retry_upload #true to retry the upload from @a upload_reference, + * #false to chain a new block after it (prev_nonce ← ref's nonce) + * @param refs_len number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by this block + * @param object_incs array of reference count adjustments * @return the command */ struct TALER_TESTING_Command -SYNC_TESTING_cmd_backup_nx (const char *label, - const char *sync_url); +SYNC_TESTING_cmd_block_append (const char *label, + const char *sync_url, + const char *upload_reference, + const struct SYNC_BlockNonce *nonce, + unsigned int http_status, + const void *block_data, + size_t block_data_size, + bool retry_upload, + unsigned int refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs); /** - * Make the "backup download" command. + * Make the "block update" command. * * @param label command label * @param sync_url base URL of the sync serving - * the policy store request. + * @param upload_reference reference to a previous command that + * provides account keys, NULL to generate fresh keys + * @param nonce block nonce to use, NULL to generate a random one + * @param old_data_hash hash of the existing block data * @param http_status expected HTTP status. - * @param upload_ref reference to upload command + * @param block_data data to upload + * @param block_data_size number of bytes in @a block_data + * @param refs_len number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by this block + * @param object_incs array of reference count adjustments * @return the command */ struct TALER_TESTING_Command -SYNC_TESTING_cmd_backup_download (const char *label, - const char *sync_url, - unsigned int http_status, - const char *upload_ref); +SYNC_TESTING_cmd_block_update (const char *label, + const char *sync_url, + const char *upload_reference, + const struct SYNC_BlockNonce *nonce, + const struct GNUNET_HashCode *old_data_hash, + unsigned int http_status, + const void *block_data, + size_t block_data_size, + unsigned int refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs); /** - * Types of options for performing the upload. Used as a bitmask. + * Make the "block list" command. + * + * @param label command label + * @param sync_url base URL of the sync serving + * @param account_reference reference to a previous command that + * provides the account public key + * @param limit maximum number of blocks to return + * @param start_nonce optional starting nonce, NULL for beginning + * @param http_status expected HTTP status. + * @param num_expected number of expected entries (0 to skip checking) + * @param expected_nonces array of expected nonces (length @a num_expected) + * @param expected_hashes array of expected block hashes + * (length @a num_expected) + * @return the command */ -enum SYNC_TESTING_UploadOption -{ - /** - * Do everything by the book. - */ - SYNC_TESTING_UO_NONE = 0, - - /** - * Use random hash for previous upload instead of correct - * previous hash. - */ - SYNC_TESTING_UO_PREV_HASH_WRONG = 1, - - /** - * Request payment. - */ - SYNC_TESTING_UO_REQUEST_PAYMENT = 2, - - /** - * Reference payment order ID from linked previous upload. - */ - SYNC_TESTING_UO_REFERENCE_ORDER_ID = 4 +struct TALER_TESTING_Command +SYNC_TESTING_cmd_block_list (const char *label, + const char *sync_url, + const char *account_reference, + int16_t limit, + const struct SYNC_BlockNonce *start_nonce, + unsigned int http_status, + unsigned int num_expected, + const struct SYNC_BlockNonce *expected_nonces, + const struct GNUNET_HashCode *expected_hashes); -}; +/** + * Make the "block delete" command. + * + * @param label command label + * @param sync_url base URL of the sync serving + * @param account_reference reference to a previous command that + * provides account keys and block nonce/hash + * @param http_status expected HTTP status + * @param refs_len number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by this block + * @param object_incs array of reference count adjustments + * @return the command + */ +struct TALER_TESTING_Command +SYNC_TESTING_cmd_block_delete (const char *label, + const char *sync_url, + const char *account_reference, + unsigned int http_status, + unsigned int refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs); /** - * Make the "backup upload" command. + * Make the "object upload" command. * * @param label command label * @param sync_url base URL of the sync serving - * the policy store request. - * @param prev_upload reference to a previous upload we are - * supposed to update, NULL for none - * @param last_upload reference to the last upload for the - * same account, used to check result on MHD_HTTP_CONFLICT - * @param uo upload options - * @param http_status expected HTTP status. - * @param backup_data data to upload - * @param backup_data_size number of bytes in @a backup_data + * @param account_reference reference to a previous command that + * provides account keys + * @param uid object UID to use, NULL to generate a random one + * @param http_status expected HTTP status + * @param data object data + * @param data_size number of bytes in @a data * @return the command */ struct TALER_TESTING_Command -SYNC_TESTING_cmd_backup_upload (const char *label, +SYNC_TESTING_cmd_object_upload (const char *label, const char *sync_url, - const char *prev_upload, - const char *last_upload, - enum SYNC_TESTING_UploadOption uo, + const char *account_reference, + const struct SYNC_ObjectUID *uid, unsigned int http_status, - const void *backup_data, - size_t backup_data_size); + const void *data, + size_t data_size); + /** - * Call @a op on all simple traits. + * Make the "object fetch" command. * - * @param op macro to call + * @param label command label + * @param sync_url base URL of the sync serving + * @param account_reference reference to a previous command that + * provides account keys + * @param uid object UID to fetch + * @param http_status expected HTTP status + * @param expected_data expected object data (for verification) + * @param expected_data_size number of bytes in @a expected_data + * @return the command */ -#define SYNC_TESTING_SIMPLE_TRAITS(op) \ - op (sync_account_pub, const struct SYNC_AccountPublicKeyP) \ - op (sync_account_priv, const struct SYNC_AccountPrivateKeyP) \ - op (prev_hash, const struct GNUNET_HashCode) \ - op (curr_hash, const struct GNUNET_HashCode) - +struct TALER_TESTING_Command +SYNC_TESTING_cmd_object_fetch (const char *label, + const char *sync_url, + const char *account_reference, + const struct SYNC_ObjectUID *uid, + unsigned int http_status, + const void *expected_data, + size_t expected_data_size); -/* FIXME: eventually switch to libgnunettesting with the SYNC_ prefix for - the symbols instead of TALER_TESTING_! */ -SYNC_TESTING_SIMPLE_TRAITS (TALER_TESTING_MAKE_DECL_SIMPLE_TRAIT) #endif diff --git a/src/include/sync/sync_util.h b/src/include/sync/sync_util.h @@ -14,14 +14,23 @@ SYNC; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file include/sync_util.h - * @brief Interface for common utility functions + * @file include/sync/sync_util.h + * @brief Interface for common utility functions and JSON + * serialization/parsing of blocks and objects * @author Christian Grothoff + * @author Iván Ávalos */ #ifndef SYNC_UTIL_H #define SYNC_UTIL_H #include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include "sync_service.h" + + +/* ******************** Utility ************************ */ + /** * Return default project data used by Sync. @@ -30,4 +39,201 @@ const struct GNUNET_OS_ProjectData * SYNC_project_data (void); -#endif +/* ******************** Block entry ************************ */ + + +/** + * Generate a random block nonce. + * + * @param[out] nonce set to a fresh random nonce + */ +void +SYNC_block_nonce_generate ( + struct SYNC_BlockNonce *nonce); + + +/** + * Parse a single block entry from a JSON object + * (as returned in GET /backups/$KEY/blocks responses). + * + * @param input JSON object to parse + * @param[out] entry where to write the data (pointers point into @a input) + * @return #GNUNET_OK upon successful parsing, #GNUNET_SYSERR upon error + */ +enum GNUNET_GenericReturnValue +SYNC_block_entry_parse ( + json_t *input, + struct SYNC_BlockListEntry *entry); + + +/** + * Free data allocated by #SYNC_block_entry_parse(). + * + * @param[in] entry entry to clear + */ +void +SYNC_block_entry_clear ( + struct SYNC_BlockListEntry *entry); + + +/** + * Serialize a block entry into a JSON object + * (for building GET /backups/$KEY/blocks responses). + * + * @param nonce nonce identifyng the block + * @param block_hash hash of the block data + * @param prev_nonce nonce of the preceding block, or NULL + * @param next_nonce nonce of the succeeding block, or NULL + * @param data_size number of bytes in @a data + * @param data block data + * @param upload_sig signature stored with the block + * @param old_hash hash of the replaced block data (zeros for appends) + * @param refs_hash hash over the object references + * @return JSON object representing the entry + */ +json_t * +SYNC_block_entry_serialize ( + const struct SYNC_BlockNonce *nonce, + const struct GNUNET_HashCode *block_hash, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + size_t data_size, + const void *data, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *refs_hash); + + +/* ************************ Object entry ************************ */ + + +/** + * One object entry as returned in object responses. + */ +struct SYNC_ObjectEntry +{ + /** + * UID identifying the object. + */ + struct SYNC_ObjectUID uid; + + /** + * Number of bytes in @e data. + */ + size_t data_size; + + /** + * Object data, allocated by the parser. + */ + void *data; +}; + + +/** + * Parse a single object entry from a JSON object + * (as returned in GET /backups/$KEY/objects/$UID responses). + * + * @param input JSON object to parse + * @param[out] entry where to write the data (data allocated by parser, + * caller must free via #SYNC_object_entry_clear()) + * @return #GNUNET_OK upon successful parsing, #GNUNET_SYSERR upon error + */ +enum GNUNET_GenericReturnValue +SYNC_object_entry_parse ( + json_t *input, + struct SYNC_ObjectEntry *entry); + + +/** + * Free data allocated by #SYNC_object_entry_parse(). + * + * @param[in] entry entry to clear + */ +void +SYNC_object_entry_clear ( + struct SYNC_ObjectEntry *entry); + + +/** + * Serialize an object entry into a JSON object + * (for building GET /backups/$KEY/objects/$UID responses). + * + * @param uid UID of the object + * @param data_size number of bytes in @a data + * @param data object data + * @return JSON object representing the entry + */ +json_t * +SYNC_object_entry_serialize ( + const struct SYNC_ObjectUID *uid, + size_t data_size, + const void *data); + + +/* ******************** Object references ******************** */ + + +/** + * Serialize the object references carried by a block operation + * into the JSON object used for the "object_refs" field: a map from + * the base32-encoded object UID to the reference count adjustment. + * + * @param refs_len number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by the block + * @param object_incs array of reference count adjustments + * @return JSON object representing the references, NULL if @a refs_len is 0 + */ +json_t * +SYNC_object_refs_serialize ( + size_t refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs); + + +/** + * Parse the "object_refs" field of a block operation into parallel + * arrays of object UIDs and reference count adjustments. + * + * On success the caller must #GNUNET_free() both output arrays; on + * failure nothing is allocated and the outputs are left untouched. + * + * @param refs JSON object to parse, NULL for "no references" + * @param[out] refs_len set to the number of entries parsed + * @param[out] object_uids set to a fresh array of object UIDs + * @param[out] object_incs set to a fresh array of adjustments + * @return #GNUNET_OK upon successful parsing, #GNUNET_SYSERR upon error + */ +enum GNUNET_GenericReturnValue +SYNC_object_refs_parse ( + const json_t *refs, + size_t *refs_len, + struct SYNC_ObjectUID **object_uids, + int16_t **object_incs); + + +/** + * Compute the hash over the object references, as covered by the block + * upload and delete signatures. + * + * Binary encoding, sorted by UID: 64 raw UID bytes followed by the + * adjustment as an int16 in network byte order. Sorting keeps the hash + * independent of the order the references arrive in, as the JSON object + * they travel in does not preserve member order. UIDs must be unique, + * as the wire format keys them by UID. + * + * @param refs_len number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by the block + * @param object_incs array of reference count adjustments + * @param[out] refs_hash set to the hash over the references + * @return #GNUNET_OK on success, + * #GNUNET_SYSERR if a UID appears more than once + */ +enum GNUNET_GenericReturnValue +SYNC_object_refs_hash ( + size_t refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + struct GNUNET_HashCode *refs_hash); + + +#endif /* SYNC_UTIL_H */ diff --git a/src/lib/meson.build b/src/lib/meson.build @@ -1,26 +1,30 @@ # This build file is in the public domain -libsync_la_SOURCES = [ +libsync_SOURCES = [ 'sync_api_curl_defaults.c', - 'sync_api_download.c', - 'sync_api_upload.c', + 'sync_api_block_operation_common.c', + 'sync_api_block_append.c', + 'sync_api_block_update.c', + 'sync_api_block_delete.c', + 'sync_api_block_list.c', + 'sync_api_object_upload.c', + 'sync_api_object_fetch.c', ] libsync = library( 'sync', - libsync_la_SOURCES, + libsync_SOURCES, soversion: solibversions['libsync']['soversion'], version: solibversions['libsync']['soversion'], install_rpath: rpath_option, dependencies: [ + libsyncutil_dep, talerutil_dep, talercurl_dep, talerjson_dep, gnunetutil_dep, gnunetjson_dep, gnunetcurl_dep, - talerutil_dep, - talerjson_dep, json_dep, curl_dep, ], @@ -35,5 +39,3 @@ pkg.generate( url: 'https://taler.net', description: 'GNU Taler sync library', ) - - diff --git a/src/lib/sync_api_block_append.c b/src/lib/sync_api_block_append.c @@ -0,0 +1,45 @@ +/* + This file is part of TALER + Copyright (C) 2014-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with TALER; see the file COPYING.LGPL. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/sync_api_block_append.c + * @brief Implementation of the block append (POST) operation + * @author Iván Ávalos + */ +#include "sync_api_block_operation_common.h" + + +struct SYNC_BlockOperation * +SYNC_block_append (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockOperationArgs *args, + SYNC_BlockOperationCallback cb, + void *cb_cls) +{ + return SYNC_block_operation_internal_ (ctx, + base_url, + priv, + args, + cb, + cb_cls, + false); +} + + +/* end of sync_api_block_append.c */ diff --git a/src/lib/sync_api_block_delete.c b/src/lib/sync_api_block_delete.c @@ -0,0 +1,45 @@ +/* + This file is part of TALER + Copyright (C) 2014-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with TALER; see the file COPYING.LGPL. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/sync_api_block_delete.c + * @brief Implementation of the block delete (DELETE) operation + * @author Iván Ávalos + */ +#include "sync_api_block_operation_common.h" + + +struct SYNC_BlockOperation * +SYNC_block_delete (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockOperationArgs *args, + SYNC_BlockOperationCallback cb, + void *cb_cls) +{ + return SYNC_block_operation_internal_ (ctx, + base_url, + priv, + args, + cb, + cb_cls, + true); +} + + +/* end of sync_api_block_delete.c */ diff --git a/src/lib/sync_api_block_list.c b/src/lib/sync_api_block_list.c @@ -0,0 +1,238 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/sync_api_block_list.c + * @brief Implementation of the block list GET + * @author Iván Ávalos + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_curl_lib.h> +#include <sync/sync_service.h> +#include <sync/sync_util.h> +#include "sync_api_curl_defaults.h" + + +/** + * Handle for a block list operation. + */ +struct SYNC_BlockListOperation +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Function to call with the result. + */ + SYNC_BlockListCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + +}; + + +/** + * Function called when we're done processing the + * GET /backups/$KEY/blocks request. + * + * @param cls the `struct SYNC_BlockListOperation *` + * @param response_code HTTP response code, 0 on error + * @param response response body as parsed JSON, or NULL + */ +static void +handle_block_list_finished (void *cls, + long response_code, + const void *response) +{ + struct SYNC_BlockListOperation *op = cls; + const json_t *json = response; + struct SYNC_BlockListDetails ld = { + .http_status = (unsigned int) response_code, + .ec = TALER_EC_INVALID + }; + + op->job = NULL; + switch (response_code) + { + case 0: + break; + case MHD_HTTP_OK: + if (NULL != json) + { + unsigned int idx; + json_t *entry; + + ld.ec = TALER_EC_NONE; + ld.num_entries = json_array_size (json); + ld.entries = GNUNET_new_array (ld.num_entries, + struct SYNC_BlockListEntry); + json_array_foreach (json, idx, entry) + { + struct SYNC_BlockListEntry *e = &ld.entries[idx]; + + if (GNUNET_OK != + SYNC_block_entry_parse (entry, + e)) + { + GNUNET_break_op (0); + ld.ec = TALER_EC_GENERIC_PARAMETER_MALFORMED; + break; + } + } + } + else + { + ld.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + } + break; + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_PAYMENT_REQUIRED: + case MHD_HTTP_INTERNAL_SERVER_ERROR: + default: + if ( (response_code >= 400) && + (response_code < 500) ) + ld.ec = (NULL != json) + ? TALER_JSON_get_error_code (json) + : TALER_EC_GENERIC_PARAMETER_MALFORMED; + else if (response_code >= 500) + ld.ec = (NULL != json) + ? TALER_JSON_get_error_code (json) + : TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + if (NULL != op->cb) + { + op->cb (op->cb_cls, + &ld); + op->cb = NULL; + } + if (NULL != ld.entries) + { + for (unsigned int i = 0; i < ld.num_entries; i++) + SYNC_block_entry_clear (&ld.entries[i]); + GNUNET_free (ld.entries); + } + SYNC_block_list_cancel (op); +} + + +struct SYNC_BlockListOperation * +SYNC_block_list (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPublicKeyP *pub, + int16_t limit, + const struct SYNC_BlockNonce *start_nonce, + SYNC_BlockListCallback cb, + void *cb_cls) +{ + struct SYNC_BlockListOperation *op; + CURL *eh; + char *pub_s; + char *nonce_s = NULL; + char *path; + char *limit_s; + char *url; + + pub_s = GNUNET_STRINGS_data_to_string_alloc (pub, + sizeof (*pub)); + GNUNET_asprintf (&path, + "backups/%s/blocks", + pub_s); + GNUNET_free (pub_s); + GNUNET_asprintf (&limit_s, + "%d", + (int) limit); + if (NULL != start_nonce) + nonce_s = GNUNET_STRINGS_data_to_string_alloc ( + start_nonce, + sizeof (*start_nonce)); + url = TALER_url_join (base_url, + path, + "limit", + limit_s, + "start_nonce", + nonce_s, + NULL); + GNUNET_free (nonce_s); + GNUNET_free (limit_s); + GNUNET_free (path); + if (NULL == url) + { + GNUNET_break (0); + return NULL; + } + + op = GNUNET_new (struct SYNC_BlockListOperation); + op->ctx = ctx; + op->cb = cb; + op->cb_cls = cb_cls; + op->url = url; + + eh = SYNC_curl_easy_get_ (op->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (op->url); + GNUNET_free (op); + return NULL; + } + + op->job = GNUNET_CURL_job_add2 (ctx, + eh, + NULL, + &handle_block_list_finished, + op); + return op; +} + + +void +SYNC_block_list_cancel (struct SYNC_BlockListOperation *op) +{ + if (NULL != op->job) + { + GNUNET_CURL_job_cancel (op->job); + op->job = NULL; + } + GNUNET_free (op->url); + GNUNET_free (op); +} diff --git a/src/lib/sync_api_block_operation_common.c b/src/lib/sync_api_block_operation_common.c @@ -0,0 +1,518 @@ +/* + This file is part of TALER + Copyright (C) 2014-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with TALER; see the file COPYING.LGPL. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/sync_api_block_operation_common.c + * @brief Shared implementation for block append, update, and delete + * @author Iván Ávalos + */ +#include "sync_api_block_operation_common.h" + + +char * +SYNC_block_operation_make_url_ ( + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const char *collection, + size_t id_size, + const void *id) +{ + struct SYNC_AccountPublicKeyP pub; + char *pub_s; + char *id_s; + char *path; + char *url; + + GNUNET_CRYPTO_eddsa_key_get_public (&priv->eddsa_priv, + &pub.eddsa_pub); + pub_s = GNUNET_STRINGS_data_to_string_alloc (&pub, + sizeof (pub)); + id_s = GNUNET_STRINGS_data_to_string_alloc (id, + id_size); + GNUNET_asprintf (&path, + "backups/%s/%s/%s", + pub_s, + collection, + id_s); + GNUNET_free (pub_s); + GNUNET_free (id_s); + url = TALER_url_join (base_url, + path, + NULL); + GNUNET_free (path); + return url; +} + + +void +SYNC_block_operation_add_etag_ ( + struct SYNC_BlockOperation *op, + const char *header, + const struct GNUNET_HashCode *hash) +{ + char *val; + char *hdr; + + val = GNUNET_STRINGS_data_to_string_alloc (hash, + sizeof (*hash)); + GNUNET_asprintf (&hdr, + "%s: \"%s\"", + header, + val); + GNUNET_free (val); + op->post_ctx.headers = curl_slist_append (op->post_ctx.headers, + hdr); + GNUNET_free (hdr); +} + + +/** + * Handle HTTP header received by curl. + * + * @param buffer one line of HTTP header data + * @param size size of an item + * @param nitems number of items passed + * @param userdata our `struct SYNC_BlockOperation *` + * @return `size * nitems` + */ +size_t +SYNC_block_operation_handle_header_ (char *buffer, + size_t size, + size_t nitems, + void *userdata) +{ + struct SYNC_BlockOperation *op = userdata; + size_t total = size * nitems; + char *ndup; + const char *hdr_type; + char *hdr_val; + char *sp; + + ndup = GNUNET_strndup (buffer, + total); + hdr_type = strtok_r (ndup, + ":", + &sp); + if (NULL == hdr_type) + { + GNUNET_free (ndup); + return total; + } + hdr_val = strtok_r (NULL, + "", + &sp); + if (NULL == hdr_val) + { + GNUNET_free (ndup); + return total; + } + if (' ' == *hdr_val) + hdr_val++; + if (0 == strcasecmp (hdr_type, + "Taler")) + { + size_t len; + + /* A second "Taler:" header would leak the first one */ + GNUNET_free (op->pay_uri); + op->pay_uri = GNUNET_strdup (hdr_val); + len = strlen (op->pay_uri); + while ( (len > 0) && + ( ('\n' == op->pay_uri[len - 1]) || + ('\r' == op->pay_uri[len - 1]) ) ) + { + len--; + op->pay_uri[len] = '\0'; + } + } + GNUNET_free (ndup); + return total; +} + + +/** + * Function called when we're done processing the + * HTTP /backups/$KEY/blocks/$NONCE request. + * + * @param cls the `struct SYNC_BlockOperation` + * @param response_code HTTP response code, 0 on error + * @param response response body parsed as JSON, or NULL + */ +void +SYNC_block_operation_handle_finished_ (void *cls, + long response_code, + const void *response) +{ + struct SYNC_BlockOperation *op = cls; + const json_t *json = response; + struct SYNC_BlockOperationDetails ud = { + .http_status = (unsigned int) response_code, + .ec = TALER_EC_INVALID + }; + + op->job = NULL; + switch (response_code) + { + case 0: + break; + case MHD_HTTP_NO_CONTENT: + ud.bos = SYNC_BOS_SUCCESS; + ud.ec = TALER_EC_NONE; + break; + case MHD_HTTP_NOT_MODIFIED: + ud.bos = SYNC_BOS_NOT_MODIFIED; + ud.ec = TALER_EC_NONE; + break; + case MHD_HTTP_PAYMENT_REQUIRED: + ud.bos = SYNC_BOS_PAYMENT_REQUIRED; + ud.pay_uri = op->pay_uri; + ud.ec = TALER_EC_NONE; + break; + case MHD_HTTP_BAD_REQUEST: + ud.ec = (NULL != json) + ? TALER_JSON_get_error_code (json) + : TALER_EC_GENERIC_PARAMETER_MALFORMED; + break; + case MHD_HTTP_FORBIDDEN: + ud.ec = (NULL != json) + ? TALER_JSON_get_error_code (json) + : TALER_EC_SYNC_INVALID_SIGNATURE; + break; + case MHD_HTTP_CONTENT_TOO_LARGE: + case MHD_HTTP_INTERNAL_SERVER_ERROR: + default: + if ( (response_code >= 400) && + (response_code < 500) ) + ud.bos = SYNC_BOS_CLIENT_ERROR; + else if (response_code >= 500) + ud.bos = SYNC_BOS_SERVER_ERROR; + else + ud.bos = SYNC_BOS_HTTP_ERROR; + ud.ec = (NULL != json) + ? TALER_JSON_get_error_code (json) + : TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + if (NULL != op->cb) + { + op->cb (op->cb_cls, + &ud); + op->cb = NULL; + } + SYNC_block_operation_cancel (op); +} + + +/** + * Build the upload signature covering the relink of a block. + * + * A relink never changes the data or the references, so the signature + * covers the block's stored hash and refs_hash on both sides and its + * new links (the nonces of the previous and next blocks). + * + * @param priv private key of the account + * @param entry the block being relinked (from a block list) + * @param new_prev the block's new predecessor, NULL for none + * @param new_next the block's new successor, NULL for none + * @param[out] sig signature to create + */ +static void +relink_sign (const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockListEntry *entry, + const struct SYNC_BlockNonce *new_prev, + const struct SYNC_BlockNonce *new_next, + struct SYNC_AccountSignatureP *sig) +{ + SYNC_block_upload_sign (priv, + &entry->nonce, + new_prev, + new_next, + &entry->hash, + &entry->hash, + &entry->refs_hash, + sig); +} + + +/** + * Serialize a relink object (relink_prev/relink_next): the new + * signature of the relinked neighbour. + * + * @param sig upload signature covering the relink + * @return JSON object to embed in the request body + */ +static json_t * +relink_serialize (const struct SYNC_AccountSignatureP *sig) +{ + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("upload_sig", + sig)); +} + + +struct SYNC_BlockOperation * +SYNC_block_operation_internal_ ( + struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockOperationArgs *args, + SYNC_BlockOperationCallback cb, + void *cb_cls, + bool is_delete) +{ + struct SYNC_BlockOperation *op; + struct SYNC_AccountSignatureP account_sig; + struct SYNC_AccountSignatureP relink_sig; + struct GNUNET_HashCode new_data_hash; + struct GNUNET_HashCode refs_hash; + CURL *eh; + json_t *req; + + if (NULL != args->block_data) + { + /* Compute hash of block data */ + GNUNET_CRYPTO_hash (args->block_data, + args->block_data_size, + &new_data_hash); + } + + /* Also rejects a repeated UID, which the wire format cannot carry */ + if (GNUNET_OK != + SYNC_object_refs_hash (args->refs_len, + args->object_uids, + args->object_incs, + &refs_hash)) + { + GNUNET_break (0); + return NULL; + } + + /* Build signature */ + if (is_delete) + { + SYNC_block_delete_sign (priv, + args->nonce, + args->prev_nonce, + args->next_nonce, + args->block_hash, + &refs_hash, + &account_sig); + } + else + { + SYNC_block_upload_sign (priv, + args->nonce, + args->prev_nonce, + args->next_nonce, + args->old_data_hash, + &new_data_hash, + &refs_hash, + &account_sig); + } + + /* Allocate operation handle */ + op = GNUNET_new (struct SYNC_BlockOperation); + op->ctx = ctx; + op->cb = cb; + op->cb_cls = cb_cls; + op->post_ctx.disable_compression = true; + op->url = SYNC_block_operation_make_url_ (base_url, + priv, + "blocks", + sizeof (*args->nonce), + args->nonce); + + /* Build JSON body */ + if (is_delete) + { + json_t *relink_prev = NULL; + json_t *relink_next = NULL; + + if (NULL != args->prev_entry) + { + relink_sign (priv, + args->prev_entry, + args->prev_entry->have_prev_nonce + ? &args->prev_entry->prev_nonce + : NULL, + args->next_nonce, + &relink_sig); + relink_prev = relink_serialize (&relink_sig); + } + + if (NULL != args->next_entry) + { + relink_sign (priv, + args->next_entry, + args->prev_nonce, + args->next_entry->have_next_nonce + ? &args->next_entry->next_nonce + : NULL, + &relink_sig); + relink_next = relink_serialize (&relink_sig); + } + + req = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("delete_sig", + &account_sig), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("prev_nonce", + args->prev_nonce)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("next_nonce", + args->next_nonce)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ( + "object_refs", + SYNC_object_refs_serialize (args->refs_len, + args->object_uids, + args->object_incs))), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("relink_prev", + relink_prev)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("relink_next", + relink_next))); + } + else + { + json_t *relink_prev = NULL; + + if (NULL != args->prev_entry) + { + relink_sign (priv, + args->prev_entry, + args->prev_entry->have_prev_nonce + ? &args->prev_entry->prev_nonce + : NULL, + args->nonce, + &relink_sig); + relink_prev = relink_serialize (&relink_sig); + } + + req = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("upload_sig", + &account_sig), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("prev_nonce", + args->prev_nonce)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("next_nonce", + args->next_nonce)), + GNUNET_JSON_pack_data_varsize ("data", + args->block_data, + args->block_data_size), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ( + "object_refs", + SYNC_object_refs_serialize (args->refs_len, + args->object_uids, + args->object_incs))), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("relink_prev", + relink_prev))); + } + + /* Set up CURL easy handle */ + eh = SYNC_curl_easy_get_ (op->url); + if (NULL == eh) + { + GNUNET_break (0); + json_decref (req); + goto cleanup; + } + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &SYNC_block_operation_handle_header_)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + op)); + + if (GNUNET_OK != + TALER_curl_easy_post (&op->post_ctx, + eh, + req)) + { + GNUNET_break (0); + json_decref (req); + curl_easy_cleanup (eh); + goto cleanup; + } + json_decref (req); + + /* Override method to PUT/DELETE as needed */ + if (! is_delete && NULL != args->old_data_hash) + { + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_CUSTOMREQUEST, + "PUT")); + } + if (is_delete) + { + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_CUSTOMREQUEST, + "DELETE")); + } + + SYNC_block_operation_add_etag_ (op, + MHD_HTTP_HEADER_IF_NONE_MATCH, + &new_data_hash); + /* Add If-Match header for updates and deletes */ + if (NULL != args->old_data_hash) + SYNC_block_operation_add_etag_ (op, + MHD_HTTP_HEADER_IF_MATCH, + args->old_data_hash); + if (is_delete) + SYNC_block_operation_add_etag_ (op, + MHD_HTTP_HEADER_IF_MATCH, + args->block_hash); + + op->job = GNUNET_CURL_job_add2 (ctx, + eh, + op->post_ctx.headers, + &SYNC_block_operation_handle_finished_, + op); + return op; + +cleanup: + GNUNET_free (op->url); + GNUNET_free (op); + return NULL; +} + + +void +SYNC_block_operation_cancel (struct SYNC_BlockOperation *op) +{ + if (NULL != op->job) + { + GNUNET_CURL_job_cancel (op->job); + op->job = NULL; + } + TALER_curl_easy_post_finished (&op->post_ctx); + GNUNET_free (op->pay_uri); + GNUNET_free (op->url); + GNUNET_free (op); +} + + +/* end of sync_api_block_operation_common.c */ diff --git a/src/lib/sync_api_block_operation_common.h b/src/lib/sync_api_block_operation_common.h @@ -0,0 +1,178 @@ +/* + This file is part of TALER + Copyright (C) 2014-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with TALER; see the file COPYING.LGPL. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/sync_api_block_operation_common.h + * @brief Internal declarations shared between block append, + * block update, and block delete implementations. + * @author Iván Ávalos + */ +#ifndef SYNC_BLOCK_OPERATION_COMMON_H +#define SYNC_BLOCK_OPERATION_COMMON_H + +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_curl_lib.h> +#include <sync/sync_service.h> +#include <sync/sync_signatures.h> +#include <sync/sync_util.h> +#include "sync_api_curl_defaults.h" + + +/** + * Internal: full definition of the block operation handle. + */ +struct SYNC_BlockOperation +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Function to call with the result. + */ + SYNC_BlockOperationCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Context for the POST body. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Payment URI from the "Taler" header, or NULL. + */ + char *pay_uri; + +}; + + +/** + * Build the URL of a resource under the account of @a priv. + * + * @param base_url base URL of the Sync server + * @param priv private key of the account + * @param collection collection under the account, e.g. "blocks" + * @param id_size number of bytes in @a id + * @param id identifier of the resource within @a collection + * @return the full URL, to be freed by the caller + */ +char * +SYNC_block_operation_make_url_ ( + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const char *collection, + size_t id_size, + const void *id); + + +/** + * Append an ETag-style header with the base32-encoded @a hash + * as its value to the headers of @a op. + * + * @param[in,out] op operation to add the header to + * @param header name of the header to add + * @param hash hash to encode as the header value + */ +void +SYNC_block_operation_add_etag_ ( + struct SYNC_BlockOperation *op, + const char *header, + const struct GNUNET_HashCode *hash); + + +/** + * Internal shared helper that implements block append, update and + * delete. + * + * @param ctx for HTTP client request processing + * @param base_url base URL of the Sync server + * @param priv private key of the account + * @param args operation parameters (including relinks) + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @param is_delete true to send DELETE instead of POST/PUT + * @return handle for the operation + */ +struct SYNC_BlockOperation * +SYNC_block_operation_internal_ ( + struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockOperationArgs *args, + SYNC_BlockOperationCallback cb, + void *cb_cls, + bool is_delete); + + +/** + * Handle HTTP header received by curl. Shared between block + * append, update, and delete implementations. + * + * @param buffer one line of HTTP header data + * @param size size of an item + * @param nitems number of items passed + * @param userdata our `struct SYNC_BlockOperation *` + * @return `size * nitems` + */ +size_t +SYNC_block_operation_handle_header_ (char *buffer, + size_t size, + size_t nitems, + void *userdata); + + +/** + * Function called when we're done processing the + * HTTP /backups/$KEY/blocks request. Shared between + * block append, update, and delete. + * + * @param cls the `struct SYNC_BlockOperation` + * @param response_code HTTP response code, 0 on error + * @param response response body parsed as JSON, or NULL + */ +void +SYNC_block_operation_handle_finished_ (void *cls, + long response_code, + const void *response); + + +#endif diff --git a/src/lib/sync_api_block_update.c b/src/lib/sync_api_block_update.c @@ -0,0 +1,45 @@ +/* + This file is part of TALER + Copyright (C) 2014-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with TALER; see the file COPYING.LGPL. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/sync_api_block_update.c + * @brief Implementation of the block update (PUT) operation + * @author Iván Ávalos + */ +#include "sync_api_block_operation_common.h" + + +struct SYNC_BlockOperation * +SYNC_block_update (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_BlockOperationArgs *args, + SYNC_BlockOperationCallback cb, + void *cb_cls) +{ + return SYNC_block_operation_internal_ (ctx, + base_url, + priv, + args, + cb, + cb_cls, + false); +} + + +/* end of sync_api_block_update.c */ diff --git a/src/lib/sync_api_download.c b/src/lib/sync_api_download.c @@ -1,307 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 2.1, - or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with TALER; see the file COPYING.LGPL. If not, - see <http://www.gnu.org/licenses/> -*/ - -/** - * @file lib/sync_api_download.c - * @brief Implementation of the download - * @author Christian Grothoff - */ -#include "platform.h" -#include <curl/curl.h> -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include <taler/taler_signatures.h> -#include "sync/sync_service.h" -#include "sync_api_curl_defaults.h" - - -/** - * @brief Handle for a download operation. - */ -struct SYNC_DownloadOperation -{ - - /** - * The url for this request. - */ - char *url; - - /** - * Handle for the request. - */ - struct GNUNET_CURL_Job *job; - - /** - * Reference to the execution context. - */ - struct GNUNET_CURL_Context *ctx; - - /** - * Function to call with the result. - */ - SYNC_DownloadCallback cb; - - /** - * Closure for @e cb. - */ - void *cb_cls; - - /** - * Public key of the account we are downloading from. - */ - struct SYNC_AccountPublicKeyP account_pub; - - /** - * Signature returned in the "Sync-Signature" - * header, or all zeros for none. - */ - struct SYNC_AccountSignatureP account_sig; - - /** - * Hash code returned by the server in the - * "Sync-Previous" header, or all zeros for - * none. - */ - struct GNUNET_HashCode sync_previous; - -}; - - -/** - * Function called when we're done processing the - * HTTP /backup request. - * - * @param cls the `struct SYNC_DownloadOperation` - * @param response_code HTTP response code, 0 on error - * @param data data we downloaded - * @param data_size number of bytes in @a data - */ -static void -handle_download_finished (void *cls, - long response_code, - const void *data, - size_t data_size) -{ - struct SYNC_DownloadOperation *download = cls; - struct SYNC_DownloadDetails dd = { - .http_status = (unsigned int) response_code - }; - - download->job = NULL; - switch (response_code) - { - case 0: - break; - case MHD_HTTP_OK: - { - struct SYNC_UploadSignaturePS usp = { - .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD), - .purpose.size = htonl (sizeof (usp)), - .old_backup_hash = download->sync_previous - }; - - GNUNET_CRYPTO_hash (data, - data_size, - &usp.new_backup_hash); - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD, - &usp, - &download->account_sig.eddsa_sig, - &download->account_pub.eddsa_pub)) - { - GNUNET_break_op (0); - dd.http_status = 0; - break; - } - /* Success, call callback with all details! */ - dd.details.ok.sig = download->account_sig; - dd.details.ok.prev_backup_hash = download->sync_previous; - dd.details.ok.curr_backup_hash = usp.new_backup_hash; - dd.details.ok.backup = data; - dd.details.ok.backup_size = data_size; - download->cb (download->cb_cls, - &dd); - download->cb = NULL; - SYNC_download_cancel (download); - return; - } - case MHD_HTTP_BAD_REQUEST: - /* This should never happen, either us or the sync server is buggy - (or API version conflict); just pass JSON reply to the application */ - break; - case MHD_HTTP_NOT_FOUND: - /* Nothing really to verify */ - break; - case MHD_HTTP_INTERNAL_SERVER_ERROR: - /* Server had an internal issue; we should retry, but this API - leaves this to the application */ - break; - default: - /* unexpected response code */ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unexpected response code %u\n", - (unsigned int) response_code); - GNUNET_break (0); - dd.http_status = 0; - break; - } - if (NULL != download->cb) - { - download->cb (download->cb_cls, - &dd); - download->cb = NULL; - } - SYNC_download_cancel (download); -} - - -/** - * Handle HTTP header received by curl. - * - * @param buffer one line of HTTP header data - * @param size size of an item - * @param nitems number of items passed - * @param userdata our `struct SYNC_DownloadOperation *` - * @return `size * nitems` - */ -static size_t -handle_header (char *buffer, - size_t size, - size_t nitems, - void *userdata) -{ - struct SYNC_DownloadOperation *download = userdata; - size_t total = size * nitems; - char *ndup; - const char *hdr_type; - char *hdr_val; - char *sp; - - ndup = GNUNET_strndup (buffer, - total); - hdr_type = strtok_r (ndup, - ":", - &sp); - if (NULL == hdr_type) - { - GNUNET_free (ndup); - return total; - } - hdr_val = strtok_r (NULL, - "\n\r", - &sp); - if (NULL == hdr_val) - { - GNUNET_free (ndup); - return total; - } - if (' ' == *hdr_val) - hdr_val++; - if (0 == strcasecmp (hdr_type, - "Sync-Signature")) - { - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (hdr_val, - strlen (hdr_val), - &download->account_sig, - sizeof (struct SYNC_AccountSignatureP))) - { - GNUNET_break_op (0); - GNUNET_free (ndup); - return 0; - } - } - if (0 == strcasecmp (hdr_type, - "Sync-Previous")) - { - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (hdr_val, - strlen (hdr_val), - &download->sync_previous, - sizeof (struct GNUNET_HashCode))) - { - GNUNET_break_op (0); - GNUNET_free (ndup); - return 0; - } - } - GNUNET_free (ndup); - return total; -} - - -struct SYNC_DownloadOperation * -SYNC_download (struct GNUNET_CURL_Context *ctx, - const char *base_url, - const struct SYNC_AccountPublicKeyP *pub, - SYNC_DownloadCallback cb, - void *cb_cls) -{ - struct SYNC_DownloadOperation *download; - char *pub_str; - CURL *eh; - - download = GNUNET_new (struct SYNC_DownloadOperation); - download->account_pub = *pub; - pub_str = GNUNET_STRINGS_data_to_string_alloc (pub, - sizeof (*pub)); - GNUNET_asprintf (&download->url, - "%s%sbackups/%s", - base_url, - '/' == base_url[strlen (base_url) - 1] - ? "" - : "/", - pub_str); - GNUNET_free (pub_str); - eh = SYNC_curl_easy_get_ (download->url); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_HEADERFUNCTION, - &handle_header)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_HEADERDATA, - download)); - download->cb = cb; - download->cb_cls = cb_cls; - download->job = GNUNET_CURL_job_add_raw (ctx, - eh, - NULL, - &handle_download_finished, - download); - return download; -} - - -void -SYNC_download_cancel (struct SYNC_DownloadOperation *download) -{ - if (NULL != download->job) - { - GNUNET_CURL_job_cancel (download->job); - download->job = NULL; - } - GNUNET_free (download->url); - GNUNET_free (download); -} - - -/* end of sync_api_download.c */ diff --git a/src/lib/sync_api_object_fetch.c b/src/lib/sync_api_object_fetch.c @@ -0,0 +1,217 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/sync_api_object_fetch.c + * @brief Implementation of the object fetch (GET) operation + * @author Iván Ávalos + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_curl_lib.h> +#include <sync/sync_service.h> +#include <sync/sync_util.h> +#include "sync_api_curl_defaults.h" + + +/** + * Handle for an object fetch operation. + */ +struct SYNC_ObjectFetchOperation +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Function to call with the result. + */ + SYNC_ObjectFetchCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + +}; + + +/** + * Function called when we're done processing the + * GET /backups/$KEY/objects/$UID request. + * + * @param cls the `struct SYNC_ObjectFetchOperation *` + * @param response_code HTTP response code, 0 on error + * @param response response body as parsed JSON, or NULL + */ +static void +handle_object_fetch_finished (void *cls, + long response_code, + const void *response) +{ + struct SYNC_ObjectFetchOperation *op = cls; + const json_t *json = response; + unsigned int http_status = (unsigned int) response_code; + enum TALER_ErrorCode ec = TALER_EC_INVALID; + struct SYNC_ObjectEntry entry = { 0 }; + + op->job = NULL; + switch (response_code) + { + case 0: + break; + case MHD_HTTP_OK: + if (NULL != json) + { + if (GNUNET_OK != + SYNC_object_entry_parse ((json_t *) json, + &entry)) + { + GNUNET_break_op (0); + ec = TALER_EC_GENERIC_PARAMETER_MALFORMED; + break; + } + ec = TALER_EC_NONE; + } + else + { + ec = TALER_EC_GENERIC_INVALID_RESPONSE; + } + break; + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_INTERNAL_SERVER_ERROR: + default: + if ( (response_code >= 400) && + (response_code < 500) ) + ec = (NULL != json) + ? TALER_JSON_get_error_code (json) + : TALER_EC_GENERIC_PARAMETER_MALFORMED; + else if (response_code >= 500) + ec = (NULL != json) + ? TALER_JSON_get_error_code (json) + : TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + if (NULL != op->cb) + { + op->cb (op->cb_cls, + http_status, + ec, + (TALER_EC_NONE == ec) ? &entry.uid : NULL, + (TALER_EC_NONE == ec) ? entry.data_size : 0, + (TALER_EC_NONE == ec) ? entry.data : NULL); + op->cb = NULL; + } + if (TALER_EC_NONE == ec) + SYNC_object_entry_clear (&entry); + SYNC_object_fetch_cancel (op); +} + + +struct SYNC_ObjectFetchOperation * +SYNC_object_fetch (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPublicKeyP *pub, + const struct SYNC_ObjectUID *uid, + SYNC_ObjectFetchCallback cb, + void *cb_cls) +{ + struct SYNC_ObjectFetchOperation *op; + CURL *eh; + char *pub_s; + char *uid_s; + char *path; + char *url; + + pub_s = GNUNET_STRINGS_data_to_string_alloc (pub, + sizeof (*pub)); + uid_s = GNUNET_STRINGS_data_to_string_alloc (uid, + sizeof (*uid)); + GNUNET_asprintf (&path, + "backups/%s/objects/%s", + pub_s, + uid_s); + GNUNET_free (pub_s); + GNUNET_free (uid_s); + url = TALER_url_join (base_url, + path, + NULL); + GNUNET_free (path); + if (NULL == url) + { + GNUNET_break (0); + return NULL; + } + + op = GNUNET_new (struct SYNC_ObjectFetchOperation); + op->ctx = ctx; + op->cb = cb; + op->cb_cls = cb_cls; + op->url = url; + + eh = SYNC_curl_easy_get_ (op->url); + if (NULL == eh) + { + GNUNET_break (0); + GNUNET_free (op->url); + GNUNET_free (op); + return NULL; + } + + op->job = GNUNET_CURL_job_add2 (ctx, + eh, + NULL, + &handle_object_fetch_finished, + op); + return op; +} + + +void +SYNC_object_fetch_cancel (struct SYNC_ObjectFetchOperation *op) +{ + if (NULL != op->job) + { + GNUNET_CURL_job_cancel (op->job); + op->job = NULL; + } + GNUNET_free (op->url); + GNUNET_free (op); +} + + +/* end of sync_api_object_fetch.c */ diff --git a/src/lib/sync_api_object_upload.c b/src/lib/sync_api_object_upload.c @@ -0,0 +1,121 @@ +/* + This file is part of TALER + Copyright (C) 2014-2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1, + or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with TALER; see the file COPYING.LGPL. If not, + see <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/sync_api_object_upload.c + * @brief Implementation of the object upload (POST) operation + * @author Iván Ávalos + */ +#include "sync_api_block_operation_common.h" + + +struct SYNC_BlockOperation * +SYNC_object_upload (struct GNUNET_CURL_Context *ctx, + const char *base_url, + const struct SYNC_AccountPrivateKeyP *priv, + const struct SYNC_ObjectUID *uid, + size_t data_size, + const void *data, + SYNC_BlockOperationCallback cb, + void *cb_cls) +{ + struct SYNC_BlockOperation *op; + struct SYNC_AccountSignatureP account_sig; + struct GNUNET_HashCode data_hash; + CURL *eh; + + /* Compute hash of data */ + GNUNET_CRYPTO_hash (data, + data_size, + &data_hash); + + /* Build signature */ + SYNC_object_upload_sign (priv, + uid, + &data_hash, + &account_sig); + + /* Allocate operation handle */ + op = GNUNET_new (struct SYNC_BlockOperation); + op->ctx = ctx; + op->cb = cb; + op->cb_cls = cb_cls; + op->post_ctx.disable_compression = true; + op->url = SYNC_block_operation_make_url_ (base_url, + priv, + "objects", + sizeof (*uid), + uid); + + /* Build JSON body */ + { + json_t *req; + + req = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("object_sig", + &account_sig), + GNUNET_JSON_pack_data_varsize ("data", + data, + data_size)); + + /* Set up CURL easy handle */ + eh = SYNC_curl_easy_get_ (op->url); + if (NULL == eh) + { + GNUNET_break (0); + json_decref (req); + goto cleanup; + } + + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERFUNCTION, + &SYNC_block_operation_handle_header_)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HEADERDATA, + op)); + + if (GNUNET_OK != + TALER_curl_easy_post (&op->post_ctx, + eh, + req)) + { + GNUNET_break (0); + json_decref (req); + curl_easy_cleanup (eh); + goto cleanup; + } + json_decref (req); + } + + op->job = GNUNET_CURL_job_add2 (ctx, + eh, + op->post_ctx.headers, + &SYNC_block_operation_handle_finished_, + op); + return op; + +cleanup: + GNUNET_free (op->url); + GNUNET_free (op); + return NULL; +} + + +/* end of sync_api_object_upload.c */ diff --git a/src/lib/sync_api_upload.c b/src/lib/sync_api_upload.c @@ -1,441 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014-2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 2.1, - or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with TALER; see the file COPYING.LGPL. If not, - see <http://www.gnu.org/licenses/> -*/ - -/** - * @file lib/sync_api_upload.c - * @brief Implementation of the upload POST - * @author Christian Grothoff - */ -#include "platform.h" -#include <curl/curl.h> -#include <jansson.h> -#include <microhttpd.h> /* just for HTTP status codes */ -#include <gnunet/gnunet_util_lib.h> -#include <gnunet/gnunet_curl_lib.h> -#include <taler/taler_signatures.h> -#include <taler/taler_json_lib.h> -#include "sync/sync_service.h" -#include "sync_api_curl_defaults.h" - - -/** - * @brief Handle for an upload operation. - */ -struct SYNC_UploadOperation -{ - - /** - * The url for this request. - */ - char *url; - - /** - * Handle for the request. - */ - struct GNUNET_CURL_Job *job; - - /** - * Reference to the execution context. - */ - struct GNUNET_CURL_Context *ctx; - - /** - * Function to call with the result. - */ - SYNC_UploadCallback cb; - - /** - * Closure for @e cb. - */ - void *cb_cls; - - /** - * Payment URI we received from the service, or NULL. - */ - char *pay_uri; - - /** - * Hash of the data we are uploading. - */ - struct GNUNET_HashCode new_upload_hash; -}; - - -/** - * Function called when we're done processing the - * HTTP /backup request. - * - * @param cls the `struct SYNC_UploadOperation` - * @param response_code HTTP response code, 0 on error - * @param data data we downloaded - * @param data_size number of bytes in @a data - */ -static void -handle_upload_finished (void *cls, - long response_code, - const void *data, - size_t data_size) -{ - struct SYNC_UploadOperation *uo = cls; - struct SYNC_UploadDetails ud = { - .http_status = (unsigned int) response_code, - .ec = TALER_EC_INVALID - }; - - uo->job = NULL; - switch (response_code) - { - case 0: - break; - case MHD_HTTP_NO_CONTENT: - ud.us = SYNC_US_SUCCESS; - ud.details.success.curr_backup_hash = &uo->new_upload_hash; - ud.ec = TALER_EC_NONE; - break; - case MHD_HTTP_NOT_MODIFIED: - ud.us = SYNC_US_SUCCESS; - ud.details.success.curr_backup_hash = &uo->new_upload_hash; - ud.ec = TALER_EC_NONE; - break; - case MHD_HTTP_BAD_REQUEST: - GNUNET_break (0); - ud.ec = TALER_JSON_get_error_code2 (data, - data_size); - break; - case MHD_HTTP_PAYMENT_REQUIRED: - ud.us = SYNC_US_PAYMENT_REQUIRED; - ud.details.payment_required.payment_request = uo->pay_uri; - ud.ec = TALER_EC_NONE; - break; - case MHD_HTTP_FORBIDDEN: - GNUNET_break (0); - ud.ec = TALER_JSON_get_error_code2 (data, - data_size); - break; - case MHD_HTTP_CONFLICT: - ud.us = SYNC_US_CONFLICTING_BACKUP; - GNUNET_CRYPTO_hash (data, - data_size, - &ud.details.recovered_backup.existing_backup_hash); - ud.details.recovered_backup.existing_backup_size - = data_size; - ud.details.recovered_backup.existing_backup - = data; - ud.ec = TALER_EC_NONE; - break; - case MHD_HTTP_GONE: - ud.ec = TALER_JSON_get_error_code2 (data, - data_size); - break; - case MHD_HTTP_LENGTH_REQUIRED: - GNUNET_break (0); - break; - case MHD_HTTP_REQUEST_ENTITY_TOO_LARGE: - ud.ec = TALER_JSON_get_error_code2 (data, - data_size); - break; - case MHD_HTTP_TOO_MANY_REQUESTS: - ud.ec = TALER_JSON_get_error_code2 (data, - data_size); - break; - case MHD_HTTP_INTERNAL_SERVER_ERROR: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Internal server error: `%.*s\n", - (int) data_size, - (const char *) data); - break; - } - if (NULL != uo->cb) - { - uo->cb (uo->cb_cls, - &ud); - uo->cb = NULL; - } - SYNC_upload_cancel (uo); -} - - -/** - * Handle HTTP header received by curl. - * - * @param buffer one line of HTTP header data - * @param size size of an item - * @param nitems number of items passed - * @param userdata our `struct SYNC_DownloadOperation *` - * @return `size * nitems` - */ -static size_t -handle_header (char *buffer, - size_t size, - size_t nitems, - void *userdata) -{ - struct SYNC_UploadOperation *uo = userdata; - size_t total = size * nitems; - char *ndup; - const char *hdr_type; - char *hdr_val; - char *sp; - - ndup = GNUNET_strndup (buffer, - total); - hdr_type = strtok_r (ndup, - ":", - &sp); - if (NULL == hdr_type) - { - GNUNET_free (ndup); - return total; - } - hdr_val = strtok_r (NULL, - "", - &sp); - if (NULL == hdr_val) - { - GNUNET_free (ndup); - return total; - } - if (' ' == *hdr_val) - hdr_val++; - if (0 == strcasecmp (hdr_type, - "Taler")) - { - size_t len; - - /* found payment URI we care about! */ - uo->pay_uri = GNUNET_strdup (hdr_val); - len = strlen (uo->pay_uri); - while ( (len > 0) && - ( ('\n' == uo->pay_uri[len - 1]) || - ('\r' == uo->pay_uri[len - 1]) ) ) - { - len--; - uo->pay_uri[len] = '\0'; - } - } - GNUNET_free (ndup); - return total; -} - - -struct SYNC_UploadOperation * -SYNC_upload (struct GNUNET_CURL_Context *ctx, - const char *base_url, - struct SYNC_AccountPrivateKeyP *priv, - const struct GNUNET_HashCode *prev_backup_hash, - size_t backup_size, - const void *backup, - enum SYNC_PaymentOptions po, - const char *paid_order_id, - SYNC_UploadCallback cb, - void *cb_cls) -{ - struct SYNC_AccountSignatureP account_sig; - struct SYNC_UploadOperation *uo; - CURL *eh; - struct curl_slist *job_headers; - struct SYNC_UploadSignaturePS usp = { - .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD), - .purpose.size = htonl (sizeof (usp)) - }; - - if (NULL != prev_backup_hash) - usp.old_backup_hash = *prev_backup_hash; - GNUNET_CRYPTO_hash (backup, - backup_size, - &usp.new_backup_hash); - GNUNET_CRYPTO_eddsa_sign (&priv->eddsa_priv, - &usp, - &account_sig.eddsa_sig); - - /* setup our HTTP headers */ - job_headers = NULL; - { - struct curl_slist *ext; - char *val; - char *hdr; - - /* Set Sync-Signature header */ - val = GNUNET_STRINGS_data_to_string_alloc (&account_sig, - sizeof (account_sig)); - GNUNET_asprintf (&hdr, - "Sync-Signature: %s", - val); - GNUNET_free (val); - ext = curl_slist_append (job_headers, - hdr); - GNUNET_free (hdr); - if (NULL == ext) - { - GNUNET_break (0); - curl_slist_free_all (job_headers); - return NULL; - } - job_headers = ext; - - /* set If-None-Match header */ - val = GNUNET_STRINGS_data_to_string_alloc (&usp.new_backup_hash, - sizeof (struct GNUNET_HashCode)); - GNUNET_asprintf (&hdr, - "%s: \"%s\"", - MHD_HTTP_HEADER_IF_NONE_MATCH, - val); - GNUNET_free (val); - ext = curl_slist_append (job_headers, - hdr); - GNUNET_free (hdr); - if (NULL == ext) - { - GNUNET_break (0); - curl_slist_free_all (job_headers); - return NULL; - } - job_headers = ext; - - /* Setup If-Match header */ - if (NULL != prev_backup_hash) - { - val = GNUNET_STRINGS_data_to_string_alloc (&usp.old_backup_hash, - sizeof (struct - GNUNET_HashCode)); - GNUNET_asprintf (&hdr, - "If-Match: \"%s\"", - val); - GNUNET_free (val); - ext = curl_slist_append (job_headers, - hdr); - GNUNET_free (hdr); - if (NULL == ext) - { - GNUNET_break (0); - curl_slist_free_all (job_headers); - return NULL; - } - job_headers = ext; - } - } - /* Finished setting up headers */ - - uo = GNUNET_new (struct SYNC_UploadOperation); - uo->new_upload_hash = usp.new_backup_hash; - { - char *path; - char *account_s; - struct SYNC_AccountPublicKeyP pub; - - GNUNET_CRYPTO_eddsa_key_get_public (&priv->eddsa_priv, - &pub.eddsa_pub); - account_s = GNUNET_STRINGS_data_to_string_alloc (&pub, - sizeof (pub)); - GNUNET_asprintf (&path, - "backups/%s", - account_s); - GNUNET_free (account_s); - if (0 != (po & SYNC_PO_FRESH_ORDER)) - { - uo->url = (0 != (po & SYNC_PO_FORCE_PAYMENT)) - ? TALER_url_join (base_url, - path, - "fresh", - "y", - "pay", - "y", - (NULL != paid_order_id) - ? "paying" - : NULL, - paid_order_id, - NULL) - : TALER_url_join (base_url, - path, - "fresh", - "y", - (NULL != paid_order_id) - ? "paying" - : NULL, - paid_order_id, - NULL); - } - else - { - uo->url = (0 != (po & SYNC_PO_FORCE_PAYMENT)) - ? TALER_url_join (base_url, - path, - "pay", - "y", - (NULL != paid_order_id) - ? "paying" - : NULL, - paid_order_id, - NULL) - : TALER_url_join (base_url, - path, - (NULL != paid_order_id) - ? "paying" - : NULL, - paid_order_id, - NULL); - } - - GNUNET_free (path); - } - uo->ctx = ctx; - uo->cb = cb; - uo->cb_cls = cb_cls; - eh = SYNC_curl_easy_get_ (uo->url); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_POSTFIELDS, - backup)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_POSTFIELDSIZE, - (long) backup_size)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_HEADERFUNCTION, - &handle_header)); - GNUNET_assert (CURLE_OK == - curl_easy_setopt (eh, - CURLOPT_HEADERDATA, - uo)); - uo->job = GNUNET_CURL_job_add_raw (ctx, - eh, - job_headers, - &handle_upload_finished, - uo); - curl_slist_free_all (job_headers); - return uo; -} - - -void -SYNC_upload_cancel (struct SYNC_UploadOperation *uo) -{ - if (NULL != uo->job) - { - GNUNET_CURL_job_cancel (uo->job); - uo->job = NULL; - } - GNUNET_free (uo->pay_uri); - GNUNET_free (uo->url); - GNUNET_free (uo); -} - - -/* end of sync_api_upload.c */ diff --git a/src/sync/meson.build b/src/sync/meson.build @@ -2,13 +2,17 @@ install_data('sync.conf', install_dir: pkgcfgdir) executable( - 'sync-httpd', + 'sync-httpd2', [ - 'sync-httpd.c', - 'sync-httpd_backup.c', - 'sync-httpd_backup_post.c', - 'sync-httpd_config.c', - 'sync-httpd_mhd.c', + 'sync-httpd2.c', + 'sync-httpd2_config.c', + 'sync-httpd2_post-backups-KEY-blocks-NONCE.c', + 'sync-httpd2_delete-backups-KEY-blocks-NONCE.c', + 'sync-httpd2_get-backups-KEY-objects-UID.c', + 'sync-httpd2_post-backups-KEY-objects-UID.c', + 'sync-httpd2_get-backups-KEY.c', + 'sync-httpd2_get-backups-KEY-blocks.c', + 'sync-httpd2_payments.c', ], dependencies: [ libsyncutil_dep, @@ -17,40 +21,13 @@ executable( talerjson_dep, talermerchant_dep, talermhd_dep, + talermhd2_dep, gnunetutil_dep, gnunetjson_dep, gnunetcurl_dep, - mhd_dep, + mhd2_dep, json_dep, ], include_directories: [incdir, configuration_inc], install: true, ) - -if mhd2_dep.found() and talermhd2_dep.found() - executable( - 'sync-httpd2', - [ - 'sync-httpd2.c', - 'sync-httpd2_backup.c', - 'sync-httpd2_backup_post.c', - 'sync-httpd2_config.c', - ], - dependencies: [ - libsyncutil_dep, - libsyncdb_dep, - talerutil_dep, - talerjson_dep, - talermerchant_dep, - talermhd_dep, - talermhd2_dep, - gnunetutil_dep, - gnunetjson_dep, - gnunetcurl_dep, - mhd2_dep, - json_dep, - ], - include_directories: [incdir, configuration_inc], - install: true, - ) -endif diff --git a/src/sync/sync-httpd.c b/src/sync/sync-httpd.c @@ -1,655 +0,0 @@ -/* - This file is part of TALER - (C) 2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync/sync-httpd.c - * @brief HTTP serving layer intended to provide basic backup operations - * @author Christian Grothoff - */ -#include "platform.h" -#include <microhttpd.h> -#include <gnunet/gnunet_util_lib.h> -#include "sync/sync_util.h" -#include "sync-httpd.h" -#include "sync-httpd_mhd.h" -#include "sync/sync_database_lib.h" -#include "sync-httpd_backup.h" -#include "sync-httpd_config.h" - - -/** - * Should a "Connection: close" header be added to each HTTP response? - */ -static int SH_sync_connection_close; - -/** - * Upload limit to the service, in megabytes. - */ -unsigned long long int SH_upload_limit_mb; - -/** - * Annual fee for the backup account. - */ -struct TALER_Amount SH_annual_fee; - -/** - * Our Taler backend to process payments. - */ -char *SH_backend_url; - -/** - * Our fulfillment URL. - */ -char *SH_fulfillment_url; - -/** - * Our context for making HTTP requests. - */ -struct GNUNET_CURL_Context *SH_ctx; - -/** - * Reschedule context for #SH_ctx. - */ -static struct GNUNET_CURL_RescheduleContext *rc; - -/** - * Global return code - */ -static int global_ret; - -/** - * Set to true if we have started an MHD daemons. - */ -static bool have_daemons; - -/** - * Username and password to use for client authentication - * (optional). - */ -static char *userpass; - -/** - * Type of the client's TLS certificate (optional). - */ -static char *certtype; - -/** - * File with the client's TLS certificate (optional). - */ -static char *certfile; - -/** - * File with the client's TLS private key (optional). - */ -static char *keyfile; - -/** - * This value goes in the Authorization:-header. - */ -static char *apikey; - -/** - * Passphrase to decrypt client's TLS private key file (optional). - */ -static char *keypass; - -/** - * Amount of insurance. - */ -struct TALER_Amount SH_insurance; - - -/** - * A client has requested the given url using the given method - * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT, - * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). The callback - * must call MHD callbacks to provide content to give back to the - * client and return an HTTP status code (i.e. #MHD_HTTP_OK, - * #MHD_HTTP_NOT_FOUND, etc.). - * - * @param cls argument given together with the function - * pointer when the handler was registered with MHD - * @param connection connection handle - * @param url the requested url - * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, - * #MHD_HTTP_METHOD_PUT, etc.) - * @param version the HTTP version string (i.e. - * #MHD_HTTP_VERSION_1_1) - * @param upload_data the data being uploaded (excluding HEADERS, - * for a POST that fits into memory and that is encoded - * with a supported encoding, the POST data will NOT be - * given in upload_data and is instead available as - * part of #MHD_get_connection_values; very large POST - * data *will* be made available incrementally in - * @a upload_data) - * @param upload_data_size set initially to the size of the - * @a upload_data provided; the method must update this - * value to the number of bytes NOT processed; - * @param con_cls pointer that the callback can set to some - * address and that will be preserved by MHD for future - * calls for this request; since the access handler may - * be called many times (i.e., for a PUT/POST operation - * with plenty of upload data) this allows the application - * to easily associate some request-specific state. - * If necessary, this state can be cleaned up in the - * global #MHD_RequestCompletedCallback (which - * can be set with the #MHD_OPTION_NOTIFY_COMPLETED). - * Initially, `*con_cls` will be NULL. - * @return #MHD_YES if the connection was handled successfully, - * #MHD_NO if the socket must be closed due to a serious - * error while handling the request - */ -static enum MHD_Result -url_handler (void *cls, - struct MHD_Connection *connection, - const char *url, - const char *method, - const char *version, - const char *upload_data, - size_t *upload_data_size, - void **con_cls) -{ - static struct SH_RequestHandler handlers[] = { - /* Landing page, tell humans to go away. */ - { "/", MHD_HTTP_METHOD_GET, "text/plain", - "Hello, I'm sync. This HTTP server is not for humans.\n", 0, - &SH_MHD_handler_static_response, MHD_HTTP_OK }, - { "/agpl", MHD_HTTP_METHOD_GET, "text/plain", - NULL, 0, - &SH_handler_config, MHD_HTTP_FOUND }, - { "/config", MHD_HTTP_METHOD_GET, "text/json", - NULL, 0, - &SH_handler_config, MHD_HTTP_OK }, - {NULL, NULL, NULL, NULL, 0, 0 } - }; - static struct SH_RequestHandler h404 = { - "", NULL, "text/html", - "<html><title>404: not found</title></html>", 0, - &SH_MHD_handler_static_response, MHD_HTTP_NOT_FOUND - }; - - struct TM_HandlerContext *hc = *con_cls; - struct GNUNET_AsyncScopeId aid; - const char *correlation_id = NULL; - struct SYNC_AccountPublicKeyP account_pub; - - (void) cls; - (void) version; - if (NULL == hc) - { - GNUNET_async_scope_fresh (&aid); - /* We only read the correlation ID on the first callback for every client */ - correlation_id = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - "Sync-Correlation-Id"); - if ((NULL != correlation_id) && - (GNUNET_YES != GNUNET_CURL_is_valid_scope_id (correlation_id))) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "illegal incoming correlation ID\n"); - correlation_id = NULL; - } - } - else - { - aid = hc->async_scope_id; - } - GNUNET_SCHEDULER_begin_async_scope (&aid); - - if (NULL != correlation_id) - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling request for (%s) URL '%s', correlation_id=%s\n", - method, - url, - correlation_id); - else - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling request (%s) for URL '%s'\n", - method, - url); - - if (0 == strncmp (url, - "/backups/", - strlen ("/backups/"))) - { - const char *ac = &url[strlen ("/backups/")]; - - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_public_key_from_string (ac, - strlen (ac), - &account_pub.eddsa_pub)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - ac); - } - if (0 == strcasecmp (method, - MHD_HTTP_METHOD_OPTIONS)) - { - return TALER_MHD_reply_cors_preflight (connection); - } - if (0 == strcasecmp (method, - MHD_HTTP_METHOD_GET)) - { - return SH_backup_get (connection, - &account_pub); - } - if (0 == strcasecmp (method, - MHD_HTTP_METHOD_POST)) - { - int ret; - - ret = SH_backup_post (connection, - con_cls, - &account_pub, - upload_data, - upload_data_size); - hc = *con_cls; - if (NULL != hc) - { - /* Store the async context ID, so we can restore it if - * we get another callback for this request. */ - hc->async_scope_id = aid; - } - return ret; - } - } - for (unsigned int i = 0; NULL != handlers[i].url; i++) - { - struct SH_RequestHandler *rh = &handlers[i]; - - if (0 == strcmp (url, - rh->url)) - { - if (0 == strcasecmp (method, - MHD_HTTP_METHOD_OPTIONS)) - { - return TALER_MHD_reply_cors_preflight (connection); - } - if ( (NULL == rh->method) || - (0 == strcasecmp (method, - rh->method)) ) - { - enum MHD_Result ret; - - ret = rh->handler (rh, - connection, - con_cls, - upload_data, - upload_data_size); - hc = *con_cls; - if (NULL != hc) - { - /* Store the async context ID, so we can restore it if - * we get another callback for this request. */ - hc->async_scope_id = aid; - } - return ret; - } - } - } - return SH_MHD_handler_static_response (&h404, - connection, - con_cls, - upload_data, - upload_data_size); -} - - -/** - * Shutdown task. Invoked when the application is being terminated. - * - * @param cls NULL - */ -static void -do_shutdown (void *cls) -{ - (void) cls; - SH_resume_all_bc (); - TALER_MHD_daemons_halt (); - if (NULL != SH_ctx) - { - GNUNET_CURL_fini (SH_ctx); - SH_ctx = NULL; - } - if (NULL != rc) - { - GNUNET_CURL_gnunet_rc_destroy (rc); - rc = NULL; - } - TALER_MHD_daemons_destroy (); - SYNCDB_fini (); -} - - -/** - * Function called whenever MHD is done with a request. If the - * request was a POST, we may have stored a `struct Buffer *` in the - * @a con_cls that might still need to be cleaned up. Call the - * respective function to free the memory. - * - * @param cls client-defined closure - * @param connection connection handle - * @param con_cls value as set by the last call to - * the #MHD_AccessHandlerCallback - * @param toe reason for request termination - * @see #MHD_OPTION_NOTIFY_COMPLETED - * @ingroup request - */ -static void -handle_mhd_completion_callback (void *cls, - struct MHD_Connection *connection, - void **con_cls, - enum MHD_RequestTerminationCode toe) -{ - struct TM_HandlerContext *hc = *con_cls; - - (void) cls; - (void) connection; - if (NULL == hc) - return; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Finished handling request with status %d\n", - (int) toe); - hc->cc (hc); - *con_cls = NULL; -} - - -/** - * Kick MHD to run now, to be called after MHD_resume_connection(). - * Basically, we need to explicitly resume MHD's event loop whenever - * we made progress serving a request. This function re-schedules - * the task processing MHD's activities to run immediately. - */ -void -SH_trigger_daemon () -{ - TALER_MHD_daemon_trigger (); -} - - -/** - * Kick GNUnet Curl scheduler to begin curl interactions. - */ -void -SH_trigger_curl () -{ - GNUNET_CURL_gnunet_scheduler_reschedule (&rc); -} - - -/** - * Callback invoked on every listen socket to start the - * respective MHD HTTP daemon. - * - * @param cls unused - * @param lsock the listen socket - */ -static void -start_daemon (void *cls, - int lsock) -{ - struct MHD_Daemon *mhd; - - (void) cls; - GNUNET_assert (-1 != lsock); - mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK, - 0 /* port */, - NULL, NULL, - &url_handler, NULL, - MHD_OPTION_LISTEN_SOCKET, lsock, - MHD_OPTION_NOTIFY_COMPLETED, - &handle_mhd_completion_callback, NULL, - MHD_OPTION_CONNECTION_TIMEOUT, - (unsigned int) 10 /* 10s */, - MHD_OPTION_END); - if (NULL == mhd) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to launch HTTP service, exiting.\n"); - global_ret = EXIT_NO_RESTART; - GNUNET_SCHEDULER_shutdown (); - return; - } - have_daemons = true; - TALER_MHD_daemon_start (mhd); -} - - -/** - * Main function that will be run by the scheduler. - * - * @param cls closure - * @param args remaining command-line arguments - * @param cfgfile name of the configuration file used (for saving, can be - * NULL!) - * @param config configuration - */ -static void -run (void *cls, - char *const *args, - const char *cfgfile, - const struct GNUNET_CONFIGURATION_Handle *config) -{ - enum TALER_MHD_GlobalOptions go; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Starting sync-httpd\n"); - go = TALER_MHD_GO_NONE; - if (SH_sync_connection_close) - go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE; - TALER_MHD_setup (go); - global_ret = EXIT_NOTCONFIGURED; - GNUNET_SCHEDULER_add_shutdown (&do_shutdown, - NULL); - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_number (config, - "sync", - "UPLOAD_LIMIT_MB", - &SH_upload_limit_mb)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "sync", - "UPLOAD_LIMIT_MB"); - GNUNET_SCHEDULER_shutdown (); - return; - } - if (GNUNET_OK != - TALER_config_get_amount (config, - "sync", - "INSURANCE", - &SH_insurance)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "sync", - "INSURANCE"); - GNUNET_SCHEDULER_shutdown (); - return; - } - if (GNUNET_OK != - TALER_config_get_amount (config, - "sync", - "ANNUAL_FEE", - &SH_annual_fee)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "sync", - "ANNUAL_FEE"); - GNUNET_SCHEDULER_shutdown (); - return; - } - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (config, - "sync", - "PAYMENT_BACKEND_URL", - &SH_backend_url)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "sync", - "PAYMENT_BACKEND_URL"); - GNUNET_SCHEDULER_shutdown (); - return; - } - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (config, - "sync", - "FULFILLMENT_URL", - &SH_fulfillment_url)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "sync", - "BASE_URL"); - GNUNET_SCHEDULER_shutdown (); - return; - } - - /* setup HTTP client event loop */ - SH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, - &rc); - rc = GNUNET_CURL_gnunet_rc_create (SH_ctx); - if (NULL != userpass) - GNUNET_CURL_set_userpass (SH_ctx, - userpass); - if (NULL != keyfile) - GNUNET_CURL_set_tlscert (SH_ctx, - certtype, - certfile, - keyfile, - keypass); - if (GNUNET_OK == - GNUNET_CONFIGURATION_get_value_string (config, - "sync", - "API_KEY", - &apikey)) - { - char *auth_header; - - GNUNET_asprintf (&auth_header, - "%s: %s", - MHD_HTTP_HEADER_AUTHORIZATION, - apikey); - if (GNUNET_OK != - GNUNET_CURL_append_header (SH_ctx, - auth_header)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to set %s header, trying without\n", - MHD_HTTP_HEADER_AUTHORIZATION); - } - GNUNET_free (auth_header); - } - - if (GNUNET_OK != - SYNCDB_init (config, - false)) - { - global_ret = EXIT_NOTCONFIGURED; - GNUNET_SCHEDULER_shutdown (); - return; - } - if (GNUNET_OK != - SYNCDB_preflight ()) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database not setup. Did you run sync-dbinit?\n"); - GNUNET_SCHEDULER_shutdown (); - return; - } - { - enum GNUNET_GenericReturnValue ret; - - ret = TALER_MHD_listen_bind (config, - "sync", - &start_daemon, - NULL); - switch (ret) - { - case GNUNET_SYSERR: - global_ret = EXIT_NOTCONFIGURED; - GNUNET_SCHEDULER_shutdown (); - return; - case GNUNET_NO: - if (! have_daemons) - { - global_ret = EXIT_NOTCONFIGURED; - GNUNET_SCHEDULER_shutdown (); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Could not open all configured listen sockets\n"); - break; - case GNUNET_OK: - break; - } - } - global_ret = EXIT_SUCCESS; -} - - -/** - * The main function of the serve tool - * - * @param argc number of arguments from the command line - * @param argv command line arguments - * @return 0 ok, 1 on error - */ -int -main (int argc, - char *const *argv) -{ - struct GNUNET_GETOPT_CommandLineOption options[] = { - GNUNET_GETOPT_option_string ('A', - "auth", - "USERNAME:PASSWORD", - "use the given USERNAME and PASSWORD for client authentication", - &userpass), - GNUNET_GETOPT_option_flag ('C', - "connection-close", - "force HTTP connections to be closed after each request", - &SH_sync_connection_close), - GNUNET_GETOPT_option_string ('k', - "key", - "KEYFILE", - "file with the private TLS key for TLS client authentication", - &keyfile), - GNUNET_GETOPT_option_string ('p', - "pass", - "KEYFILEPASSPHRASE", - "passphrase needed to decrypt the TLS client private key file", - &keypass), - GNUNET_GETOPT_option_string ('t', - "type", - "CERTTYPE", - "type of the TLS client certificate, defaults to PEM if not specified", - &certtype), - GNUNET_GETOPT_OPTION_END - }; - enum GNUNET_GenericReturnValue ret; - - ret = GNUNET_PROGRAM_run (SYNC_project_data (), - argc, argv, - "sync-httpd", - "sync HTTP interface", - options, - &run, NULL); - if (GNUNET_NO == ret) - return EXIT_SUCCESS; - if (GNUNET_SYSERR == ret) - return EXIT_INVALIDARGUMENT; - return global_ret; -} diff --git a/src/sync/sync-httpd.h b/src/sync/sync-httpd.h @@ -1,173 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019-2021 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync/sync-httpd.h - * @brief HTTP serving layer - * @author Christian Grothoff - */ -#ifndef sync_HTTPD_H -#define sync_HTTPD_H - -#include "platform.h" -#include <microhttpd.h> -#include <taler/taler_mhd_lib.h> -#include "sync/sync_database_lib.h" -#include <gnunet/gnunet_mhd_compat.h> - -/** - * @brief Struct describing an URL and the handler for it. - */ -struct SH_RequestHandler -{ - - /** - * URL the handler is for. - */ - const char *url; - - /** - * Method the handler is for, NULL for "all". - */ - const char *method; - - /** - * Mime type to use in reply (hint, can be NULL). - */ - const char *mime_type; - - /** - * Raw data for the @e handler - */ - const void *data; - - /** - * Number of bytes in @e data, 0 for 0-terminated. - */ - size_t data_size; - - /** - * Function to call to handle the request. - * - * @param rh this struct - * @param mime_type the @e mime_type for the reply (hint, can be NULL) - * @param connection the MHD connection to handle - * @param[in,out] connection_cls the connection's closure (can be updated) - * @param upload_data upload data - * @param[in,out] upload_data_size number of bytes (left) in @a upload_data - * @return MHD result code - */ - enum MHD_Result (*handler)(struct SH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size); - - /** - * Default response code. - */ - unsigned int response_code; -}; - - -/** - * Each MHD response handler that sets the "connection_cls" to a - * non-NULL value must use a struct that has this struct as its first - * member. This struct contains a single callback, which will be - * invoked to clean up the memory when the contection is completed. - */ -struct TM_HandlerContext; - -/** - * Signature of a function used to clean up the context - * we keep in the "connection_cls" of MHD when handling - * a request. - * - * @param hc header of the context to clean up. - */ -typedef void -(*TM_ContextCleanup)(struct TM_HandlerContext *hc); - - -/** - * Each MHD response handler that sets the "connection_cls" to a - * non-NULL value must use a struct that has this struct as its first - * member. This struct contains a single callback, which will be - * invoked to clean up the memory when the connection is completed. - */ -struct TM_HandlerContext -{ - - /** - * Function to execute the handler-specific cleanup of the - * (typically larger) context. - */ - TM_ContextCleanup cc; - - /** - * Asynchronous request context id. - */ - struct GNUNET_AsyncScopeId async_scope_id; -}; - - -/** - * Upload limit to the service, in megabytes. - */ -extern unsigned long long SH_upload_limit_mb; - -/** - * Annual fee for the backup account. - */ -extern struct TALER_Amount SH_annual_fee; - -/** - * Our Taler backend to process payments. - */ -extern char *SH_backend_url; - -/** - * Our fulfillment URL - */ -extern char *SH_fulfillment_url; - -/** - * Our context for making HTTP requests. - */ -extern struct GNUNET_CURL_Context *SH_ctx; - -/** - * Amount of insurance. - */ -extern struct TALER_Amount SH_insurance; - -/** - * Kick MHD to run now, to be called after MHD_resume_connection(). - * Basically, we need to explicitly resume MHD's event loop whenever - * we made progress serving a request. This function re-schedules - * the task processing MHD's activities to run immediately. - */ -void -SH_trigger_daemon (void); - - -/** - * Kick GNUnet Curl scheduler to begin curl interactions. - */ -void -SH_trigger_curl (void); - - -#endif diff --git a/src/sync/sync-httpd2.c b/src/sync/sync-httpd2.c @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file sync/sync-httpd.c + * @file sync/sync-httpd2.c * @brief HTTP serving layer intended to provide basic backup operations * @author Christian Grothoff */ @@ -23,8 +23,62 @@ #include "sync/sync_util.h" #include "sync-httpd2.h" #include "sync/sync_database_lib.h" -#include "sync-httpd2_backup.h" #include "sync-httpd2_config.h" +#include "sync-httpd2_post-backups-KEY-blocks-NONCE.h" +#include "sync-httpd2_delete-backups-KEY-blocks-NONCE.h" +#include "sync-httpd2_get-backups-KEY-objects-UID.h" +#include "sync-httpd2_post-backups-KEY-objects-UID.h" +#include "sync-httpd2_get-backups-KEY.h" +#include "sync-httpd2_get-backups-KEY-blocks.h" + + +/** + * @brief A handler for a backup sub-operation under /backups/$KEY/. + * + * Each entry maps an operation name ("block" or "object") and an + * HTTP method to a handler function. + */ +struct SH_BackupOperationHandler +{ + + /** + * Operation name, e.g. "block" or "object". + */ + const char *operation; + + /** + * HTTP method this handler is for. + */ + enum MHD_HTTP_Method method; + + /** + * Function to call. + * + * @param request the MHD request to handle + * @param account_pub public key of the account + * @param args path segments after the operation name + * @param upload_size number of bytes being uploaded + * @param is_update whether the operation is an update + * @return MHD action + */ + const struct MHD_Action *(*handler)( + struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update); + + /** + * Number of path segments this handler expects + * after the operation name. + */ + unsigned int nargs; + + /** + * Is @e nargs only an upper bound? + */ + bool nargs_is_upper_bound; +}; /** @@ -38,6 +92,43 @@ static int SH_sync_connection_close; unsigned long long int SH_upload_limit_mb; /** + * How many uploads of the maximum permitted size the shared upload + * buffer pool must have room for. + * + * Block uploads are handed to the handler in one piece + * (#MHD_action_process_upload_full()), so MHD buffers the whole body + * out of a daemon-wide pool. The pool is a budget, not a + * pre-allocation -- MHD mallocs per request and gives the space back + * when the request ends -- so this bounds the worst-case resident size + * of concurrent uploads rather than costing anything up front. + */ +#define UPLOAD_POOL_CONCURRENCY 8 + + +bool +SH_upload_too_large (uint_fast64_t upload_size, + size_t *buff_size) +{ + uint_fast64_t limit; + + limit = ((uint_fast64_t) SH_upload_limit_mb) * 1024 * 1024; + if (MHD_SIZE_UNKNOWN == upload_size) + { + /* Chunked upload: the length is not known yet, so bound the buffer + by the limit and let MHD fail the request if it does not fit. */ + *buff_size = (size_t) GNUNET_MIN (limit, + (uint_fast64_t) SIZE_MAX); + return false; + } + if ( (upload_size >= limit) || + (upload_size > SIZE_MAX) ) + return true; + *buff_size = (size_t) upload_size; + return false; +} + + +/** * Annual fee for the backup account. */ struct TALER_Amount SH_annual_fee; @@ -183,6 +274,176 @@ respond_agpl (struct MHD_Request *request, /** + * Maximum length of the encoded account public key in the URL. + * An EdDSA public key encodes to 52 characters. + */ +#define ACCOUNT_KEY_MAX 64 + +/** + * Maximum reasonable subpath length after /backups/$KEY/. + */ +#define SUBPATH_MAX 256 + +/** + * Maximum number of path segments we split a subpath into, + * including the operation name. + */ +#define SUBPATH_MAX_SEGMENTS 3 + + +/** + * Handle a request on a subpath under /backups/$KEY/. + * + * Parses the subpath (e.g. "block/NONCE" or "object/UID"), + * splits it into segments, and dispatches to the appropriate + * handler using the data-driven #SH_BackupOperationHandler + * table. + * + * @param request the MHD request to handle + * @param account_pub public key of the account + * @param method HTTP method of the request + * @param subpath remainder of the URL after /backups/$KEY/ + * @param upload_size number of bytes being uploaded + * @return MHD action + */ +static const struct MHD_Action * +handle_backup_subpath (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + enum MHD_HTTP_Method method, + const char *subpath, + uint_fast64_t upload_size) +{ + static struct SH_BackupOperationHandler bhandlers[] = { + { + .operation = "blocks", + .method = MHD_HTTP_METHOD_POST, + .handler = &SH_backup_block_post, + .nargs = 1, + }, + { + .operation = "blocks", + /* FIXME: replace with PATCH when mhd2 supports it */ + .method = MHD_HTTP_METHOD_PUT, + .handler = &SH_backup_block_post, + .nargs = 1, + }, + { + .operation = "blocks", + .method = MHD_HTTP_METHOD_DELETE, + .handler = &SH_backup_block_delete, + .nargs = 1, + }, + { + .operation = "blocks", + .method = MHD_HTTP_METHOD_GET, + .handler = &SH_backup_blocks_get, + .nargs = 0, + }, + { + .operation = "objects", + .method = MHD_HTTP_METHOD_GET, + .handler = &SH_backup_object_get, + .nargs = 1, + }, + { + .operation = "objects", + .method = MHD_HTTP_METHOD_POST, + .handler = &SH_backup_object_post, + .nargs = 1, + }, + { .operation = NULL } + }; + size_t slen; + char d[SUBPATH_MAX]; + char *sp; + /* NULL-terminated, hence one more entry than we ever fill */ + const char *args[SUBPATH_MAX_SEGMENTS + 1] = { NULL }; + unsigned int i; + + GNUNET_assert (NULL != request); + GNUNET_assert (NULL != subpath); + slen = strlen (subpath); + if (slen >= SUBPATH_MAX) + { + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_URI_TOO_LONG, + TALER_EC_GENERIC_URI_TOO_LONG, + subpath); + } + memcpy (d, + subpath, + slen + 1); + + i = 0; + args[i++] = strtok_r (d, + "/", + &sp); + while ( (NULL != args[i - 1]) && + (i < SUBPATH_MAX_SEGMENTS) ) + args[i++] = strtok_r (NULL, + "/", + &sp); + + if (NULL == args[0]) + { + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + subpath); + } + + if (MHD_HTTP_METHOD_OPTIONS == method) + return TALER_MHD2_reply_cors_preflight (request); + + for (unsigned int j = 0; NULL != bhandlers[j].operation; j++) + { + const struct SH_BackupOperationHandler *bh = &bhandlers[j]; + unsigned int nargs; + + if (0 != strcmp (args[0], + bh->operation)) + continue; + if (bh->method != method) + continue; + + /* Count path segments after the operation name */ + nargs = 0; + while (NULL != args[nargs + 1]) + nargs++; + + if (bh->nargs_is_upper_bound + ? (nargs > bh->nargs) + : (nargs != bh->nargs)) + { + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + subpath); + } + return bh->handler (request, + account_pub, + &args[1], + upload_size, + /* FIXME: replace with PATCH when mhd2 supports it */ + MHD_HTTP_METHOD_PUT == bh->method); + } + + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + args[0]); +} + + +/** * A client has requested the given url using the given method * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT, * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). The callback @@ -191,14 +452,14 @@ respond_agpl (struct MHD_Request *request, * * @param cls argument given together with the function * pointer when the handler was registered with MHD - * @param request request the request to handle + * @param request the request to handle * @param path the requested uri (without arguments after "?") * @param method the HTTP method used (#MHD_HTTP_METHOD_GET, * #MHD_HTTP_METHOD_PUT, etc.) * @param upload_size the size of the message upload content payload, - * #MHD_SIZE_UNKNOWN for chunked uploads (if the - * final chunk has not been processed yet) - * @return next action + * #MHD_SIZE_UNKNOWN for chunked uploads (if the final chunk + * has not been processed yet) + * @return next action */ static const struct MHD_Action * url_handler (void *cls, @@ -231,46 +492,82 @@ url_handler (void *cls, struct SYNC_AccountPublicKeyP account_pub; (void) cls; -#if BUG - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling %s request for URL '%s'\n", - MHD_http_method_to_string (method)->cstr, - path->cstr); -#endif + /* Backup API */ if (0 == strncmp (path->cstr, "/backups/", strlen ("/backups/"))) { - const char *ac = &path->cstr[strlen ("/backups/")]; + const char *ac = path->cstr + strlen ("/backups/"); + const char *next_slash; + size_t ac_len; + + /* Find end of account key (next '/' or end of string) */ + next_slash = strchr (ac, + '/'); + if (NULL == next_slash) + ac_len = strlen (ac); + else + ac_len = next_slash - ac; - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_public_key_from_string (ac, - strlen (ac), - &account_pub.eddsa_pub)) { - GNUNET_break_op (0); - return TALER_MHD2_reply_with_error (request, - MHD_HTTP_STATUS_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - ac); - } - if (MHD_HTTP_METHOD_OPTIONS == method) - { - return TALER_MHD2_reply_cors_preflight (request); - } - if (MHD_HTTP_METHOD_GET == method) - { - return SH_backup_get (request, - &account_pub); + /* Bound before we put it on the stack; the segment is + client-controlled */ + char ac_str[ACCOUNT_KEY_MAX + 1]; + + if (ac_len > ACCOUNT_KEY_MAX) + { + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "account public key"); + } + memcpy (ac_str, + ac, + ac_len); + ac_str[ac_len] = '\0'; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_public_key_from_string ( + ac_str, + ac_len, + &account_pub.eddsa_pub)) + { + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ac_str); + } } - if (MHD_HTTP_METHOD_POST == method) + + /* Without a subpath this is the account itself; only GET makes + sense on it, everything else needs an operation */ + if ( (NULL == next_slash) || + ('\0' == next_slash[1]) ) { - return SH_backup_post (request, - &account_pub, - upload_size); + if (MHD_HTTP_METHOD_OPTIONS == method) + return TALER_MHD2_reply_cors_preflight (request); + if (MHD_HTTP_METHOD_GET == method) + return SH_backup_account_get (request, + &account_pub); + GNUNET_break_op (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_GENERIC_ENDPOINT_UNKNOWN, + path->cstr); } - // FIXME: return bad method! + + /* Dispatch to sub-handler */ + return handle_backup_subpath (request, + &account_pub, + method, + next_slash + 1, + upload_size); } + for (unsigned int i = 0; NULL != handlers[i].url; i++) { struct SH_RequestHandler *rh = &handlers[i]; @@ -304,7 +601,6 @@ do_shutdown (void *cls) { (void) cls; TALER_MHD2_daemons_halt (); - SH_resume_all_bc (); if (NULL != SH_ctx) { GNUNET_CURL_fini (SH_ctx); @@ -326,9 +622,9 @@ do_shutdown (void *cls) * we made progress serving a request. This function re-schedules * the task processing MHD's activities to run immediately. */ -// FIXME: replace with direct call... +/* FIXME: replace with direct call... */ void -SH_trigger_daemon () +SH_trigger_daemon (void) { TALER_MHD2_daemons_trigger (); } @@ -338,7 +634,7 @@ SH_trigger_daemon () * Kick GNUnet Curl scheduler to begin curl interactions. */ void -SH_trigger_curl () +SH_trigger_curl (void) { GNUNET_CURL_gnunet_scheduler_reschedule (&rc); } @@ -369,10 +665,23 @@ start_daemon (void *cls, GNUNET_SCHEDULER_shutdown (); return; } + /* Without this, the pool defaults to a fraction of the memory the + daemon would use for connections -- about 1 MB, far below + UPLOAD_LIMIT_MB -- and MHD answers anything bigger with its own + 413 before the handler ever runs, whatever the configured limit + says. See the FIXME in sync-httpd2_post-backups-KEY-blocks-NONCE.c + for the incremental-upload alternative that would drop the pool + dependency entirely. */ GNUNET_assert (MHD_SC_OK == MHD_DAEMON_SET_OPTIONS ( mhd, MHD_D_OPTION_DEFAULT_TIMEOUT_MILSEC (10000), + MHD_D_OPTION_LARGE_POOL_SIZE ( + (size_t) GNUNET_MIN ( + ((uint_fast64_t) SH_upload_limit_mb) + * 1024 * 1024 + * UPLOAD_POOL_CONCURRENCY, + (uint_fast64_t) SIZE_MAX)), MHD_D_OPTION_LISTEN_SOCKET (lsock))); have_daemons = true; TALER_MHD2_daemon_start (mhd); @@ -453,6 +762,43 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } + /* The merchant backend API expects the base URL to end with '/' + (see TALER_url_join), so ensure it does. */ + if ('/' != SH_backend_url[strlen (SH_backend_url) - 1]) + { + char *tmp; + + GNUNET_asprintf (&tmp, + "%s/", + SH_backend_url); + GNUNET_free (SH_backend_url); + SH_backend_url = tmp; + } + /* The payment flow turns this into a "taler://" URI, so it needs a + scheme and a host */ + { + const char *host = NULL; + + if (0 == strncasecmp ("https://", + SH_backend_url, + strlen ("https://"))) + host = &SH_backend_url[strlen ("https://")]; + else if (0 == strncasecmp ("http://", + SH_backend_url, + strlen ("http://"))) + host = &SH_backend_url[strlen ("http://")]; + if ( (NULL == host) || + ('\0' == *host) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "sync", + "PAYMENT_BACKEND_URL", + "must be an http:// or https:// URL" + " with a host name"); + GNUNET_SCHEDULER_shutdown (); + return; + } + } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (config, "sync", @@ -461,7 +807,7 @@ run (void *cls, { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "sync", - "BASE_URL"); + "FULFILLMENT_URL"); GNUNET_SCHEDULER_shutdown (); return; } @@ -469,6 +815,14 @@ run (void *cls, /* setup HTTP client event loop */ SH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc); + if (NULL == SH_ctx) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to initialize HTTP client, exiting.\n"); + global_ret = EXIT_NO_RESTART; + GNUNET_SCHEDULER_shutdown (); + return; + } rc = GNUNET_CURL_gnunet_rc_create (SH_ctx); if (NULL != userpass) GNUNET_CURL_set_userpass (SH_ctx, @@ -503,8 +857,7 @@ run (void *cls, } if (GNUNET_OK != - SYNCDB_init (config, - false)) + SYNCDB_init (config)) { global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); @@ -566,6 +919,13 @@ main (int argc, "USERNAME:PASSWORD", "use the given USERNAME and PASSWORD for client authentication", &userpass), + /* Note: -c, -h, -L, -l and -v are reserved by GNUNET_PROGRAM_run(). + We use -E for the client certificate, as curl does. */ + GNUNET_GETOPT_option_string ('E', + "cert", + "CERTFILE", + "file with the TLS client certificate for TLS client authentication", + &certfile), GNUNET_GETOPT_option_flag ('C', "connection-close", "force HTTP connections to be closed after each request", diff --git a/src/sync/sync-httpd2.h b/src/sync/sync-httpd2.h @@ -24,7 +24,32 @@ #include "platform.h" #include <microhttpd2.h> #include <taler/taler_mhd2_lib.h> -#include "sync/sync_database_lib.h" +#include <sync/sync_database_lib.h> + +/* FIXME: implement in exchange */ +#ifndef TALER_EC_SYNC_CONFLICTING_WRITE +#define TALER_EC_SYNC_CONFLICTING_WRITE 6115 +#endif +#ifndef TALER_EC_SYNC_INCONSISTENT_BACKUP +#define TALER_EC_SYNC_INCONSISTENT_BACKUP 6116 +#endif + + +/** + * Like #TALER_MHD2_make_error() but with an explicit HTTP status, + * for error codes that do not have a registered HTTP mapping yet. + * + * @param sc HTTP status code to use + * @param ec Taler error code to report + * @param detail human-readable detail, or NULL + */ +#define SYNC_MHD_MAKE_ERROR(sc,ec,detail) \ + TALER_MHD2_MAKE_JSON_PACK ((sc), \ + TALER_MHD2_PACK_EC (ec), \ + GNUNET_JSON_pack_allow_null ( \ + GNUNET_JSON_pack_string ("detail", \ + (detail)))) + /** * @brief Struct describing an URL and the handler for it. @@ -51,7 +76,7 @@ struct SH_RequestHandler */ const struct MHD_Action * (*handler)(struct MHD_Request *request, - size_t upload_size); + uint_fast64_t upload_size); }; @@ -102,6 +127,25 @@ struct TM_HandlerContext extern unsigned long long SH_upload_limit_mb; /** + * Check a request body against the configured upload limit and + * determine the upload buffer size to hand to MHD. + * + * MHD reports #MHD_SIZE_UNKNOWN while a chunked upload is still being + * received, so the length cannot be checked up-front in that case; the + * buffer is then bounded by the configured limit instead, which caps + * how much a client can send before MHD gives up. + * + * @param upload_size size reported by MHD, #MHD_SIZE_UNKNOWN if unknown + * @param[out] buff_size buffer size to pass to MHD, set unless the + * upload is rejected + * @return true if the upload is already known to exceed the limit + */ +bool +SH_upload_too_large (uint_fast64_t upload_size, + size_t *buff_size); + + +/** * Annual fee for the backup account. */ extern struct TALER_Amount SH_annual_fee; diff --git a/src/sync/sync-httpd2_backup.c b/src/sync/sync-httpd2_backup.c @@ -1,237 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019--2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync-httpd2_backup.c - * @brief functions to handle incoming requests for backups - * @author Christian Grothoff - */ -#include "platform.h" -#include "sync-httpd2.h" -#include <gnunet/gnunet_util_lib.h> -#include "sync-httpd2_backup.h" - - -/** - * Handle request on @a request for retrieval of the latest - * backup of @a account. - * - * @param request the MHD request to handle - * @param account public key of the account the request is for - * @return MHD action - */ -const struct MHD_Action * -SH_backup_get (struct MHD_Request *request, - const struct SYNC_AccountPublicKeyP *account) -{ - struct GNUNET_HashCode backup_hash; - enum SYNC_DB_QueryStatus qs; - - qs = SYNCDB_lookup_account_TR ( - account, - &backup_hash); - switch (qs) - { - case SYNC_DB_OLD_BACKUP_MISSING: - GNUNET_break (0); - return TALER_MHD2_reply_with_error (request, - MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - case SYNC_DB_OLD_BACKUP_MISMATCH: - GNUNET_break (0); - return TALER_MHD2_reply_with_error (request, - MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - case SYNC_DB_PAYMENT_REQUIRED: - return TALER_MHD2_reply_with_error (request, - MHD_HTTP_STATUS_NOT_FOUND, - TALER_EC_SYNC_ACCOUNT_UNKNOWN, - NULL); - case SYNC_DB_HARD_ERROR: - return TALER_MHD2_reply_with_error (request, - MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case SYNC_DB_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD2_reply_with_error (request, - MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - case SYNC_DB_NO_RESULTS: - { - struct MHD_Response *resp; - - resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT); - TALER_MHD2_add_global_headers (resp, - false); - return MHD_action_from_response (request, - resp); - } - case SYNC_DB_ONE_RESULT: - { - struct MHD_StringNullable inm; - - if ( (MHD_YES == - MHD_request_get_value (request, - MHD_VK_HEADER, - MHD_HTTP_HEADER_IF_NONE_MATCH, - &inm)) && - (2 < inm.len) && - ('"' == inm.cstr[0]) && - ('=' == inm.cstr[inm.len - 1]) ) - { - struct GNUNET_HashCode inm_h; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (inm.cstr + 1, - inm.len - 2, - &inm_h, - sizeof (inm_h))) - { - GNUNET_break_op (0); - return TALER_MHD2_reply_with_error (request, - MHD_HTTP_STATUS_BAD_REQUEST, - TALER_EC_SYNC_BAD_IF_NONE_MATCH, - "Etag does not include a base32-encoded SHA-512 hash"); - } - if (0 == GNUNET_memcmp (&inm_h, - &backup_hash)) - { - struct MHD_Response *resp; - - resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); - TALER_MHD2_add_global_headers (resp, - false); - return MHD_action_from_response (request, - resp); - } - } - } - /* We have a result, should fetch and return it! */ - break; - } - return MHD_action_from_response (request, - SH_make_backup (account, - MHD_HTTP_STATUS_OK)); -} - - -/** - * Return the current backup of @a account on @a request - * using @a default_http_status on success. - * - * @param account account to query - * @param default_http_status HTTP status to queue response - * with on success (#MHD_HTTP_STATUS_OK or #MHD_HTTP_STATUS_CONFLICT) - * @return MHD response - */ -struct MHD_Response * -SH_make_backup (const struct SYNC_AccountPublicKeyP *account, - enum MHD_HTTP_StatusCode default_http_status) -{ - enum SYNC_DB_QueryStatus qs; - struct MHD_Response *resp; - struct SYNC_AccountSignatureP account_sig; - struct GNUNET_HashCode backup_hash; - struct GNUNET_HashCode prev_hash; - size_t backup_size; - void *backup; - - qs = SYNCDB_lookup_backup_TR ( - account, - &account_sig, - &prev_hash, - &backup_hash, - &backup_size, - &backup); - switch (qs) - { - case SYNC_DB_OLD_BACKUP_MISSING: - GNUNET_break (0); - return TALER_MHD2_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected return status (backup missing)"); - case SYNC_DB_OLD_BACKUP_MISMATCH: - GNUNET_break (0); - return TALER_MHD2_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected return status (backup mismatch)"); - case SYNC_DB_PAYMENT_REQUIRED: - GNUNET_break (0); - return TALER_MHD2_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected return status (payment required)") - ; - case SYNC_DB_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case SYNC_DB_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - case SYNC_DB_NO_RESULTS: - GNUNET_break (0); - /* Note: can theoretically happen due to non-transactional nature if - the backup expired / was gc'ed JUST between the two SQL calls. - But too rare to handle properly, as doing a transaction would be - expensive. Just admit to failure ;-) */ - return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - NULL); - case SYNC_DB_ONE_RESULT: - /* interesting case below */ - break; - } - resp = MHD_response_from_buffer (default_http_status, - backup_size, - backup, - &free, - backup); - TALER_MHD2_add_global_headers (resp, - false); - { - char *sig_s; - char *prev_s; - char *etag; - char *etagq; - - sig_s = GNUNET_STRINGS_data_to_string_alloc (&account_sig, - sizeof (account_sig)); - prev_s = GNUNET_STRINGS_data_to_string_alloc (&prev_hash, - sizeof (prev_hash)); - etag = GNUNET_STRINGS_data_to_string_alloc (&backup_hash, - sizeof (backup_hash)); - GNUNET_break (MHD_SC_OK == - MHD_response_add_header (resp, - "Sync-Signature", - sig_s)); - GNUNET_break (MHD_SC_OK == - MHD_response_add_header (resp, - "Sync-Previous", - prev_s)); - GNUNET_asprintf (&etagq, - "\"%s\"", - etag); - GNUNET_break (MHD_SC_OK == - MHD_response_add_header (resp, - MHD_HTTP_HEADER_ETAG, - etagq)); - GNUNET_free (etagq); - GNUNET_free (etag); - GNUNET_free (prev_s); - GNUNET_free (sig_s); - } - return resp; -} diff --git a/src/sync/sync-httpd2_backup.h b/src/sync/sync-httpd2_backup.h @@ -1,74 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014--2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync-httpd2_backup.h - * @brief functions to handle incoming requests on /backup/ - * @author Christian Grothoff - */ -#ifndef SYNC_HTTPD2_BACKUP_H -#define SYNC_HTTPD2_BACKUP_H - -#include <microhttpd2.h> - -/** - * Service is shutting down, resume all MHD requests NOW. - */ -void -SH_resume_all_bc (void); - - -/** - * Return the current backup of @a account on @a request - * using @a default_http_status on success. - * - * @param account account to query - * @param default_http_status HTTP status to queue response - * with on success (#MHD_HTTP_STATUS_OK or #MHD_HTTP_STATUS_CONFLICT) - * @return MHD response - */ -struct MHD_Response * -SH_make_backup (const struct SYNC_AccountPublicKeyP *account, - enum MHD_HTTP_StatusCode default_http_status); - - -/** - * Handle request on @a request for retrieval of the latest - * backup of @a account. - * - * @param request the MHD request to handle - * @param account public key of the account the request is for - * @return MHD action - */ -const struct MHD_Action * -SH_backup_get (struct MHD_Request *request, - const struct SYNC_AccountPublicKeyP *account); - - -/** - * Handle POST /backup requests. - * - * @param request the MHD request to handle - * @param account public key of the account the request is for - * @param upload_size number of bytes being uploaded - * @return MHD result code - */ -const struct MHD_Action * -SH_backup_post (struct MHD_Request *request, - const struct SYNC_AccountPublicKeyP *account, - uint_fast64_t upload_size); - - -#endif diff --git a/src/sync/sync-httpd2_backup_post.c b/src/sync/sync-httpd2_backup_post.c @@ -1,1013 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019--2025 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync-httpd_backup_post.c - * @brief functions to handle incoming requests for backups - * @author Christian Grothoff - */ -#include "platform.h" -struct BackupContext; -#define TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE struct BackupContext -#define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE struct BackupContext -#include "sync-httpd2.h" -#include <gnunet/gnunet_util_lib.h> -#include "sync-httpd2_backup.h" -#include <microhttpd2.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_mhd2_lib.h> -#include <taler/taler_merchant_service.h> -#include <taler/taler_signatures.h> - - -/** - * How long do we hold an HTTP client connection if - * we are awaiting payment before giving up? - */ -#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_MINUTES, 30) - - -/** - * Context for an upload operation. - */ -struct BackupContext -{ - - /** - * Context for cleanup logic. - */ - struct TM_HandlerContext hc; - - /** - * Signature of the account holder. - */ - struct SYNC_AccountSignatureP account_sig; - - /** - * Public key of the account holder. - */ - struct SYNC_AccountPublicKeyP account; - - /** - * Hash of the previous upload, or zeros if first upload. - */ - struct GNUNET_HashCode old_backup_hash; - - /** - * Hash of the upload we are receiving right now (as promised - * by the client, to be verified!). - */ - struct GNUNET_HashCode new_backup_hash; - - /** - * Claim token, all zeros if not known. Only set if @e existing_order_id is non-NULL. - */ - struct TALER_ClaimTokenP token; - - /** - * Kept in DLL for shutdown handling while suspended. - */ - struct BackupContext *next; - - /** - * Kept in DLL for shutdown handling while suspended. - */ - struct BackupContext *prev; - - /** - * Used while suspended for resumption. - */ - struct MHD_Request *request; - - /** - * To be returned upon resuming. - */ - struct MHD_Response *resp; - - /** - * Used while we are awaiting proposal creation. - */ - struct TALER_MERCHANT_PostPrivateOrdersHandle *po; - - /** - * Used while we are waiting payment. - */ - struct TALER_MERCHANT_GetPrivateOrderHandle *omgh; - - /** - * Order under which the client promised payment, or NULL. - */ - const char *order_id; - - /** - * Order ID for the client that we found in our database. - */ - char *existing_order_id; - - /** - * Timestamp of the order in @e existing_order_id. Used to - * select the most recent unpaid offer. - */ - struct GNUNET_TIME_Timestamp existing_order_timestamp; - - /** - * Do not look for an existing order, force a fresh order to be created. - */ - bool force_fresh_order; -}; - - -/** - * Kept in DLL for shutdown handling while suspended. - */ -static struct BackupContext *bc_head; - -/** - * Kept in DLL for shutdown handling while suspended. - */ -static struct BackupContext *bc_tail; - - -/** - * Service is shutting down, resume all MHD requests NOW. - */ -void -SH_resume_all_bc () -{ - struct BackupContext *bc; - - while (NULL != (bc = bc_head)) - { - GNUNET_CONTAINER_DLL_remove (bc_head, - bc_tail, - bc); - MHD_request_resume (bc->request); - if (NULL != bc->po) - { - TALER_MERCHANT_post_private_orders_cancel (bc->po); - bc->po = NULL; - } - if (NULL != bc->omgh) - { - TALER_MERCHANT_get_private_order_cancel (bc->omgh); - bc->omgh = NULL; - } - } -} - - -/** - * Function called to clean up a backup context. - * - * @param cls the `struct BackupContext` - * @param data the details about the event - * @param request_app_context the application request context - */ -static void -cleanup_ctx (void *cls, - const struct MHD_RequestEndedData *data, - void *request_app_context) -{ - struct BackupContext *bc = cls; - - GNUNET_assert (cls == request_app_context); - if (NULL != bc->po) - { - TALER_MERCHANT_post_private_orders_cancel (bc->po); - bc->po = NULL; - } - if (NULL != bc->omgh) - { - TALER_MERCHANT_get_private_order_cancel (bc->omgh); - bc->omgh = NULL; - } - GNUNET_free (bc->existing_order_id); - GNUNET_free (bc); -} - - -/** - * Transmit a payment request for @a order_id on @a request - * - * @param order_id our backend's order ID - * @param token the claim token generated by the merchant (NULL if - * it wasn't generated). - * @return MHD response to use - */ -static struct MHD_Response * -make_payment_request (const char *order_id, - const struct TALER_ClaimTokenP *token) -{ - struct MHD_Response *resp; - - /* request payment via Taler */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Creating payment request for order `%s'\n", - order_id); - resp = MHD_response_from_empty (MHD_HTTP_STATUS_PAYMENT_REQUIRED); - TALER_MHD2_add_global_headers (resp, - false); - { - char *hdr; - const char *pfx; - char *hn; - struct GNUNET_Buffer hdr_buf = { 0 }; - - if (0 == strncasecmp ("https://", - SH_backend_url, - strlen ("https://"))) - { - pfx = "taler://"; - hn = &SH_backend_url[strlen ("https://")]; - } - else if (0 == strncasecmp ("http://", - SH_backend_url, - strlen ("http://"))) - { - pfx = "taler+http://"; - hn = &SH_backend_url[strlen ("http://")]; - } - else - { - GNUNET_break (0); - MHD_response_destroy (resp); - return NULL; - } - if (0 == strlen (hn)) - { - GNUNET_break (0); - MHD_response_destroy (resp); - return NULL; - } - - GNUNET_buffer_write_str (&hdr_buf, pfx); - GNUNET_buffer_write_str (&hdr_buf, "pay/"); - GNUNET_buffer_write_str (&hdr_buf, hn); - GNUNET_buffer_write_path (&hdr_buf, order_id); - /* No session ID */ - GNUNET_buffer_write_path (&hdr_buf, ""); - if (NULL != token) - { - GNUNET_buffer_write_str (&hdr_buf, "?c="); - GNUNET_buffer_write_data_encoded (&hdr_buf, - token, - sizeof (*token)); - } - hdr = GNUNET_buffer_reap_str (&hdr_buf); - GNUNET_break (MHD_SC_OK == - MHD_response_add_header (resp, - "Taler", - hdr)); - GNUNET_free (hdr); - } - return resp; -} - - -/** - * Callbacks of this type are used to serve the result of submitting a - * /contract request to a merchant. - * - * @param cls our `struct BackupContext` - * @param por response details - */ -static void -proposal_cb ( - struct BackupContext *bc, - const struct TALER_MERCHANT_PostPrivateOrdersResponse *por) -{ - enum SYNC_DB_QueryStatus qs; - - bc->po = NULL; - GNUNET_CONTAINER_DLL_remove (bc_head, - bc_tail, - bc); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming request with order `%s'\n", - bc->order_id); - MHD_request_resume (bc->request); - SH_trigger_daemon (); - if (MHD_HTTP_STATUS_OK != por->hr.http_status) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Backend returned status %u/%u\n", - por->hr.http_status, - (unsigned int) por->hr.ec); - GNUNET_break_op (0); - bc->resp = TALER_MHD2_MAKE_JSON_PACK ( - MHD_HTTP_STATUS_BAD_GATEWAY, - TALER_JSON_pack_ec (TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR), - GNUNET_JSON_pack_uint64 ("backend-ec", - por->hr.ec), - GNUNET_JSON_pack_uint64 ("backend-http-status", - por->hr.http_status), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("backend-reply", - (json_t *) por->hr.reply))); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Storing payment request for order `%s'\n", - por->details.ok.order_id); - qs = SYNCDB_store_payment_TR ( - &bc->account, - por->details.ok.order_id, - por->details.ok.token, - &SH_annual_fee); - if (0 >= qs) - { - GNUNET_break (0); - bc->resp = TALER_MHD2_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "Failed to persist payment request in sync database"); - GNUNET_assert (NULL != bc->resp); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Obtained fresh order `%s'\n", - por->details.ok.order_id); - bc->resp = make_payment_request (por->details.ok.order_id, - por->details.ok.token); - GNUNET_assert (NULL != bc->resp); -} - - -/** - * Function called on all pending payments for the right - * account. - * - * @param cls closure, our `struct BackupContext` - * @param timestamp for how long have we been waiting - * @param order_id order id in the backend - * @param token claim token to use (or NULL for none) - * @param amount how much is the order for - */ -static void -ongoing_payment_cb (void *cls, - struct GNUNET_TIME_Timestamp timestamp, - const char *order_id, - const struct TALER_ClaimTokenP *token, - const struct TALER_Amount *amount) -{ - struct BackupContext *bc = cls; - - (void) amount; - if (0 != TALER_amount_cmp (amount, - &SH_annual_fee)) - return; /* can't reuse, fees changed */ - if ( (NULL == bc->existing_order_id) || - (GNUNET_TIME_timestamp_cmp (bc->existing_order_timestamp, - <, - timestamp)) ) - { - GNUNET_free (bc->existing_order_id); - bc->existing_order_id = GNUNET_strdup (order_id); - bc->existing_order_timestamp = timestamp; - if (NULL != token) - bc->token = *token; - } -} - - -/** - * Callback to process a GET /check-payment request - * - * @param cls our `struct BackupContext` - * @param osr order status - */ -static void -check_payment_cb ( - struct BackupContext *bc, - const struct TALER_MERCHANT_GetPrivateOrderResponse *osr) -{ - const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; - - /* refunds are not supported, verify */ - bc->omgh = NULL; - GNUNET_CONTAINER_DLL_remove (bc_head, - bc_tail, - bc); - MHD_request_resume (bc->request); - SH_trigger_daemon (); - switch (hr->http_status) - { - case 0: - /* Likely timeout, complain! */ - bc->resp = TALER_MHD2_make_error ( - TALER_EC_SYNC_GENERIC_BACKEND_TIMEOUT, - NULL); - return; - case MHD_HTTP_STATUS_OK: - break; /* handled below */ - default: - /* Unexpected backend response */ - bc->resp = TALER_MHD2_MAKE_JSON_PACK ( - MHD_HTTP_STATUS_BAD_GATEWAY, - GNUNET_JSON_pack_uint64 ("code", - TALER_EC_SYNC_GENERIC_BACKEND_ERROR), - GNUNET_JSON_pack_string ("hint", - TALER_ErrorCode_get_hint ( - TALER_EC_SYNC_GENERIC_BACKEND_ERROR)), - GNUNET_JSON_pack_uint64 ("backend-ec", - (json_int_t) hr->ec), - GNUNET_JSON_pack_uint64 ("backend-http-status", - (json_int_t) hr->http_status), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("backend-reply", - (json_t *) hr->reply))); - return; - } - - GNUNET_assert (MHD_HTTP_STATUS_OK == hr->http_status); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment status checked: %d\n", - osr->details.ok.status); - switch (osr->details.ok.status) - { - case TALER_MERCHANT_OSC_PAID: - { - enum SYNC_DB_QueryStatus qs; - - qs = SYNCDB_increment_lifetime_TR ( - &bc->account, - bc->order_id, - GNUNET_TIME_UNIT_YEARS); /* always annual */ - if (0 <= qs) - return; /* continue as planned */ - GNUNET_break (0); - bc->resp = TALER_MHD2_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "increment lifetime"); - GNUNET_assert (NULL != bc->resp); - return; /* continue as planned */ - } - case TALER_MERCHANT_OSC_UNPAID: - case TALER_MERCHANT_OSC_CLAIMED: - break; - } - if (NULL != bc->existing_order_id) - { - /* repeat payment request */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Repeating payment request\n"); - bc->resp = make_payment_request (bc->existing_order_id, - (GNUNET_YES == GNUNET_is_zero (&bc->token)) - ? NULL - : &bc->token); - GNUNET_assert (NULL != bc->resp); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Timeout waiting for payment\n"); - bc->resp = TALER_MHD2_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT, - "Timeout awaiting promised payment"); - GNUNET_assert (NULL != bc->resp); -} - - -/** - * Helper function used to ask our backend to await - * a payment for the user's account. - * - * @param bc context to begin payment for. - * @param timeout when to give up trying - * @param order_id which order to check for the payment - */ -static void -await_payment (struct BackupContext *bc, - struct GNUNET_TIME_Relative timeout, - const char *order_id) -{ - GNUNET_CONTAINER_DLL_insert (bc_head, - bc_tail, - bc); - bc->order_id = order_id; - bc->omgh = TALER_MERCHANT_get_private_order_create ( - SH_ctx, - SH_backend_url, - order_id); - GNUNET_assert (GNUNET_OK == - TALER_MERCHANT_get_private_order_set_options ( - bc->omgh, - TALER_MERCHANT_get_private_order_option_timeout (timeout))); - GNUNET_assert (TALER_EC_NONE == - TALER_MERCHANT_get_private_order_start ( - bc->omgh, - &check_payment_cb, - bc)); - SH_trigger_curl (); -} - - -/** - * Helper function used to ask our backend to begin - * processing a payment for the user's account. - * May perform asynchronous operations by suspending the request - * if required. - * - * @param bc context to begin payment for. - * @param pay_req #GNUNET_YES if payment was explicitly requested, - * #GNUNET_NO if payment is needed - * @param[out] suspend set to true if the request should be suspended - * @return MHD response - */ -static struct MHD_Response * -begin_payment (struct BackupContext *bc, - int pay_req, - bool *suspend) -{ - *suspend = false; - if (! bc->force_fresh_order) - { - enum GNUNET_DB_QueryStatus qs; - - qs = SYNCDB_lookup_pending_payments_by_account_TR ( - &bc->account, - &ongoing_payment_cb, - bc); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, - "pending payments"); - } - if (NULL != bc->existing_order_id) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Have existing order, waiting for `%s' to complete\n", - bc->existing_order_id); - await_payment (bc, - GNUNET_TIME_UNIT_ZERO /* no long polling */, - bc->existing_order_id); - *suspend = true; - return NULL; - } - } - GNUNET_CONTAINER_DLL_insert (bc_head, - bc_tail, - bc); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending request while creating order at `%s'\n", - SH_backend_url); - *suspend = true; - { - json_t *order; - - order = GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - &SH_annual_fee), - GNUNET_JSON_pack_string ("summary", - "annual fee for sync service"), - GNUNET_JSON_pack_string ("fulfillment_url", - SH_fulfillment_url)); - bc->po = TALER_MERCHANT_post_private_orders_create (SH_ctx, - SH_backend_url, - order); - json_decref (order); - } - GNUNET_assert (TALER_EC_NONE == - TALER_MERCHANT_post_private_orders_start ( - bc->po, - &proposal_cb, - bc)); - SH_trigger_curl (); - return NULL; -} - - -/** - * We got some query status from the DB. Handle the error cases. - * May perform asynchronous operations by suspending the request - * if required. - * - * @param bc request to handle status for - * @param qs query status to handle - * @param[out] suspend set to true if the request should be suspended - * @return MHD response to return - */ -static struct MHD_Response * -handle_database_error (struct BackupContext *bc, - enum SYNC_DB_QueryStatus qs, - bool *suspend) -{ - *suspend = false; - switch (qs) - { - case SYNC_DB_OLD_BACKUP_MISSING: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Update failed: no existing backup\n"); - return TALER_MHD2_make_error (TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN, - NULL); - case SYNC_DB_OLD_BACKUP_MISMATCH: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Conflict detected, returning existing backup\n"); - return SH_make_backup (&bc->account, - MHD_HTTP_STATUS_CONFLICT); - case SYNC_DB_PAYMENT_REQUIRED: - { - struct MHD_StringNullable order_id; - - if ( (MHD_YES != - MHD_request_get_value (bc->request, - MHD_VK_URI_QUERY_PARAM, - "paying", - &order_id)) || - (NULL == order_id.cstr) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment required, starting payment process\n"); - return begin_payment (bc, - GNUNET_NO, - suspend); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment required, awaiting completion of `%s'\n", - order_id.cstr); - await_payment (bc, - CHECK_PAYMENT_GENERIC_TIMEOUT, - order_id.cstr); - *suspend = true; - return NULL; - } - case SYNC_DB_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - case SYNC_DB_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - case SYNC_DB_NO_RESULTS: - GNUNET_assert (0); - return NULL; - /* intentional fall-through! */ - case SYNC_DB_ONE_RESULT: - GNUNET_assert (0); - return NULL; - } - GNUNET_break (0); - return NULL; -} - - -/** - * Function to process data uploaded by a client. - * - * @param upload_cls our `struct BackupContext *` - * @param request the request is being processed - * @param content_data_size the size of the @a content_data, - * zero when all data have been processed - * @param[in] content_data the uploaded content data, - * may be modified in the callback, - * valid only until return from the callback, - * NULL when all data have been processed - * @return action specifying how to proceed: - * #MHD_upload_action_continue() to continue upload (for incremental - * upload processing only), - * #MHD_upload_action_suspend() to stop reading the upload until - * the request is resumed, - * #MHD_upload_action_abort_request() to close the socket, - * or a response to discard the rest of the upload and transmit - * the response - */ -static const struct MHD_UploadAction * -handle_upload (void *upload_cls, - struct MHD_Request *request, - size_t content_data_size, - void *content_data) -{ - struct BackupContext *bc = upload_cls; - - if (NULL != bc->resp) - { - /* This can happen on resume */ - return MHD_upload_action_from_response (request, - bc->resp); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing %llu bytes of upload data\n", - (unsigned long long) content_data_size); - /* Check hash matches */ - { - struct GNUNET_HashCode our_hash; - - GNUNET_CRYPTO_hash (content_data, - content_data_size, - &our_hash); - if (0 != GNUNET_memcmp (&our_hash, - &bc->new_backup_hash)) - { - GNUNET_break_op (0); - return MHD_upload_action_from_response ( - request, - TALER_MHD2_make_error ( - TALER_EC_SYNC_INVALID_UPLOAD, - NULL)); - } - } - - /* store backup to database */ - { - enum SYNC_DB_QueryStatus qs; - - if (GNUNET_is_zero (&bc->old_backup_hash)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Uploading first backup to account\n"); - qs = SYNCDB_store_backup_TR ( - &bc->account, - &bc->account_sig, - &bc->new_backup_hash, - content_data_size, - content_data); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Uploading existing backup of account\n"); - qs = SYNCDB_update_backup_TR ( - &bc->account, - &bc->old_backup_hash, - &bc->account_sig, - &bc->new_backup_hash, - content_data_size, - content_data); - } - if (qs < 0) - { - bool suspend; - struct MHD_Response *resp; - - resp = handle_database_error (bc, - qs, - &suspend); - if (suspend) - return MHD_upload_action_suspend (request); - return MHD_upload_action_from_response (request, - resp); - } - if (0 == qs) - { - /* database says nothing actually changed, 304 (could - theoretically happen if another equivalent upload succeeded - since we last checked!) */ - struct MHD_Response *resp; - - resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); - TALER_MHD2_add_global_headers (resp, - false); - return MHD_upload_action_from_response (request, - resp); - } - } - - /* generate main (204) standard success reply */ - { - struct MHD_Response *resp; - - resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT); - TALER_MHD2_add_global_headers (resp, - false); - return MHD_upload_action_from_response (request, - resp); - } -} - - -const struct MHD_Action * -SH_backup_post (struct MHD_Request *request, - const struct SYNC_AccountPublicKeyP *account, - uint_fast64_t upload_size) -{ - struct BackupContext *bc; - union MHD_RequestInfoFixedData fi; - - GNUNET_assert (MHD_SC_OK == - MHD_request_get_info_fixed ( - request, - MHD_REQUEST_INFO_FIXED_APP_CONTEXT, - &fi)); - bc = *fi.v_app_context_ppvoid; - if (NULL == bc) - { - /* first call, setup internals */ - bc = GNUNET_new (struct BackupContext); - bc->request = request; - bc->account = *account; -#if BUG - GNUNET_assert (MHD_SC_OK == - MHD_REQUEST_SET_OPTIONS ( - MHD_R_OPTION_TERMINATION_CALLBACK (&cleanup_ctx, - bc))); -#endif - *fi.v_app_context_ppvoid = bc; - } - if (NULL != bc->resp) - return MHD_action_from_response (request, - bc->resp); - if ( (upload_size / 1024 / 1024 >= SH_upload_limit_mb) || - (upload_size > SIZE_MAX) ) - { - GNUNET_break_op (0); - return TALER_MHD2_reply_with_error ( - request, - MHD_HTTP_STATUS_CONTENT_TOO_LARGE, - TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH, - NULL); - } - { - struct MHD_StringNullable fresh; - - if (MHD_YES == - MHD_request_get_value (request, - MHD_VK_URI_QUERY_PARAM, - "fresh", - &fresh)) - bc->force_fresh_order = true; - } - { - struct MHD_StringNullable im; - - if ( (MHD_YES == - MHD_request_get_value (request, - MHD_VK_HEADER, - MHD_HTTP_HEADER_IF_MATCH, - &im)) && - ( (2 >= im.len) || - ('"' != im.cstr[0]) || - ('"' != im.cstr[im.len - 1]) || - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (im.cstr + 1, - im.len - 2, - &bc->old_backup_hash, - sizeof (bc->old_backup_hash))) ) ) - { - GNUNET_break_op (0); - return TALER_MHD2_reply_with_error ( - request, - MHD_HTTP_STATUS_BAD_REQUEST, - TALER_EC_SYNC_BAD_IF_MATCH, - NULL); - } - } - { - struct MHD_StringNullable sig_s; - - if ( (MHD_YES != - MHD_request_get_value (request, - MHD_VK_HEADER, - "Sync-Signature", - &sig_s)) || - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (sig_s.cstr, - sig_s.len, - &bc->account_sig, - sizeof (bc->account_sig))) ) - { - GNUNET_break_op (0); - return TALER_MHD2_reply_with_error ( - request, - MHD_HTTP_STATUS_BAD_REQUEST, - TALER_EC_SYNC_BAD_SYNC_SIGNATURE, - NULL); - } - } - { - struct MHD_StringNullable etag; - - if ( (MHD_YES != - MHD_request_get_value (request, - MHD_VK_HEADER, - MHD_HTTP_HEADER_IF_NONE_MATCH, - &etag)) || - (2 >= etag.len) || - ('"' != etag.cstr[0]) || - ('"' != etag.cstr[etag.len - 1]) || - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (etag.cstr + 1, - etag.len - 2, - &bc->new_backup_hash, - sizeof (bc->new_backup_hash))) ) - { - GNUNET_break_op (0); - return TALER_MHD2_reply_with_error ( - request, - MHD_HTTP_STATUS_BAD_REQUEST, - TALER_EC_SYNC_BAD_IF_NONE_MATCH, - NULL); - } - } - /* validate signature */ - { - struct SYNC_UploadSignaturePS usp = { - .purpose.size = htonl (sizeof (usp)), - .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD), - .old_backup_hash = bc->old_backup_hash, - .new_backup_hash = bc->new_backup_hash - }; - - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD, - &usp, - &bc->account_sig.eddsa_sig, - &account->eddsa_pub)) - { - GNUNET_break_op (0); - return TALER_MHD2_reply_with_error ( - request, - MHD_HTTP_STATUS_FORBIDDEN, - TALER_EC_SYNC_INVALID_SIGNATURE, - NULL); - } - } - - /* Check database to see if the transaction is permissible */ - { - struct GNUNET_HashCode hc; - enum SYNC_DB_QueryStatus qs; - - qs = SYNCDB_lookup_account_TR ( - account, - &hc); - if (qs < 0) - { - bool suspend; - struct MHD_Response *resp; - - resp = handle_database_error (bc, - qs, - &suspend); - if (suspend) - return MHD_action_suspend (request); - return MHD_action_from_response (request, - resp); - } - if (SYNC_DB_NO_RESULTS == qs) - memset (&hc, - 0, - sizeof (hc)); - if (0 == GNUNET_memcmp (&hc, - &bc->new_backup_hash)) - { - /* Refuse upload: we already have that backup! */ - struct MHD_Response *resp; - - resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); - TALER_MHD2_add_global_headers (resp, - false); - return MHD_action_from_response (request, - resp); - } - if (0 != GNUNET_memcmp (&hc, - &bc->old_backup_hash)) - { - /* Refuse upload: if-none-match failed! */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Conflict detected, returning existing backup\n"); - return MHD_action_from_response ( - request, - SH_make_backup (account, - MHD_HTTP_STATUS_CONFLICT)); - } - } - /* check if the client insists on paying */ - { - struct MHD_StringNullable order_req; - - if (MHD_YES == - MHD_request_get_value (request, - MHD_VK_URI_QUERY_PARAM, - "pay", - &order_req)) - { - struct MHD_Response *resp; - bool suspend; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment requested, starting payment process\n"); - resp = begin_payment (bc, - GNUNET_YES, - &suspend); - if (suspend) - return MHD_action_suspend (request); - return MHD_action_from_response (request, - resp); - } - } - /* ready to begin upload! */ - return MHD_action_process_upload_full (request, - upload_size, - &handle_upload, - bc); -} diff --git a/src/sync/sync-httpd2_config.c b/src/sync/sync-httpd2_config.c @@ -30,6 +30,11 @@ * 0: original design * 1: adds ?fresh=y to POST backup operation to force fresh contract * to be created + * 2: block upload signatures are stored server-side and returned with + * the block list, so wallets can verify the backup before applying + * (see PROMPTS.md P1) + * 3: adds GET /backups/$ACCOUNT_KEY, reporting when the account expires + * and how much of the storage allowance it uses */ const struct MHD_Action * @@ -39,7 +44,7 @@ SH_handler_config (struct MHD_Request *request, static struct MHD_Response *response; static struct GNUNET_TIME_Absolute a; - GNUNET_break (0 == upload_size); + GNUNET_break_op (0 == upload_size); if ( (GNUNET_TIME_absolute_is_past (a)) && (NULL != response) ) { @@ -75,7 +80,7 @@ SH_handler_config (struct MHD_Request *request, TALER_JSON_pack_amount ("annual_fee", &SH_annual_fee), GNUNET_JSON_pack_string ("version", - "2:2:0")); + "4:0:1")); GNUNET_break (MHD_SC_OK == MHD_response_add_header (response, MHD_HTTP_HEADER_EXPIRES, diff --git a/src/sync/sync-httpd2_config.h b/src/sync/sync-httpd2_config.h @@ -14,7 +14,7 @@ Sync; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> */ /** - * @file sync/sync-httpd_config.h + * @file sync/sync-httpd2_config.h * @brief headers for /config handler * @author Christian Grothoff */ @@ -36,4 +36,4 @@ SH_handler_config (struct MHD_Request *request, #endif -/* end of sync-httpd_config.h */ +/* end of sync-httpd2_config.h */ diff --git a/src/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.c b/src/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.c @@ -0,0 +1,701 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_delete-backups-KEY-blocks-NONCE.c + * @brief Handle DELETE /backups/$KEY/blocks/$NONCE requests + * @author Iván Ávalos + */ +#include "sync-httpd2.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_mhd2_lib.h> +#include <taler/taler_signatures.h> +#include <sync/sync_signatures.h> +#include <sync/sync_util.h> +#include "sync-httpd2_delete-backups-KEY-blocks-NONCE.h" + + +/** + * Context for a block delete operation. + */ +struct BlockDeleteContext +{ + + /** + * Context for cleanup logic. + */ + struct TM_HandlerContext hc; + + /** + * Public key of the account holder. + */ + struct SYNC_AccountPublicKeyP account_pub; + + /** + * Nonce of the block to delete. + */ + struct SYNC_BlockNonce nonce; + + /** + * Nonce of the preceding block (NULL if first). + */ + struct SYNC_BlockNonce prev_nonce; + + /** + * True if @e prev_nonce was missing in the JSON body. + */ + bool null_prev_nonce; + + /** + * Nonce of the succeeding block (NULL if last). + */ + struct SYNC_BlockNonce next_nonce; + + /** + * True if @e next_nonce was missing in the JSON body. + */ + bool null_next_nonce; + + /** + * Hash of the block to delete (from If-Match). + */ + struct GNUNET_HashCode block_hash; + + /** + * Signature of the delete request. + */ + struct SYNC_AccountSignatureP delete_sig; + + /** + * Relink signature for the block at @e prev_nonce. + */ + struct SYNC_AccountSignatureP prev_upload_sig; + + /** + * True if the relink_prev object was present in the JSON body. + */ + bool have_relink_prev; + + /** + * Hash of the block at @e prev_nonce as fetched by this handler; + * the delete transaction re-checks the stored neighbour still + * hashes to it before storing the relink signature. + */ + struct GNUNET_HashCode prev_hash; + + /** + * Nonce preceding @e prev_nonce as fetched by this handler, as + * covered by @e prev_upload_sig; the delete transaction re-checks it + * for the same reason as @e prev_hash. + */ + struct SYNC_BlockNonce prev_prev_nonce; + + /** + * True if the block at @e prev_nonce has no predecessor, i.e. + * @e prev_prev_nonce is unset. + */ + bool null_prev_prev_nonce; + + /** + * Relink signature for the block at @e next_nonce. + */ + struct SYNC_AccountSignatureP next_upload_sig; + + /** + * True if the relink_next object was present in the JSON body. + */ + bool have_relink_next; + + /** + * Hash of the block at @e next_nonce as fetched by this handler; + * the delete transaction re-checks the stored neighbour still + * hashes to it before storing the relink signature. + */ + struct GNUNET_HashCode next_hash; + + /** + * Nonce succeeding @e next_nonce as fetched by this handler, as + * covered by @e next_upload_sig; the delete transaction re-checks it + * for the same reason as @e next_hash. + */ + struct SYNC_BlockNonce next_next_nonce; + + /** + * True if the block at @e next_nonce has no successor, i.e. + * @e next_next_nonce is unset. + */ + bool null_next_next_nonce; + + /** + * Array of object UIDs referenced by this block. + */ + struct SYNC_ObjectUID *object_uids; + + /** + * Array of object reference count adjustments. + */ + int16_t *object_incs; + + /** + * Length of @e object_uids and @e object_incs. + */ + size_t refs_len; + +}; + + +/** + * Clean up and free a block delete context. + * + * @param hc the @e hc member of the `struct BlockDeleteContext` + * to clean up and free + */ +static void +cleanup_block_delete_free (struct TM_HandlerContext *hc) +{ + struct BlockDeleteContext *bdc; + + bdc = (struct BlockDeleteContext *) hc; + GNUNET_free (bdc->object_uids); + GNUNET_free (bdc->object_incs); + GNUNET_free (bdc); +} + + +/** + * Function to process data uploaded by a client. + * + * @param upload_cls our `struct BlockDeleteContext *` + * @param request the request being processed + * @param content_data_size the size of the @a content_data, + * zero when all data have been processed + * @param[in] content_data the uploaded content data, + * may be modified in the callback, + * valid only until return from the callback, + * NULL when all data have been processed + * @return action specifying how to proceed + */ +static const struct MHD_UploadAction * +handle_delete_upload (void *upload_cls, + struct MHD_Request *request, + size_t content_data_size, + void *content_data) +{ + struct BlockDeleteContext *bdc = upload_cls; + + if ( (0 == content_data_size) && + (NULL == content_data) ) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + + /* parse JSON request */ + { + json_error_t jerr; + json_t *root; + const json_t *relink_prev = NULL; + const json_t *relink_next = NULL; + + root = json_loadb (content_data, + content_data_size, + 0, + &jerr); + if (NULL == root) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + + const json_t *refs = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("delete_sig", + &bdc->delete_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("prev_nonce", + &bdc->prev_nonce), + &bdc->null_prev_nonce), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("next_nonce", + &bdc->next_nonce), + &bdc->null_next_nonce), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("object_refs", + &refs), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("relink_prev", + &relink_prev), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("relink_next", + &relink_next), + NULL), + GNUNET_JSON_spec_end (), + }; + const char *ename; + unsigned int eline; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + { + GNUNET_break_op (0); + json_decref (root); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + + if (GNUNET_OK != + SYNC_object_refs_parse (refs, + &bdc->refs_len, + &bdc->object_uids, + &bdc->object_incs)) + { + GNUNET_break_op (0); + json_decref (root); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + + if (NULL != relink_prev) + { + struct GNUNET_JSON_Specification rspec[] = { + GNUNET_JSON_spec_fixed_auto ("upload_sig", + &bdc->prev_upload_sig), + GNUNET_JSON_spec_end (), + }; + + bdc->have_relink_prev = true; + if (GNUNET_OK != GNUNET_JSON_parse ((json_t *) relink_prev, + rspec, + &ename, + &eline)) + { + GNUNET_break_op (0); + json_decref (root); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + } + + if (NULL != relink_next) + { + struct GNUNET_JSON_Specification rspec[] = { + GNUNET_JSON_spec_fixed_auto ("upload_sig", + &bdc->next_upload_sig), + GNUNET_JSON_spec_end (), + }; + + bdc->have_relink_next = true; + if (GNUNET_OK != GNUNET_JSON_parse ((json_t *) relink_next, + rspec, + &ename, + &eline)) + { + GNUNET_break_op (0); + json_decref (root); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + } + + json_decref (root); + } + + /* The relink signatures are required exactly when the respective + neighbour exists. */ + { + if (bdc->null_prev_nonce != ! bdc->have_relink_prev) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + if (bdc->null_next_nonce != ! bdc->have_relink_next) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + } + + /* Verify signature; it covers the object references too */ + { + struct GNUNET_HashCode refs_hash; + + if (GNUNET_OK != + SYNC_object_refs_hash (bdc->refs_len, + bdc->object_uids, + bdc->object_incs, + &refs_hash)) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + "object_refs")); + } + if (GNUNET_OK != + SYNC_block_delete_verify (&bdc->account_pub, + &bdc->nonce, + ! bdc->null_prev_nonce + ? &bdc->prev_nonce + : NULL, + ! bdc->null_next_nonce + ? &bdc->next_nonce + : NULL, + &bdc->block_hash, + &refs_hash, + &bdc->delete_sig)) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE, + NULL)); + } + } + + /* The deletion relinks both neighbours; verify their uploaded relink + signatures against the neighbours as they are stored now. The + delete transaction re-checks that the neighbours still hash to + what these signatures were verified against. */ + { + struct GNUNET_HashCode n_refs; + struct SYNC_BlockNonce n_prev; + struct SYNC_BlockNonce n_next; + size_t n_data_size; + bool n_is_first; + bool n_is_last; + enum SYNC_DB_QueryStatus qs; + + if (bdc->have_relink_prev) + { + qs = SYNCDB_lookup_block_TR (&bdc->account_pub, + &bdc->prev_nonce, + &bdc->prev_hash, + &n_prev, + &n_next, + &n_data_size, + &n_refs, + &n_is_first, + &n_is_last); + if (SYNC_DB_ONE_RESULT != qs) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT, + TALER_EC_SYNC_CONFLICTING_WRITE, + NULL)); + } + /* Remember the neighbour as it was verified against, so the + transaction can reject the delete if it changed meanwhile */ + bdc->null_prev_prev_nonce = n_is_first; + if (! n_is_first) + bdc->prev_prev_nonce = n_prev; + + if (GNUNET_OK != + SYNC_block_upload_verify (&bdc->account_pub, + &bdc->prev_nonce, + n_is_first + ? NULL + : &n_prev, + ! bdc->null_next_nonce + ? &bdc->next_nonce + : NULL, + &bdc->prev_hash, + &bdc->prev_hash, + &n_refs, + &bdc->prev_upload_sig)) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE, + NULL)); + } + } + + if (bdc->have_relink_next) + { + qs = SYNCDB_lookup_block_TR (&bdc->account_pub, + &bdc->next_nonce, + &bdc->next_hash, + &n_prev, + &n_next, + &n_data_size, + &n_refs, + &n_is_first, + &n_is_last); + if (SYNC_DB_ONE_RESULT != qs) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT, + TALER_EC_SYNC_CONFLICTING_WRITE, + NULL)); + } + bdc->null_next_next_nonce = n_is_last; + if (! n_is_last) + bdc->next_next_nonce = n_next; + + if (GNUNET_OK != + SYNC_block_upload_verify (&bdc->account_pub, + &bdc->next_nonce, + ! bdc->null_prev_nonce + ? &bdc->prev_nonce + : NULL, + n_is_last + ? NULL + : &n_next, + &bdc->next_hash, + &bdc->next_hash, + &n_refs, + &bdc->next_upload_sig)) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE, + NULL)); + } + } + } + + /* Perform the delete */ + { + enum SYNC_DB_QueryStatus qs; + + qs = SYNCDB_delete_block_TR (&bdc->account_pub, + &bdc->nonce, + ! bdc->null_prev_nonce + ? &bdc->prev_nonce + : NULL, + ! bdc->null_next_nonce + ? &bdc->next_nonce + : NULL, + &bdc->block_hash, + bdc->refs_len, + bdc->object_uids, + bdc->object_incs, + bdc->have_relink_prev + ? &bdc->prev_upload_sig + : NULL, + bdc->have_relink_prev + ? &bdc->prev_hash + : NULL, + (bdc->have_relink_prev && + ! bdc->null_prev_prev_nonce) + ? &bdc->prev_prev_nonce + : NULL, + bdc->have_relink_next + ? &bdc->next_upload_sig + : NULL, + bdc->have_relink_next + ? &bdc->next_hash + : NULL, + (bdc->have_relink_next && + ! bdc->null_next_next_nonce) + ? &bdc->next_next_nonce + : NULL); + switch (qs) + { + case SYNC_DB_ONE_RESULT: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Block deleted successfully\n"); + { + struct MHD_Response *resp; + + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT); + TALER_MHD2_add_global_headers (resp, + false); + return MHD_upload_action_from_response (request, + resp); + } + case SYNC_DB_PAYMENT_REQUIRED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Delete failed: payment required\n"); + return MHD_upload_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_PAYMENT_REQUIRED, + TALER_EC_SYNC_ACCOUNT_UNKNOWN, + NULL)); + case SYNC_DB_TARGET_MISSING: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Delete failed: block gone\n"); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN, + NULL)); + case SYNC_DB_OUTDATED_WRITE: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Delete failed: concurrent modification\n"); + return MHD_upload_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT, + TALER_EC_SYNC_CONFLICTING_WRITE, + NULL)); + case SYNC_DB_INCONSISTENT_BACKUP: + /* Unknown object or refcount underflow; nothing was modified */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Delete failed: object references do not match" + " the stored backup\n"); + return MHD_upload_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT, + TALER_EC_SYNC_INCONSISTENT_BACKUP, + NULL)); + case SYNC_DB_HARD_ERROR: + GNUNET_break (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL)); + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL)); + default: + GNUNET_break (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_GENERIC_BACKEND_ERROR, + NULL)); + } + } +} + + +const struct MHD_Action * +SH_backup_block_delete (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update) +{ + struct BlockDeleteContext *bdc; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode block_hash; + union MHD_RequestInfoFixedData fi; + + (void) is_update; + + /* Parse nonce from URL first to avoid allocating on error */ + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &nonce, + sizeof (nonce))) + { + GNUNET_break_op (0); + return MHD_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "nonce")); + } + + /* Parse If-Match header for block hash */ + { + struct MHD_StringNullable etag; + + if ( (MHD_YES != + MHD_request_get_value (request, + MHD_VK_HEADER, + MHD_HTTP_HEADER_IF_MATCH, + &etag)) || + (2 >= etag.len) || + ('"' != etag.cstr[0]) || + ('"' != etag.cstr[etag.len - 1]) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (etag.cstr + 1, + etag.len - 2, + &block_hash, + sizeof (block_hash))) ) + { + return MHD_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_BAD_IF_MATCH, + NULL)); + } + } + + GNUNET_assert (MHD_SC_OK == + MHD_request_get_info_fixed ( + request, + MHD_REQUEST_INFO_FIXED_APP_CONTEXT, + &fi)); + bdc = *fi.v_app_context_ppvoid; + if (NULL == bdc) + { + /* first call, setup internals */ + bdc = GNUNET_new (struct BlockDeleteContext); + bdc->hc.cc = &cleanup_block_delete_free; + bdc->account_pub = *account_pub; + bdc->nonce = nonce; + bdc->block_hash = block_hash; + *fi.v_app_context_ppvoid = bdc; + } + + /* The body only carries the delete signature, but it still has to be + bounded: a chunked request reports no length up-front. */ + { + size_t buff_size; + + if (SH_upload_too_large (upload_size, + &buff_size)) + { + cleanup_block_delete_free (&bdc->hc); + *fi.v_app_context_ppvoid = NULL; + return MHD_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONTENT_TOO_LARGE, + TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH, + NULL)); + } + + /* Ready to begin reading the body for the delete signature! */ + return MHD_action_process_upload_full (request, + buff_size, + &handle_delete_upload, + bdc); + } +} diff --git a/src/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.h b/src/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_delete-backups-KEY-blocks-NONCE.h + * @brief Handle DELETE /backups/$KEY/blocks/$NONCE requests + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_DELETE_BACKUPS_KEY_BLOCKS_NONCE_H +#define SYNC_HTTPD2_DELETE_BACKUPS_KEY_BLOCKS_NONCE_H + +#include <microhttpd2.h> +#include "sync/sync_service.h" + + +/** + * Handle a DELETE "/backups/$KEY/blocks/$NONCE" request. + * + * @param request the MHD request to handle + * @param account_pub public key of the account + * @param args path segments: [0]="blocks", [1]=encoded NONCE + * @param upload_size number of bytes being uploaded (unused for DELETE) + * @param is_update whether the operation is an update (unused for DELETE) + * @return MHD action + */ +const struct MHD_Action * +SH_backup_block_delete (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update); + + +#endif diff --git a/src/sync/sync-httpd2_get-backups-KEY-blocks.c b/src/sync/sync-httpd2_get-backups-KEY-blocks.c @@ -0,0 +1,334 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_get-backups-KEY-blocks.c + * @brief GET /backups/$KEY/blocks — list blocks + * @author Iván Ávalos + */ +#include "sync-httpd2.h" +#include "sync-httpd2_get-backups-KEY-blocks.h" +#include <inttypes.h> +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <sync/sync_util.h> + + +/** + * Context for collecting blocks from the DB iterator. + */ +struct BlocksGetContext +{ + /** + * Context for cleanup logic. + */ + struct TM_HandlerContext hc; + + /** + * Public key of the account holder. + */ + struct SYNC_AccountPublicKeyP account_pub; + + /** + * Maximum number of blocks to return (from ?limit=). + */ + int16_t limit; + + /** + * Nonce of the block to start listing at (from ?start_nonce=). + */ + struct SYNC_BlockNonce start_nonce; + + /** + * True if @e start_nonce was given. + */ + bool have_start_nonce; + + /** + * JSON array of the blocks collected so far. + */ + json_t *array; +}; + + +/** + * Clean up and free a blocks-get context. + * + * @param hc the @e hc member of the context to clean up and free + */ +static void +cleanup_blocks_get_free (struct TM_HandlerContext *hc) +{ + struct BlocksGetContext *bgc; + + bgc = (struct BlocksGetContext *) hc; + if (NULL != bgc->array) + json_decref (bgc->array); + GNUNET_free (bgc); +} + + +/** + * Clean up and clear a blocks-get context, then terminate + * the request with an error response. + * + * @param request MHD request (used for response) + * @param bgc context to clean up and free + * @param fi request info with app context pointer to clear + * @param sc HTTP status code + * @param ec Taler error code + * @param hint optional hint string for the error detail + */ +#define BAIL_WITH_ERROR(request, bgc, fi, sc, ec, hint) do { \ + GNUNET_break_op (0); \ + cleanup_blocks_get_free (&(bgc)->hc); \ + *(fi).v_app_context_ppvoid = NULL; \ + return TALER_MHD2_reply_with_error ( \ + (request), \ + (sc), \ + (ec), \ + (hint)); \ +} while (0) + + +/** + * Clean up and clear a blocks-get context, then terminate + * the request with a response built elsewhere. + * + * @param request MHD request (used for response) + * @param bgc context to clean up and free + * @param fi request info with app context pointer to clear + * @param resp response to return (must be non-NULL) + */ +#define BAIL_WITH_RESPONSE(request, bgc, fi, resp) do { \ + cleanup_blocks_get_free (&(bgc)->hc); \ + *(fi).v_app_context_ppvoid = NULL; \ + return MHD_action_from_response ((request), \ + (resp)); \ +} while (0) + + +/** + * Iterator callback for SYNCDB_lookup_block_range_TR. + * Appends each block as a JSON object to the context array. + * + * @param cls a `struct BlocksGetContext *` + * @param nonce block nonce + * @param block_hash hash of the block data + * @param prev_nonce previous block nonce (NULL if first) + * @param next_nonce next block nonce (NULL if last) + * @param data_size size of @a data + * @param data encrypted block contents + * @param upload_sig signature stored with the block + * @param old_hash hash of the replaced block data + * @param refs_hash hash over the object references + */ +static void +block_iter (void *cls, + const struct SYNC_BlockNonce *nonce, + const struct GNUNET_HashCode *block_hash, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + size_t data_size, + const void *data, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *refs_hash) +{ + struct BlocksGetContext *bgc = cls; + json_t *obj; + + obj = SYNC_block_entry_serialize (nonce, + block_hash, + prev_nonce, + next_nonce, + data_size, + data, + upload_sig, + old_hash, + refs_hash); + json_array_append_new (bgc->array, + obj); +} + + +const struct MHD_Action * +SH_backup_blocks_get (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update) +{ + union MHD_RequestInfoFixedData fi; + struct BlocksGetContext *bgc; + enum SYNC_DB_QueryStatus qs; + + (void) args; + (void) upload_size; + (void) is_update; + + GNUNET_assert (MHD_SC_OK == + MHD_request_get_info_fixed ( + request, + MHD_REQUEST_INFO_FIXED_APP_CONTEXT, + &fi)); + bgc = *fi.v_app_context_ppvoid; + + if (NULL == bgc) + { + struct MHD_StringNullable limit_str; + + bgc = GNUNET_new (struct BlocksGetContext); + bgc->hc.cc = &cleanup_blocks_get_free; + bgc->account_pub = *account_pub; + *fi.v_app_context_ppvoid = bgc; + + /* MHD returns MHD_YES with a NULL cstr for "?limit" without a + value, so check the pointer separately */ + if ( (MHD_YES != + MHD_request_get_value (request, + MHD_VK_URI_QUERY_PARAM, + "limit", + &limit_str)) || + (NULL == limit_str.cstr) || + (1 != sscanf (limit_str.cstr, + "%" SCNd16, + &bgc->limit)) || + (0 >= bgc->limit) ) + BAIL_WITH_ERROR (request, + bgc, + fi, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "limit"); + + { + struct MHD_StringNullable sn_str; + + if (MHD_YES == + MHD_request_get_value (request, + MHD_VK_URI_QUERY_PARAM, + "start_nonce", + &sn_str)) + { + if ( (NULL == sn_str.cstr) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (sn_str.cstr, + sn_str.len, + &bgc->start_nonce, + sizeof (bgc->start_nonce))) ) + BAIL_WITH_ERROR (request, + bgc, + fi, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "start_nonce"); + bgc->have_start_nonce = true; + } + } + } + + bgc->array = json_array (); + + qs = SYNCDB_lookup_block_range_TR (&bgc->account_pub, + bgc->have_start_nonce + ? &bgc->start_nonce + : NULL, + bgc->limit, + &block_iter, + bgc); + + switch (qs) + { + case SYNC_DB_PAYMENT_REQUIRED: + { + struct MHD_Response *resp; + + json_decref (bgc->array); + bgc->array = NULL; + resp = MHD_response_from_empty ( + MHD_HTTP_STATUS_PAYMENT_REQUIRED); + TALER_MHD2_add_global_headers (resp, + false); + BAIL_WITH_RESPONSE (request, + bgc, + fi, + resp); + } + + case SYNC_DB_TARGET_MISSING: + json_decref (bgc->array); + bgc->array = NULL; + BAIL_WITH_ERROR (request, + bgc, + fi, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "start_nonce block not found in chain"); + + case SYNC_DB_NO_RESULTS: + break; + + case SYNC_DB_HARD_ERROR: + GNUNET_break (0); + json_decref (bgc->array); + bgc->array = NULL; + BAIL_WITH_ERROR (request, + bgc, + fi, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + json_decref (bgc->array); + bgc->array = NULL; + BAIL_WITH_ERROR (request, + bgc, + fi, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + + default: + if (qs < 0) + { + /* Do not serve a partial list as if it were complete */ + GNUNET_break (0); + json_decref (bgc->array); + bgc->array = NULL; + BAIL_WITH_ERROR (request, + bgc, + fi, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_SYNC_GENERIC_BACKEND_ERROR, + NULL); + } + break; + } + + { + json_t *resp_json; + + resp_json = bgc->array; + bgc->array = NULL; + cleanup_blocks_get_free (&bgc->hc); + *fi.v_app_context_ppvoid = NULL; + return TALER_MHD2_reply_json_steal (request, + resp_json, + MHD_HTTP_STATUS_OK); + } +} diff --git a/src/sync/sync-httpd2_get-backups-KEY-blocks.h b/src/sync/sync-httpd2_get-backups-KEY-blocks.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_get-backups-KEY-blocks.h + * @brief GET /backups/$KEY/blocks — list all blocks + * @author Iván Ávalos + */ +#ifndef SYNC_HTTPD2_GET_BACKUPS_KEY_BLOCKS_H +#define SYNC_HTTPD2_GET_BACKUPS_KEY_BLOCKS_H + +#include <microhttpd2.h> +#include "sync/sync_service.h" + + +/** + * Handle GET /backups/$KEY/blocks. + * + * @param request the MHD request to handle + * @param account_pub public key of the account + * @param args path segments (args[0]="blocks") + * @param upload_size number of bytes being uploaded + * @param is_update whether the operation is an update + * @return MHD action + */ +const struct MHD_Action * +SH_backup_blocks_get (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update); + + +#endif diff --git a/src/sync/sync-httpd2_get-backups-KEY-objects-UID.c b/src/sync/sync-httpd2_get-backups-KEY-objects-UID.c @@ -0,0 +1,120 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_get-backups-KEY-objects-UID.c + * @brief Handle GET /backups/$KEY/objects/$UID requests + * @author Iván Ávalos + */ +#include "platform.h" +#include "sync-httpd2.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_mhd2_lib.h> +#include "sync-httpd2_get-backups-KEY-objects-UID.h" +#include <sync/sync_util.h> + + +const struct MHD_Action * +SH_backup_object_get (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update) +{ + struct SYNC_ObjectUID uid; + struct GNUNET_HashCode object_hash; + int64_t refcount; + size_t data_size; + void *data = NULL; + enum SYNC_DB_QueryStatus qs; + + (void) upload_size; + (void) is_update; + + /* Parse UID from URL */ + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &uid, + sizeof (uid))) + { + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "uid"); + } + + /* Look up object by UID */ + qs = SYNCDB_lookup_object_uid (account_pub, + &uid, + &object_hash, + &refcount, + &data_size, + &data); + switch (qs) + { + case SYNC_DB_ONE_RESULT: + { + json_t *resp_json; + + resp_json = SYNC_object_entry_serialize (&uid, + data_size, + data); + GNUNET_free (data); + return TALER_MHD2_reply_json_steal (request, + resp_json, + MHD_HTTP_STATUS_OK); + } + case SYNC_DB_NO_RESULTS: + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN, + "object not found"); + case SYNC_DB_PAYMENT_REQUIRED: + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_PAYMENT_REQUIRED, + TALER_EC_SYNC_ACCOUNT_UNKNOWN, + NULL); + case SYNC_DB_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + default: + GNUNET_break (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + } +} + + +/* end of sync-httpd2_get-backups-KEY-objects-UID.c */ diff --git a/src/sync/sync-httpd2_get-backups-KEY-objects-UID.h b/src/sync/sync-httpd2_get-backups-KEY-objects-UID.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_get-backups-KEY-objects-UID.h + * @brief Handle GET /backups/$KEY/objects/$UID requests + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_GET_BACKUPS_KEY_OBJECTS_UID_H +#define SYNC_HTTPD2_GET_BACKUPS_KEY_OBJECTS_UID_H + +#include <microhttpd2.h> +#include "sync/sync_service.h" + + +/** + * Handle a GET "/backups/$KEY/objects/$UID" request. + * + * @param request the MHD request to handle + * @param account_pub public key of the account + * @param args path segments: [0]="objects", [1]=encoded UID + * @param upload_size number of bytes being uploaded (unused for GET) + * @param is_update whether the operation is an update (unused for GET) + * @return MHD action + */ +const struct MHD_Action * +SH_backup_object_get (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update); + + +#endif diff --git a/src/sync/sync-httpd2_get-backups-KEY.c b/src/sync/sync-httpd2_get-backups-KEY.c @@ -0,0 +1,88 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_get-backups-KEY.c + * @brief GET /backups/$KEY — account status + * @author Christian Grothoff + */ +#include "sync-httpd2.h" +#include "sync-httpd2_get-backups-KEY.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <sync/sync_util.h> + + +const struct MHD_Action * +SH_backup_account_get (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub) +{ + struct GNUNET_TIME_Timestamp expiration_date; + uint64_t storage_used_bytes; + uint64_t block_count; + enum SYNC_DB_QueryStatus qs; + + qs = SYNCDB_lookup_account_TR (account_pub, + &expiration_date, + &storage_used_bytes, + &block_count); + switch (qs) + { + case SYNC_DB_ONE_RESULT: + break; + case SYNC_DB_TARGET_MISSING: + /* Deliberately not 402: the client is asking *whether* it has to + pay, so an unknown account is simply not found. */ + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_NOT_FOUND, + TALER_EC_SYNC_ACCOUNT_UNKNOWN, + NULL); + case SYNC_DB_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + default: + GNUNET_break (0); + return TALER_MHD2_reply_with_error ( + request, + MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR, + TALER_EC_SYNC_GENERIC_BACKEND_ERROR, + NULL); + } + + /* An account past its expiration is reported as it stands, not + refused: the date is exactly what the client came for. */ + return TALER_MHD2_reply_json_steal ( + request, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("expiration_date", + expiration_date), + GNUNET_JSON_pack_uint64 ("storage_used_bytes", + storage_used_bytes), + GNUNET_JSON_pack_uint64 ("block_count", + block_count)), + MHD_HTTP_STATUS_OK); +} diff --git a/src/sync/sync-httpd2_get-backups-KEY.h b/src/sync/sync-httpd2_get-backups-KEY.h @@ -0,0 +1,38 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_get-backups-KEY.h + * @brief GET /backups/$KEY — account status + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_GET_BACKUPS_KEY_H +#define SYNC_HTTPD2_GET_BACKUPS_KEY_H + +#include "sync-httpd2.h" + +/** + * Handle a GET /backups/$KEY request: report when the account expires + * and how much of the storage allowance it uses. + * + * @param request the MHD request + * @param account_pub public key of the account + * @return MHD action to take + */ +const struct MHD_Action * +SH_backup_account_get (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub); + +#endif diff --git a/src/sync/sync-httpd2_payments.c b/src/sync/sync-httpd2_payments.c @@ -0,0 +1,491 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync/sync-httpd2_payments.c + * @brief Helpers for merchant payment processing + * @author Christian Grothoff + */ +#include "sync-httpd2.h" +#include "sync-httpd2_payments.h" + +#include <gnunet/gnunet_util_lib.h> +#include <taler/merchant/post-private-orders.h> +#include <taler/merchant/get-private-orders-ORDER_ID.h> +#include <taler/taler_json_lib.h> + + +/** + * Kept in DLL for shutdown handling while suspended. + */ +static struct BackupPaymentContext *bpc_head; + +/** + * Kept in DLL for shutdown handling while suspended. + */ +static struct BackupPaymentContext *bpc_tail; + + +/** + * Transmit a payment request for @a order_id on @a request + * + * @param order_id our backend's order ID + * @param token the claim token generated by the merchant (NULL if + * it wasn't generated). + * @return MHD response to use + */ +static struct MHD_Response * +make_payment_request (const char *order_id, + const struct TALER_ClaimTokenP *token) +{ + struct MHD_Response *resp; + + /* request payment via Taler */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating payment request for order `%s'\n", + order_id); + resp = MHD_response_from_empty (MHD_HTTP_STATUS_PAYMENT_REQUIRED); + TALER_MHD2_add_global_headers (resp, + false); + { + char *hdr; + const char *pfx; + char *hn; + struct GNUNET_Buffer hdr_buf = { 0 }; + + if (0 == strncasecmp ("https://", + SH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &SH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + SH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &SH_backend_url[strlen ("http://")]; + } + else + { + GNUNET_break (0); + MHD_response_destroy (resp); + return NULL; + } + if (0 == strlen (hn)) + { + GNUNET_break (0); + MHD_response_destroy (resp); + return NULL; + } + + GNUNET_buffer_write_str (&hdr_buf, pfx); + GNUNET_buffer_write_str (&hdr_buf, "pay/"); + GNUNET_buffer_write_str (&hdr_buf, hn); + GNUNET_buffer_write_path (&hdr_buf, order_id); + /* No session ID */ + GNUNET_buffer_write_path (&hdr_buf, ""); + if (NULL != token) + { + GNUNET_buffer_write_str (&hdr_buf, "?c="); + GNUNET_buffer_write_data_encoded (&hdr_buf, + token, + sizeof (*token)); + } + hdr = GNUNET_buffer_reap_str (&hdr_buf); + GNUNET_break (MHD_SC_OK == + MHD_response_add_header (resp, + "Taler", + hdr)); + GNUNET_free (hdr); + } + return resp; +} + + +/** + * Resume the suspended request of @a bpc, so that the response we + * stored in @e resp gets transmitted. + * + * @param bpc context of the request to resume + */ +static void +resume_request (struct BackupPaymentContext *bpc) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming request with order `%s'\n", + bpc->order_id); + MHD_request_resume (bpc->request); + SH_trigger_daemon (); +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /contract request to a merchant. + * + * @param bpc our payment context + * @param por response details + */ +static void +proposal_cb ( + struct BackupPaymentContext *bpc, + const struct TALER_MERCHANT_PostPrivateOrdersResponse *por) +{ + enum SYNC_DB_QueryStatus qs; + + bpc->po = NULL; + GNUNET_CONTAINER_DLL_remove (bpc_head, + bpc_tail, + bpc); + if (MHD_HTTP_STATUS_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%u\n", + por->hr.http_status, + (unsigned int) por->hr.ec); + GNUNET_break_op (0); + bpc->resp = TALER_MHD2_MAKE_JSON_PACK ( + MHD_HTTP_STATUS_BAD_GATEWAY, + TALER_JSON_pack_ec (TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR), + GNUNET_JSON_pack_uint64 ("backend-ec", + (json_int_t) por->hr.ec), + GNUNET_JSON_pack_uint64 ("backend-http-status", + (json_int_t) por->hr.http_status), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("backend-reply", + (json_t *) por->hr.reply))); + resume_request (bpc); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storing payment request for order `%s'\n", + por->details.ok.order_id); + qs = SYNCDB_store_payment_TR ( + &bpc->account_pub, + por->details.ok.order_id, + por->details.ok.token, + &SH_annual_fee, + GNUNET_TIME_UNIT_YEARS); + if (0 >= qs) + { + GNUNET_break (0); + bpc->resp = TALER_MHD2_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "Failed to persist payment request in sync database"); + GNUNET_assert (NULL != bpc->resp); + resume_request (bpc); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Obtained fresh order `%s'\n", + por->details.ok.order_id); + bpc->resp = make_payment_request (por->details.ok.order_id, + por->details.ok.token); + GNUNET_assert (NULL != bpc->resp); + resume_request (bpc); +} + + +/** + * Function called on all pending payments for the right + * account. + * + * @param cls closure, our `struct BackupPaymentContext *` + * @param timestamp for how long have we been waiting + * @param order_id order id in the backend + * @param token claim token to use (or NULL for none) + * @param amount how much is the order for + */ +static void +ongoing_payment_cb (void *cls, + struct GNUNET_TIME_Timestamp timestamp, + const char *order_id, + const struct TALER_ClaimTokenP *token, + const struct TALER_Amount *amount) +{ + struct BackupPaymentContext *bpc = cls; + + if (0 != TALER_amount_cmp (amount, + &SH_annual_fee)) + return; /* can't reuse, fees changed */ + if ( (NULL == bpc->existing_order_id) || + (GNUNET_TIME_timestamp_cmp (bpc->existing_order_timestamp, + <, + timestamp)) ) + { + GNUNET_free (bpc->existing_order_id); + bpc->existing_order_id = GNUNET_strdup (order_id); + bpc->existing_order_timestamp = timestamp; + if (NULL != token) + bpc->token = *token; + } +} + + +/** + * Store the response to send for the order status @a osr in @a bpc. + * Leaving @e resp at NULL means the payment went through and the + * request should re-attempt its database operation. + * + * @param bpc our payment context + * @param osr order status + */ +static void +set_payment_response ( + struct BackupPaymentContext *bpc, + const struct TALER_MERCHANT_GetPrivateOrderResponse *osr) +{ + const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; + + switch (hr->http_status) + { + case 0: + /* Likely timeout, complain! */ + bpc->resp = TALER_MHD2_make_error ( + TALER_EC_SYNC_GENERIC_BACKEND_TIMEOUT, + NULL); + return; + case MHD_HTTP_STATUS_OK: + break; /* handled below */ + default: + /* Unexpected backend response */ + bpc->resp = TALER_MHD2_MAKE_JSON_PACK ( + MHD_HTTP_STATUS_BAD_GATEWAY, + GNUNET_JSON_pack_uint64 ("code", + TALER_EC_SYNC_GENERIC_BACKEND_ERROR), + GNUNET_JSON_pack_string ("hint", + TALER_ErrorCode_get_hint ( + TALER_EC_SYNC_GENERIC_BACKEND_ERROR)), + GNUNET_JSON_pack_uint64 ("backend-ec", + (json_int_t) hr->ec), + GNUNET_JSON_pack_uint64 ("backend-http-status", + (json_int_t) hr->http_status), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("backend-reply", + (json_t *) hr->reply))); + return; + } + + GNUNET_assert (MHD_HTTP_STATUS_OK == hr->http_status); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment status checked: %d\n", + osr->details.ok.status); + switch (osr->details.ok.status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum SYNC_DB_QueryStatus qs; + + qs = SYNCDB_increment_lifetime_TR ( + &bpc->account_pub, + bpc->order_id, + GNUNET_TIME_UNIT_YEARS); /* always annual */ + if (0 <= qs) + return; /* continue as planned */ + GNUNET_break (0); + bpc->resp = TALER_MHD2_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "increment lifetime"); + GNUNET_assert (NULL != bpc->resp); + return; /* continue as planned */ + } + case TALER_MERCHANT_OSC_UNPAID: + case TALER_MERCHANT_OSC_CLAIMED: + break; + } + if (NULL != bpc->existing_order_id) + { + /* repeat payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Repeating payment request\n"); + bpc->resp = make_payment_request ( + bpc->existing_order_id, + (GNUNET_YES == GNUNET_is_zero (&bpc->token)) + ? NULL + : &bpc->token); + GNUNET_assert (NULL != bpc->resp); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Timeout waiting for payment\n"); + bpc->resp = TALER_MHD2_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT, + "Timeout awaiting promised payment"); + GNUNET_assert (NULL != bpc->resp); +} + + +/** + * Callback to process a GET /check-payment request + * + * @param bpc our payment context + * @param osr order status + */ +static void +check_payment_cb ( + struct BackupPaymentContext *bpc, + const struct TALER_MERCHANT_GetPrivateOrderResponse *osr) +{ + bpc->omgh = NULL; + GNUNET_CONTAINER_DLL_remove (bpc_head, + bpc_tail, + bpc); + /* Set the response before resuming, MHD needs it in place */ + set_payment_response (bpc, + osr); + resume_request (bpc); +} + + +/** + * Helper function used to ask our backend to await + * a payment for the user's account. + * + * @param bpc context to await payment for + * @param timeout when to give up trying + * @param order_id which order to check for the payment + */ +static void +await_payment (struct BackupPaymentContext *bpc, + struct GNUNET_TIME_Relative timeout, + const char *order_id) +{ + GNUNET_CONTAINER_DLL_insert (bpc_head, + bpc_tail, + bpc); + /* @a order_id may be MHD-owned and die when we return an action */ + GNUNET_free (bpc->order_id); + bpc->order_id = GNUNET_strdup (order_id); + order_id = bpc->order_id; + bpc->omgh = TALER_MERCHANT_get_private_order_create ( + SH_ctx, + SH_backend_url, + order_id); + GNUNET_assert (GNUNET_OK == + TALER_MERCHANT_get_private_order_set_options ( + bpc->omgh, + TALER_MERCHANT_get_private_order_option_timeout (timeout))); + GNUNET_assert (TALER_EC_NONE == + TALER_MERCHANT_get_private_order_start ( + bpc->omgh, + &check_payment_cb, + bpc)); + SH_trigger_curl (); +} + + +struct MHD_Response * +SH_begin_payment (struct BackupPaymentContext *bpc, + int pay_req, + bool *suspend) +{ + (void) pay_req; + *suspend = false; + if (! bpc->force_fresh_order) + { + enum SYNC_DB_QueryStatus qs; + + qs = SYNCDB_lookup_pending_payments_by_account_TR ( + &bpc->account_pub, + &ongoing_payment_cb, + bpc); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, + "pending payments"); + } + + if (NULL != bpc->existing_order_id) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Have existing order, waiting for `%s' to complete\n", + bpc->existing_order_id); + await_payment (bpc, + GNUNET_TIME_UNIT_ZERO /* no long polling */, + bpc->existing_order_id); + *suspend = true; + return NULL; + } + } + GNUNET_CONTAINER_DLL_insert (bpc_head, + bpc_tail, + bpc); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending request while creating order at `%s'\n", + SH_backend_url); + *suspend = true; + { + json_t *order; + + order = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &SH_annual_fee), + GNUNET_JSON_pack_string ("summary", + "annual fee for sync service"), + GNUNET_JSON_pack_string ("fulfillment_url", + SH_fulfillment_url)); + bpc->po = TALER_MERCHANT_post_private_orders_create (SH_ctx, + SH_backend_url, + order); + json_decref (order); + } + GNUNET_assert (TALER_EC_NONE == + TALER_MERCHANT_post_private_orders_start ( + bpc->po, + &proposal_cb, + bpc)); + SH_trigger_curl (); + return NULL; +} + + +struct MHD_Response * +SH_await_payment (struct BackupPaymentContext *bpc, + const char *order_id, + bool *suspend) +{ + *suspend = true; + await_payment (bpc, + GNUNET_TIME_UNIT_ZERO, + order_id); + return NULL; +} + + +void +SH_payment_cleanup (struct BackupPaymentContext *bpc) +{ + if ( (NULL != bpc->po) || + (NULL != bpc->omgh) ) + { + /* Request went away while we were talking to the merchant; drop + the operation before its closure is freed */ + if (NULL != bpc->po) + { + TALER_MERCHANT_post_private_orders_cancel (bpc->po); + bpc->po = NULL; + } + if (NULL != bpc->omgh) + { + TALER_MERCHANT_get_private_order_cancel (bpc->omgh); + bpc->omgh = NULL; + } + GNUNET_CONTAINER_DLL_remove (bpc_head, + bpc_tail, + bpc); + } + GNUNET_free (bpc->existing_order_id); + GNUNET_free (bpc->order_id); +} diff --git a/src/sync/sync-httpd2_payments.h b/src/sync/sync-httpd2_payments.h @@ -0,0 +1,151 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_payments.h + * @brief Helpers for merchant payment processing + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_PAYMENTS_H +#define SYNC_HTTPD2_PAYMENTS_H + +#include "sync/sync_service.h" +#include <taler/taler_util.h> + + +struct BackupPaymentContext; +#define TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE \ + struct BackupPaymentContext +#define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE \ + struct BackupPaymentContext + + +/** + * Context for a merchant payment. + */ +struct BackupPaymentContext +{ + /** + * Public key of the account holder. + */ + struct SYNC_AccountPublicKeyP account_pub; + + /** + * Claim token, all zeros if not known. + * + * (only set if @e existing_order_id is non-NULL). + */ + struct TALER_ClaimTokenP token; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct BackupPaymentContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct BackupPaymentContext *prev; + + /** + * Used while suspended for resumption. + */ + struct MHD_Request *request; + + /** + * To be returned upon resuming. + */ + struct MHD_Response *resp; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostPrivateOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_GetPrivateOrderHandle *omgh; + + /** + * Order under which the client promised payment, or NULL. + * Owned by this struct. + */ + char *order_id; + + /** + * Order ID for the client that we found in our database. + * Owned by this struct. + */ + char *existing_order_id; + + /** + * Timestamp of the order in @e existing_order_id. Used to + * select the most recent unpaid offer. + */ + struct GNUNET_TIME_Timestamp existing_order_timestamp; + + /** + * Do not look for an existing order, force a fresh order to be created. + */ + bool force_fresh_order; +}; + + +/** + * Helper function used to ask our backend to begin + * processing a payment for the user's account. + * May perform asynchronous operations by suspending the request + * if required. + * + * @param bpc context to begin payment for. + * @param pay_req #GNUNET_YES if payment was explicitly requested, + * #GNUNET_NO if payment is needed + * @param[out] suspend set to true if the request should be suspended + * @return MHD response + */ +struct MHD_Response * +SH_begin_payment (struct BackupPaymentContext *bpc, + int pay_req, + bool *suspend); + + +/** + * Helper function used to await completion of an + * already-initiated payment. + * May perform asynchronous operations by suspending the request + * if required. + * + * @param bpc context for the payment to await + * @param order_id order that the client promised to pay + * @param[out] suspend set to true if the request should be suspended + * @return MHD response + */ +struct MHD_Response * +SH_await_payment (struct BackupPaymentContext *bpc, + const char *order_id, + bool *suspend); + + +/** + * Release resources held by @a bpc, cancelling a merchant operation + * that is still in flight. Does not free @a bpc itself. + * + * @param[in,out] bpc payment context to clean up + */ +void +SH_payment_cleanup (struct BackupPaymentContext *bpc); + +#endif diff --git a/src/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.c b/src/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.c @@ -0,0 +1,985 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_post-backups-KEY-blocks-NONCE.c + * @brief Handle POST /backups/$KEY/blocks/$NONCE requests + * @author Iván Ávalos + */ +#include "sync-httpd2.h" +#include "sync-httpd2_payments.h" +#include <gnunet/gnunet_time_lib.h> +#include <gnunet/gnunet_util_lib.h> +#include <taler/taler_signatures.h> +#include <sync/sync_signatures.h> +#include <sync/sync_util.h> + + +/** + * How long do we hold an HTTP client connection if + * we are awaiting payment before giving up? + */ +#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 30) + + +/** + * Context for a block append operation. + */ +struct BlockUploadContext +{ + /** + * Context for cleanup logic. + */ + struct TM_HandlerContext hc; + + /** + * Buffer size to use for reading the request body. + */ + size_t buff_size; + + /** + * Whether an existing block is being updated. + */ + bool is_update; + + /** + * Public key of the account holder. + */ + struct SYNC_AccountPublicKeyP account_pub; + + /** + * Signature of the upload request. + */ + struct SYNC_AccountSignatureP upload_sig; + + /** + * Nonce of the block to append/delete. + */ + struct SYNC_BlockNonce nonce; + + /** + * Nonce of the preceding block in the DLL. + * (set to NULL when uploading the first block) + */ + struct SYNC_BlockNonce prev_nonce; + + /** + * True if @e prev_nonce was missing in the JSON body. + */ + bool null_prev_nonce; + + /** + * Encrypted block contents. + */ + void *block_data; + + /** + * Length of @e block_data. + */ + size_t block_data_len; + + /** + * Nonce of the succeeding block in the DLL. + * (set to NULL when appending at the end) + */ + struct SYNC_BlockNonce next_nonce; + + /** + * True if @e next_nonce was missing in the JSON body. + */ + bool null_next_nonce; + + /** + * Previous hash of the block to update. + * + * (set to NULL if not updating a block) + */ + struct GNUNET_HashCode old_data_hash; + + /** + * Hash of the block to upload. + */ + struct GNUNET_HashCode new_data_hash; + + /** + * Relink signature for the block at @e prev_nonce (append only). + */ + struct SYNC_AccountSignatureP prev_upload_sig; + + /** + * True if the relink_prev object was present in the JSON body. + */ + bool have_relink_prev; + + /** + * Hash of the block at @e prev_nonce as fetched by this handler + * (append only); the append transaction re-checks the stored tail + * still hashes to it before storing the relink signature. + */ + struct GNUNET_HashCode prev_hash; + + /** + * Nonce preceding @e prev_nonce as fetched by this handler (append + * only), as covered by @e prev_upload_sig; the append transaction + * re-checks it for the same reason as @e prev_hash. + */ + struct SYNC_BlockNonce prev_prev_nonce; + + /** + * True if the block at @e prev_nonce has no predecessor, i.e. + * @e prev_prev_nonce is unset. + */ + bool null_prev_prev_nonce; + + /** + * Length of @e object_uids and @e object_incs + */ + size_t refs_len; + + /** + * Array of object UID references. + */ + struct SYNC_ObjectUID *object_uids; + + /** + * Array of object reference increments. + */ + int16_t *object_incs; + + /** + * Hash over @e object_uids and @e object_incs, as covered by + * @e upload_sig. Kept in the context because the request may be + * suspended for payment and resumed straight into the database + * operation, past the code that computes it. + */ + struct GNUNET_HashCode refs_hash; + + /** + * Context for a merchant payment. + */ + struct BackupPaymentContext bpc; +}; + + +/** + * Clean up and clear a block upload context, then terminate + * the request with an error response. + * + * @param request MHD request (used for response) + * @param buc context to clean up and free + * @param fi request info with app context pointer to clear + * @param sc HTTP status code + * @param ec Taler error code + */ +#define BAIL_WITH_ERROR(request, buc, fi, sc, ec) do { \ + GNUNET_break_op (0); \ + cleanup_block_upload_free (&(buc)->hc); \ + *(fi).v_app_context_ppvoid = NULL; \ + return TALER_MHD2_reply_with_error ( \ + (request), \ + (sc), \ + (ec), \ + NULL); \ +} while (0) + + +/** + * Clean up and clear a block upload context, then terminate + * the request with a response built elsewhere. + * + * @param request MHD request (used for response) + * @param buc context to clean up and free + * @param fi request info with app context pointer to clear + * @param resp response to return (must be non-NULL) + */ +#define BAIL_WITH_RESPONSE(request, buc, fi, resp) do { \ + cleanup_block_upload_free (&(buc)->hc); \ + *(fi).v_app_context_ppvoid = NULL; \ + return MHD_action_from_response ((request), \ + (resp)); \ +} while (0) + + +/** + * Log an operator error and return an upload action + * with a malformed-parameter error response. + * + * @param request MHD request + * @param buc block upload context (may have `block_data`, `object_uids`, + * `object_incs` allocated) + * @param root JSON root to decref + */ +#define BAIL_UPLOAD_MALFORMED(request, buc, root) do { \ + GNUNET_break_op (0); \ + json_decref (root); \ + cleanup_block_upload (buc); \ + return MHD_upload_action_from_response ( \ + (request), \ + TALER_MHD2_make_error ( \ + TALER_EC_GENERIC_PARAMETER_MALFORMED, \ + NULL)); \ +} while (0) + + +/** + * Terminate an upload handler by cleaning up the block upload + * context and returning a response. + * + * @param request MHD request + * @param buc block upload context to clean up + * @param resp response to return (must be non-NULL) + */ +#define BAIL_UPLOAD_RESPONSE(request, buc, resp) do { \ + cleanup_block_upload (buc); \ + return MHD_upload_action_from_response ((request), \ + (resp)); \ +} while (0) + + +/** + * Clean up memory used by a block upload context. + * Frees everything except @a buc itself when + * #cleanup_block_upload_free() is used. + * + * @param buc the block upload context to clean up + */ +static void +cleanup_block_upload (struct BlockUploadContext *buc) +{ + GNUNET_free (buc->block_data); + buc->block_data = NULL; + GNUNET_free (buc->object_uids); + buc->object_uids = NULL; + GNUNET_free (buc->object_incs); + buc->object_incs = NULL; + SH_payment_cleanup (&buc->bpc); +} + + +/** + * Clean up and free a block upload context. + * + * @param hc the @e hc member of the `struct BlockUploadContext` + * to clean up and free + */ +static void +cleanup_block_upload_free (struct TM_HandlerContext *hc) +{ + struct BlockUploadContext *buc; + + buc = (struct BlockUploadContext *) hc; + cleanup_block_upload (buc); + GNUNET_free (buc); +} + + +/** + * We got some query status from the DB. Handle the error cases. + * May perform asynchronous operations by suspending the request + * if required. + * + * @param buc request to handle status for + * @param qs query status to handle + * @param[out] suspend set to true if the request should be suspended + * @return MHD response to return + */ +static struct MHD_Response * +handle_database_error (struct BlockUploadContext *buc, + enum SYNC_DB_QueryStatus qs, + bool *suspend) +{ + *suspend = false; + switch (qs) + { + case SYNC_DB_TARGET_MISSING: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Update failed: no existing block\n"); + return TALER_MHD2_make_error (TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN, + NULL); + case SYNC_DB_OUTDATED_WRITE: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Conflict detected\n"); + return SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT, + TALER_EC_SYNC_CONFLICTING_WRITE, + NULL); + case SYNC_DB_INCONSISTENT_BACKUP: + /* Unknown object or refcount underflow; nothing was modified */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Object references do not match the stored backup\n"); + return SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT, + TALER_EC_SYNC_INCONSISTENT_BACKUP, + NULL); + case SYNC_DB_PAYMENT_REQUIRED: + { + struct MHD_StringNullable order_id; + + if ( (MHD_YES != + MHD_request_get_value (buc->bpc.request, + MHD_VK_URI_QUERY_PARAM, + "paying", + &order_id)) || + (NULL == order_id.cstr) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment required, starting payment process\n"); + return SH_begin_payment (&buc->bpc, + GNUNET_NO, + suspend); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment required, awaiting completion of `%s'\n", + order_id.cstr); + return SH_await_payment (&buc->bpc, + order_id.cstr, + suspend); + } + case SYNC_DB_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + case SYNC_DB_NO_RESULTS: + case SYNC_DB_ONE_RESULT: + /* not an error, so must not be passed here */ + GNUNET_assert (0); + return NULL; + } + GNUNET_break (0); + return TALER_MHD2_make_error (TALER_EC_SYNC_GENERIC_BACKEND_ERROR, + NULL); +} + + +/** + * Function to process data uploaded by a client. + * + * @param upload_cls our `struct BlockUploadContext *` + * @param request the request is being processed + * @param content_data_size the size of the @a content_data, + * zero when all data have been processed + * @param[in] content_data the uploaded content data, + * may be modified in the callback, + * valid only until return from the callback, + * NULL when all data have been processed + * @return action specifying how to proceed: + * #MHD_upload_action_continue() to continue upload (for incremental + * upload processing only), + * #MHD_upload_action_suspend() to stop reading the upload until + * the request is resumed, + * #MHD_upload_action_abort_request() to close the socket, + * or a response to discard the rest of the upload and transmit + * the response + */ +static const struct MHD_UploadAction * +handle_upload (void *upload_cls, + struct MHD_Request *request, + size_t content_data_size, + void *content_data) +{ + struct BlockUploadContext *buc = upload_cls; + + if (NULL != buc->bpc.resp) + { + /* This can happen on resume */ + return MHD_upload_action_from_response (request, + buc->bpc.resp); + } + if (NULL != buc->block_data) + { + /* Resume after payment; body was already parsed and verified */ + goto retry_db_op; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing %llu bytes of upload data\n", + (unsigned long long) content_data_size); + + /* parse JSON request */ + { + json_error_t jerr; + json_t *root; + const json_t *refs = NULL; + const json_t *relink_prev = NULL; + + root = json_loadb (content_data, + content_data_size, + 0, + &jerr); + if (NULL == root) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("upload_sig", + &buc->upload_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("prev_nonce", + &buc->prev_nonce), + &buc->null_prev_nonce), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("next_nonce", + &buc->next_nonce), + &buc->null_next_nonce), + GNUNET_JSON_spec_varsize ("data", + &buc->block_data, + &buc->block_data_len), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("object_refs", + &refs), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("relink_prev", + &relink_prev), + NULL), + GNUNET_JSON_spec_end (), + }; + const char *ename; + unsigned int eline; + + if (GNUNET_OK != GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + BAIL_UPLOAD_MALFORMED (request, + buc, + root); + + if (GNUNET_OK != + SYNC_object_refs_parse (refs, + &buc->refs_len, + &buc->object_uids, + &buc->object_incs)) + BAIL_UPLOAD_MALFORMED (request, + buc, + root); + + if (NULL != relink_prev) + { + struct GNUNET_JSON_Specification rspec[] = { + GNUNET_JSON_spec_fixed_auto ("upload_sig", + &buc->prev_upload_sig), + GNUNET_JSON_spec_end (), + }; + + buc->have_relink_prev = true; + if (GNUNET_OK != GNUNET_JSON_parse ((json_t *) relink_prev, + rspec, + &ename, + &eline)) + BAIL_UPLOAD_MALFORMED (request, + buc, + root); + } + + json_decref (root); + } + + /* Consistency of the request shape: only appends are supported (no + middle insert), and the tail is relinked by an append, so its + re-signature is mandatory when there is a tail. */ + { + if (! buc->is_update) + { + if (! buc->null_next_nonce) + BAIL_UPLOAD_MALFORMED (request, + buc, + NULL); + if (buc->null_prev_nonce != ! buc->have_relink_prev) + BAIL_UPLOAD_MALFORMED (request, + buc, + NULL); + } + else if (buc->have_relink_prev) + { + /* An update does not change the links. */ + BAIL_UPLOAD_MALFORMED (request, + buc, + NULL); + } + } + + /* Validate signature; it covers the object references too */ + { + if (GNUNET_OK != + SYNC_object_refs_hash (buc->refs_len, + buc->object_uids, + buc->object_incs, + &buc->refs_hash)) + { + GNUNET_break_op (0); + cleanup_block_upload (buc); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + "object_refs")); + } + if (GNUNET_OK != + SYNC_block_upload_verify (&buc->account_pub, + &buc->nonce, + ! buc->null_prev_nonce + ? &buc->prev_nonce + : NULL, + ! buc->null_next_nonce + ? &buc->next_nonce + : NULL, + &buc->old_data_hash, + &buc->new_data_hash, + &buc->refs_hash, + &buc->upload_sig)) + { + GNUNET_break_op (0); + cleanup_block_upload (buc); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE, + NULL)); + } + } + + /* An append relinks the current tail: verify the uploaded relink + signature against the tail as it is stored now. The append + transaction re-checks that the tail still hashes to what this + signature was verified against. */ + if (! buc->is_update && ! buc->null_prev_nonce) + { + struct GNUNET_HashCode tail_refs; + struct SYNC_BlockNonce tail_prev; + struct SYNC_BlockNonce tail_next; + size_t tail_data_size; + bool tail_is_first; + bool tail_is_last; + enum SYNC_DB_QueryStatus qs; + + qs = SYNCDB_lookup_block_TR (&buc->account_pub, + &buc->prev_nonce, + &buc->prev_hash, + &tail_prev, + &tail_next, + &tail_data_size, + &tail_refs, + &tail_is_first, + &tail_is_last); + switch (qs) + { + case SYNC_DB_ONE_RESULT: + break; + case SYNC_DB_TARGET_MISSING: + case SYNC_DB_PAYMENT_REQUIRED: + case SYNC_DB_HARD_ERROR: + case SYNC_DB_SOFT_ERROR: + { + bool suspend; + struct MHD_Response *resp; + + resp = handle_database_error (buc, + qs, + &suspend); + if (suspend) + return MHD_upload_action_suspend (request); + BAIL_UPLOAD_RESPONSE (request, + buc, + resp); + } + default: + GNUNET_break (0); + BAIL_UPLOAD_MALFORMED (request, + buc, + NULL); + } + + /* Remember the tail as it was verified against, so the transaction + can reject the append if it changed in the meantime */ + buc->null_prev_prev_nonce = tail_is_first; + if (! tail_is_first) + buc->prev_prev_nonce = tail_prev; + + if (GNUNET_OK != + SYNC_block_upload_verify (&buc->account_pub, + &buc->prev_nonce, + tail_is_first + ? NULL + : &tail_prev, + &buc->nonce, + &buc->prev_hash, + &buc->prev_hash, + &tail_refs, + &buc->prev_upload_sig)) + { + GNUNET_break_op (0); + cleanup_block_upload (buc); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE, + NULL)); + } + } + + /* Check hash matches */ + { + struct GNUNET_HashCode our_hash; + + GNUNET_CRYPTO_hash (buc->block_data, + buc->block_data_len, + &our_hash); + if (0 != GNUNET_memcmp (&our_hash, + &buc->new_data_hash)) + { + GNUNET_break_op (0); + cleanup_block_upload (buc); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error ( + TALER_EC_SYNC_INVALID_UPLOAD, + NULL)); + } + } + +retry_db_op: + /* store backup to database */ + { + enum SYNC_DB_QueryStatus qs; + + if (! buc->is_update) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Uploading a new block\n"); + qs = SYNCDB_append_block_TR ( + &buc->account_pub, + &buc->nonce, + (! buc->null_prev_nonce && + ! GNUNET_is_zero (&buc->prev_nonce)) + ? &buc->prev_nonce + : NULL, + &buc->new_data_hash, + buc->block_data_len, + buc->block_data, + buc->refs_len, + buc->object_uids, + buc->object_incs, + &buc->upload_sig, + &buc->refs_hash, + (! buc->null_prev_nonce && + ! GNUNET_is_zero (&buc->prev_nonce)) + ? &buc->prev_upload_sig + : NULL, + (! buc->null_prev_nonce && + ! GNUNET_is_zero (&buc->prev_nonce)) + ? &buc->prev_hash + : NULL, + (! buc->null_prev_nonce && + ! GNUNET_is_zero (&buc->prev_nonce) && + ! buc->null_prev_prev_nonce) + ? &buc->prev_prev_nonce + : NULL); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Updating an existing block\n"); + qs = SYNCDB_update_block_TR ( + &buc->account_pub, + &buc->nonce, + (! buc->null_prev_nonce && + ! GNUNET_is_zero (&buc->prev_nonce)) + ? &buc->prev_nonce + : NULL, + (! buc->null_next_nonce && + ! GNUNET_is_zero (&buc->next_nonce)) + ? &buc->next_nonce + : NULL, + &buc->old_data_hash, + &buc->new_data_hash, + buc->block_data_len, + buc->block_data, + buc->refs_len, + buc->object_uids, + buc->object_incs, + &buc->upload_sig, + &buc->refs_hash); + } + if (qs < 0) + { + bool suspend; + struct MHD_Response *resp; + + resp = handle_database_error (buc, + qs, + &suspend); + if (suspend) + return MHD_upload_action_suspend (request); + BAIL_UPLOAD_RESPONSE (request, + buc, + resp); + } + if (0 == qs) + { + /* database says nothing actually changed, 304 (could + theoretically happen if another equivalent upload succeeded + since we last checked!) */ + struct MHD_Response *resp; + + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); + TALER_MHD2_add_global_headers (resp, + false); + BAIL_UPLOAD_RESPONSE (request, + buc, + resp); + } + } + + /* generate main (204) standard success reply */ + { + struct MHD_Response *resp; + + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT); + TALER_MHD2_add_global_headers (resp, + false); + BAIL_UPLOAD_RESPONSE (request, + buc, + resp); + } +} + + +const struct MHD_Action * +SH_backup_block_post (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update) +{ + struct BlockUploadContext *buc; + union MHD_RequestInfoFixedData fi; + + GNUNET_assert (MHD_SC_OK == + MHD_request_get_info_fixed ( + request, + MHD_REQUEST_INFO_FIXED_APP_CONTEXT, + &fi)); + buc = *fi.v_app_context_ppvoid; + if (NULL == buc) + { + /* first call, setup internals */ + buc = GNUNET_new (struct BlockUploadContext); + buc->hc.cc = &cleanup_block_upload_free; + buc->bpc.request = request; + buc->is_update = is_update; + buc->account_pub = *account_pub; + buc->bpc.account_pub = *account_pub; + *fi.v_app_context_ppvoid = buc; + } + + if (NULL != buc->bpc.resp) + BAIL_WITH_RESPONSE (request, + buc, + fi, + buc->bpc.resp); + + if (SH_upload_too_large (upload_size, + &buc->buff_size)) + BAIL_WITH_ERROR (request, + buc, + fi, + MHD_HTTP_STATUS_CONTENT_TOO_LARGE, + TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH); + + { + struct MHD_StringNullable fresh; + + if (MHD_YES == + MHD_request_get_value (request, + MHD_VK_URI_QUERY_PARAM, + "fresh", + &fresh)) + buc->bpc.force_fresh_order = true; + } + + { + struct MHD_StringNullable im; + + /* old_data_hash only provided for block update */ + if (is_update && + (MHD_YES == + MHD_request_get_value (request, + MHD_VK_HEADER, + MHD_HTTP_HEADER_IF_MATCH, + &im)) && + ( (2 >= im.len) || + ('"' != im.cstr[0]) || + ('"' != im.cstr[im.len - 1]) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (im.cstr + 1, + im.len - 2, + &buc->old_data_hash, + sizeof (buc->old_data_hash))) )) + BAIL_WITH_ERROR (request, + buc, + fi, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_SYNC_BAD_IF_MATCH); + } + + { + struct MHD_StringNullable etag; + + if ( (MHD_YES != + MHD_request_get_value (request, + MHD_VK_HEADER, + MHD_HTTP_HEADER_IF_NONE_MATCH, + &etag)) || + (2 >= etag.len) || + ('"' != etag.cstr[0]) || + ('"' != etag.cstr[etag.len - 1]) || + (GNUNET_OK != + GNUNET_STRINGS_string_to_data (etag.cstr + 1, + etag.len - 2, + &buc->new_data_hash, + sizeof (buc->new_data_hash))) ) + { + BAIL_WITH_ERROR (request, + buc, + fi, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_SYNC_BAD_IF_NONE_MATCH); + } + } + + { + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &buc->nonce, + sizeof (buc->nonce))) + { + BAIL_WITH_ERROR (request, + buc, + fi, + MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED); + } + } + + /* Check database to see if the transaction is permissible */ + { + struct GNUNET_HashCode hc; + size_t data_size; + struct GNUNET_HashCode refs_hash; + bool is_first; + bool is_last; + enum SYNC_DB_QueryStatus qs; + + qs = SYNCDB_lookup_block_TR (&buc->account_pub, + &buc->nonce, + &hc, + &buc->prev_nonce, + &buc->next_nonce, + &data_size, + &refs_hash, + &is_first, + &is_last); + + if (qs < 0) + { + bool suspend; + struct MHD_Response *resp; + + if ((SYNC_DB_TARGET_MISSING == qs) && + (! buc->is_update)) + { + /* New upload: block doesn't exist yet, that's fine */ + qs = SYNC_DB_NO_RESULTS; + memset (&hc, + 0, + sizeof (hc)); + } + else + { + resp = handle_database_error (buc, + qs, + &suspend); + if (suspend) + return MHD_action_suspend (request); + BAIL_WITH_RESPONSE (request, + buc, + fi, + resp); + } + } + + if (0 == GNUNET_memcmp (&hc, + &buc->new_data_hash)) + { + /* Refuse upload, we already have that backup! */ + struct MHD_Response *resp; + + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED); + TALER_MHD2_add_global_headers (resp, + false); + BAIL_WITH_RESPONSE (request, + buc, + fi, + resp); + } + } + + /* Check if the client insists on paying */ + { + struct MHD_StringNullable order_req; + + if (MHD_YES == + MHD_request_get_value (request, + MHD_VK_URI_QUERY_PARAM, + "pay", + &order_req)) + { + struct MHD_Response *resp; + bool suspend; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment requested, starting payment process\n"); + resp = SH_begin_payment (&buc->bpc, + GNUNET_YES, + &suspend); + if (suspend) + return MHD_action_suspend (request); + BAIL_WITH_RESPONSE (request, + buc, + fi, + resp); + } + } + + /* Ready to begin upload! + + FIXME: taking the body in one piece makes MHD buffer it whole out + of the daemon-wide "large" pool, which is why sync-httpd2.c has to + size that pool from UPLOAD_LIMIT_MB. Switching to + MHD_action_process_upload() with an incremental callback would let + MHD stream the body instead and drop the pool dependency, at the + cost of this handler having to accumulate (or parse) the JSON + across calls. */ + return MHD_action_process_upload_full (request, + buc->buff_size, + &handle_upload, + buc); +} diff --git a/src/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.h b/src/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_post-backups-KEY-blocks-NONCE.h + * @brief Handle POST /backups/$KEY/blocks/$NONCE requests + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_POST_BACKUPS_KEY_BLOCKS_NONCE_H +#define SYNC_HTTPD2_POST_BACKUPS_KEY_BLOCKS_NONCE_H + +#include <microhttpd2.h> +#include "sync/sync_service.h" + + +/** + * Handle a POST "/backups/$KEY/blocks/$NONCE" request. + * + * @param request the MHD request to handle + * @param account_pub public key of the account + * @param args path segments: [0]=encoded NONCE + * @param upload_size number of bytes being uploaded + * @param is_update whether the operation is an update + * @return MHD action + */ +const struct MHD_Action * +SH_backup_block_post (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update); + + +#endif diff --git a/src/sync/sync-httpd2_post-backups-KEY-objects-UID.c b/src/sync/sync-httpd2_post-backups-KEY-objects-UID.c @@ -0,0 +1,313 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_post-backups-KEY-objects-UID.c + * @brief Handle POST /backups/$KEY/objects/$UID requests + * @author Iván Ávalos + */ +#include "platform.h" +#include "sync-httpd2.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_mhd2_lib.h> +#include <taler/taler_signatures.h> +#include <sync/sync_signatures.h> +#include "sync-httpd2_post-backups-KEY-objects-UID.h" + + +/** + * Default lifetime for uploaded objects. + */ +#define OBJECT_DEFAULT_LIFETIME GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_DAYS, 30) + + +/** + * Context for an object upload operation. + */ +struct ObjectUploadContext +{ + /** + * Context for cleanup logic. + */ + struct TM_HandlerContext hc; + + /** + * Buffer size to use for reading the request body. + */ + size_t buff_size; + + /** + * Public key of the account holder. + */ + struct SYNC_AccountPublicKeyP account_pub; + + /** + * UID of the object to upload. + */ + struct SYNC_ObjectUID uid; + + /** + * Object data. + */ + void *data; + + /** + * Length of @e data. + */ + size_t data_size; + + /** + * Hash of @e data. + */ + struct GNUNET_HashCode data_hash; + + /** + * Signature from the request. + */ + struct SYNC_AccountSignatureP object_sig; +}; + + +/** + * Clean up and free an object upload context. + * + * @param hc the @e hc member of the context to clean up and free + */ +static void +cleanup_object_upload_free (struct TM_HandlerContext *hc) +{ + struct ObjectUploadContext *ouc; + + ouc = (struct ObjectUploadContext *) hc; + GNUNET_free (ouc->data); + GNUNET_free (ouc); +} + + +/** + * Function to process data uploaded by a client. + * + * @param upload_cls our `struct ObjectUploadContext *` + * @param request the request being processed + * @param content_data_size the size of the @a content_data, + * zero when all data have been processed + * @param[in] content_data the uploaded content data + * @return action specifying how to proceed + */ +static const struct MHD_UploadAction * +handle_object_upload (void *upload_cls, + struct MHD_Request *request, + size_t content_data_size, + void *content_data) +{ + struct ObjectUploadContext *ouc = upload_cls; + + if ( (0 == content_data_size) && + (NULL == content_data) ) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + + /* parse JSON request */ + { + json_error_t jerr; + json_t *root; + + root = json_loadb (content_data, + content_data_size, + 0, + &jerr); + if (NULL == root) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("object_sig", + &ouc->object_sig), + GNUNET_JSON_spec_varsize ("data", + &ouc->data, + &ouc->data_size), + GNUNET_JSON_spec_end (), + }; + const char *ename; + unsigned int eline; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + { + GNUNET_break_op (0); + json_decref (root); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED, + NULL)); + } + json_decref (root); + } + + /* Compute hash of data */ + GNUNET_CRYPTO_hash (ouc->data, + ouc->data_size, + &ouc->data_hash); + + /* Verify signature */ + if (GNUNET_OK != + SYNC_object_upload_verify (&ouc->account_pub, + &ouc->uid, + &ouc->data_hash, + &ouc->object_sig)) + { + GNUNET_break_op (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE, + NULL)); + } + + /* Store object */ + { + enum SYNC_DB_QueryStatus qs; + + qs = SYNCDB_store_object_TR (&ouc->uid, + &ouc->account_pub, + &ouc->data_hash, + OBJECT_DEFAULT_LIFETIME, + ouc->data_size, + ouc->data); + switch (qs) + { + case SYNC_DB_ONE_RESULT: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Object stored successfully\n"); + { + struct MHD_Response *resp; + + resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT); + TALER_MHD2_add_global_headers (resp, + false); + return MHD_upload_action_from_response (request, + resp); + } + case SYNC_DB_PAYMENT_REQUIRED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Object upload failed: payment required\n"); + return MHD_upload_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_PAYMENT_REQUIRED, + TALER_EC_SYNC_ACCOUNT_UNKNOWN, + NULL)); + case SYNC_DB_HARD_ERROR: + GNUNET_break (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL)); + case SYNC_DB_SOFT_ERROR: + GNUNET_break (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL)); + default: + GNUNET_break (0); + return MHD_upload_action_from_response ( + request, + TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL)); + } + } +} + + +const struct MHD_Action * +SH_backup_object_post (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update) +{ + struct ObjectUploadContext *ouc; + union MHD_RequestInfoFixedData fi; + + (void) is_update; + + GNUNET_assert (MHD_SC_OK == + MHD_request_get_info_fixed ( + request, + MHD_REQUEST_INFO_FIXED_APP_CONTEXT, + &fi)); + ouc = *fi.v_app_context_ppvoid; + if (NULL == ouc) + { + /* first call, setup internals */ + ouc = GNUNET_new (struct ObjectUploadContext); + ouc->hc.cc = &cleanup_object_upload_free; + ouc->account_pub = *account_pub; + *fi.v_app_context_ppvoid = ouc; + + /* Parse UID from URL */ + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (args[0], + strlen (args[0]), + &ouc->uid, + sizeof (ouc->uid))) + { + cleanup_object_upload_free (&ouc->hc); + *fi.v_app_context_ppvoid = NULL; + return MHD_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "uid")); + } + } + + /* Same upload limit as for blocks */ + if (SH_upload_too_large (upload_size, + &ouc->buff_size)) + { + cleanup_object_upload_free (&ouc->hc); + *fi.v_app_context_ppvoid = NULL; + return MHD_action_from_response ( + request, + SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONTENT_TOO_LARGE, + TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH, + NULL)); + } + + /* Ready to begin reading the body */ + return MHD_action_process_upload_full (request, + ouc->buff_size, + &handle_object_upload, + ouc); +} + + +/* end of sync-httpd2_post-backups-KEY-objects-UID.c */ diff --git a/src/sync/sync-httpd2_post-backups-KEY-objects-UID.h b/src/sync/sync-httpd2_post-backups-KEY-objects-UID.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file sync-httpd2_post-backups-KEY-objects-UID.h + * @brief Handle POST /backups/$KEY/objects/$UID requests + * @author Christian Grothoff + */ +#ifndef SYNC_HTTPD2_POST_BACKUPS_KEY_OBJECTS_UID_H +#define SYNC_HTTPD2_POST_BACKUPS_KEY_OBJECTS_UID_H + +#include <microhttpd2.h> +#include "sync/sync_service.h" + + +/** + * Handle a POST "/backups/$KEY/objects/$UID" request. + * + * @param request the MHD request to handle + * @param account_pub public key of the account + * @param args path segments: [0]="objects", [1]=encoded UID + * @param upload_size number of bytes being uploaded + * @param is_update whether the operation is an update + * @return MHD action + */ +const struct MHD_Action * +SH_backup_object_post (struct MHD_Request *request, + const struct SYNC_AccountPublicKeyP *account_pub, + const char *const args[], + uint_fast64_t upload_size, + bool is_update); + + +#endif diff --git a/src/sync/sync-httpd_backup.c b/src/sync/sync-httpd_backup.c @@ -1,263 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync-httpd_backup.c - * @brief functions to handle incoming requests for backups - * @author Christian Grothoff - */ -#include "platform.h" -#include "sync-httpd.h" -#include <gnunet/gnunet_util_lib.h> -#include "sync-httpd_backup.h" - - -/** - * Handle request on @a connection for retrieval of the latest - * backup of @a account. - * - * @param connection the MHD connection to handle - * @param account public key of the account the request is for - * @return MHD result code - */ -enum MHD_Result -SH_backup_get (struct MHD_Connection *connection, - const struct SYNC_AccountPublicKeyP *account) -{ - struct GNUNET_HashCode backup_hash; - enum SYNC_DB_QueryStatus qs; - enum MHD_Result ret; - - qs = SYNCDB_lookup_account_TR ( - account, - &backup_hash); - switch (qs) - { - case SYNC_DB_OLD_BACKUP_MISSING: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - case SYNC_DB_OLD_BACKUP_MISMATCH: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - case SYNC_DB_PAYMENT_REQUIRED: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_SYNC_ACCOUNT_UNKNOWN, - NULL); - case SYNC_DB_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case SYNC_DB_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - case SYNC_DB_NO_RESULTS: - { - struct MHD_Response *resp; - - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (resp, - false); - ret = MHD_queue_response (connection, - MHD_HTTP_NO_CONTENT, - resp); - MHD_destroy_response (resp); - } - return ret; - case SYNC_DB_ONE_RESULT: - { - const char *inm; - - inm = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if ( (NULL != inm) && - (2 < strlen (inm)) && - ('"' == inm[0]) && - ('=' == inm[strlen (inm) - 1]) ) - { - struct GNUNET_HashCode inm_h; - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (inm + 1, - strlen (inm) - 2, - &inm_h, - sizeof (inm_h))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_SYNC_BAD_IF_NONE_MATCH, - "Etag does not include a base32-encoded SHA-512 hash"); - } - if (0 == GNUNET_memcmp (&inm_h, - &backup_hash)) - { - struct MHD_Response *resp; - - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (resp, - false); - ret = MHD_queue_response (connection, - MHD_HTTP_NOT_MODIFIED, - resp); - MHD_destroy_response (resp); - return ret; - } - } - } - /* We have a result, should fetch and return it! */ - break; - } - return SH_return_backup (connection, - account, - MHD_HTTP_OK); -} - - -/** - * Return the current backup of @a account on @a connection - * using @a default_http_status on success. - * - * @param connection MHD connection to use - * @param account account to query - * @param default_http_status HTTP status to queue response - * with on success (#MHD_HTTP_OK or #MHD_HTTP_CONFLICT) - * @return MHD result code - */ -enum MHD_Result -SH_return_backup (struct MHD_Connection *connection, - const struct SYNC_AccountPublicKeyP *account, - unsigned int default_http_status) -{ - enum SYNC_DB_QueryStatus qs; - struct MHD_Response *resp; - enum MHD_Result ret; - struct SYNC_AccountSignatureP account_sig; - struct GNUNET_HashCode backup_hash; - struct GNUNET_HashCode prev_hash; - size_t backup_size; - void *backup; - - qs = SYNCDB_lookup_backup_TR ( - account, - &account_sig, - &prev_hash, - &backup_hash, - &backup_size, - &backup); - switch (qs) - { - case SYNC_DB_OLD_BACKUP_MISSING: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected return status (backup missing)"); - case SYNC_DB_OLD_BACKUP_MISMATCH: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected return status (backup mismatch)"); - case SYNC_DB_PAYMENT_REQUIRED: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected return status (payment required)"); - case SYNC_DB_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case SYNC_DB_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - case SYNC_DB_NO_RESULTS: - GNUNET_break (0); - /* Note: can theoretically happen due to non-transactional nature if - the backup expired / was gc'ed JUST between the two SQL calls. - But too rare to handle properly, as doing a transaction would be - expensive. Just admit to failure ;-) */ - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - NULL); - case SYNC_DB_ONE_RESULT: - /* interesting case below */ - break; - } - resp = MHD_create_response_from_buffer (backup_size, - backup, - MHD_RESPMEM_MUST_FREE); - TALER_MHD_add_global_headers (resp, - false); - { - char *sig_s; - char *prev_s; - char *etag; - char *etagq; - - sig_s = GNUNET_STRINGS_data_to_string_alloc (&account_sig, - sizeof (account_sig)); - prev_s = GNUNET_STRINGS_data_to_string_alloc (&prev_hash, - sizeof (prev_hash)); - etag = GNUNET_STRINGS_data_to_string_alloc (&backup_hash, - sizeof (backup_hash)); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - "Sync-Signature", - sig_s)); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - "Sync-Previous", - prev_s)); - GNUNET_asprintf (&etagq, - "\"%s\"", - etag); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_ETAG, - etagq)); - GNUNET_free (etagq); - GNUNET_free (etag); - GNUNET_free (prev_s); - GNUNET_free (sig_s); - } - ret = MHD_queue_response (connection, - default_http_status, - resp); - MHD_destroy_response (resp); - return ret; -} diff --git a/src/sync/sync-httpd_backup.h b/src/sync/sync-httpd_backup.h @@ -1,79 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014, 2015, 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync-httpd_backup.h - * @brief functions to handle incoming requests on /backup/ - * @author Christian Grothoff - */ -#ifndef SYNC_HTTPD_BACKUP_H -#define SYNC_HTTPD_BACKUP_H -#include <microhttpd.h> - -/** - * Service is shutting down, resume all MHD connections NOW. - */ -void -SH_resume_all_bc (void); - - -/** - * Return the current backup of @a account on @a connection - * using @a default_http_status on success. - * - * @param connection MHD connection to use - * @param account account to query - * @param default_http_status HTTP status to queue response - * with on success (#MHD_HTTP_OK or #MHD_HTTP_CONFLICT) - * @return MHD result code - */ -enum MHD_Result -SH_return_backup (struct MHD_Connection *connection, - const struct SYNC_AccountPublicKeyP *account, - unsigned int default_http_status); - - -/** - * Handle request on @a connection for retrieval of the latest - * backup of @a account. - * - * @param connection the MHD connection to handle - * @param account public key of the account the request is for - * @return MHD result code - */ -enum MHD_Result -SH_backup_get (struct MHD_Connection *connection, - const struct SYNC_AccountPublicKeyP *account); - - -/** - * Handle POST /backup requests. - * - * @param connection the MHD connection to handle - * @param[in,out] con_cls the connection's closure (can be updated) - * @param account public key of the account the request is for - * @param upload_data upload data - * @param[in,out] upload_data_size number of bytes (left) in @a upload_data - * @return MHD result code - */ -enum MHD_Result -SH_backup_post (struct MHD_Connection *connection, - void **con_cls, - const struct SYNC_AccountPublicKeyP *account, - const char *upload_data, - size_t *upload_data_size); - - -#endif diff --git a/src/sync/sync-httpd_backup_post.c b/src/sync/sync-httpd_backup_post.c @@ -1,1055 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync-httpd_backup_post.c - * @brief functions to handle incoming requests for backups - * @author Christian Grothoff - */ -#include "platform.h" -struct BackupContext; -#define TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE struct BackupContext -#define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE struct BackupContext -#include "sync-httpd.h" -#include <gnunet/gnunet_util_lib.h> -#include "sync-httpd_backup.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_merchant_service.h> -#include <taler/taler_signatures.h> - - -/** - * How long do we hold an HTTP client connection if - * we are awaiting payment before giving up? - */ -#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_MINUTES, 30) - - -/** - * Context for an upload operation. - */ -struct BackupContext -{ - - /** - * Context for cleanup logic. - */ - struct TM_HandlerContext hc; - - /** - * Signature of the account holder. - */ - struct SYNC_AccountSignatureP account_sig; - - /** - * Public key of the account holder. - */ - struct SYNC_AccountPublicKeyP account; - - /** - * Hash of the previous upload, or zeros if first upload. - */ - struct GNUNET_HashCode old_backup_hash; - - /** - * Hash of the upload we are receiving right now (as promised - * by the client, to be verified!). - */ - struct GNUNET_HashCode new_backup_hash; - - /** - * Claim token, all zeros if not known. Only set if @e existing_order_id is non-NULL. - */ - struct TALER_ClaimTokenP token; - - /** - * Hash context for the upload. - */ - struct GNUNET_HashContext *hash_ctx; - - /** - * Kept in DLL for shutdown handling while suspended. - */ - struct BackupContext *next; - - /** - * Kept in DLL for shutdown handling while suspended. - */ - struct BackupContext *prev; - - /** - * Used while suspended for resumption. - */ - struct MHD_Connection *con; - - /** - * Upload, with as many bytes as we have received so far. - */ - char *upload; - - /** - * Used while we are awaiting proposal creation. - */ - struct TALER_MERCHANT_PostPrivateOrdersHandle *po; - - /** - * Used while we are waiting payment. - */ - struct TALER_MERCHANT_GetPrivateOrderHandle *omgh; - - /** - * HTTP response code to use on resume, if non-NULL. - - */ - struct MHD_Response *resp; - - /** - * Order under which the client promised payment, or NULL. - */ - const char *order_id; - - /** - * Order ID for the client that we found in our database. - */ - char *existing_order_id; - - /** - * Timestamp of the order in @e existing_order_id. Used to - * select the most recent unpaid offer. - */ - struct GNUNET_TIME_Timestamp existing_order_timestamp; - - /** - * Expected total upload size. - */ - size_t upload_size; - - /** - * Current offset for the upload. - */ - size_t upload_off; - - /** - * HTTP response code to use on resume, if resp is set. - */ - unsigned int response_code; - - /** - * Do not look for an existing order, force a fresh order to be created. - */ - bool force_fresh_order; -}; - - -/** - * Kept in DLL for shutdown handling while suspended. - */ -static struct BackupContext *bc_head; - -/** - * Kept in DLL for shutdown handling while suspended. - */ -static struct BackupContext *bc_tail; - - -/** - * Service is shutting down, resume all MHD connections NOW. - */ -void -SH_resume_all_bc () -{ - struct BackupContext *bc; - - while (NULL != (bc = bc_head)) - { - GNUNET_CONTAINER_DLL_remove (bc_head, - bc_tail, - bc); - MHD_resume_connection (bc->con); - if (NULL != bc->po) - { - TALER_MERCHANT_post_private_orders_cancel (bc->po); - bc->po = NULL; - } - if (NULL != bc->omgh) - { - TALER_MERCHANT_get_private_order_cancel (bc->omgh); - bc->omgh = NULL; - } - } -} - - -/** - * Function called to clean up a backup context. - * - * @param hc a `struct BackupContext` - */ -static void -cleanup_ctx (struct TM_HandlerContext *hc) -{ - struct BackupContext *bc = (struct BackupContext *) hc; - - if (NULL != bc->po) - { - TALER_MERCHANT_post_private_orders_cancel (bc->po); - bc->po = NULL; - } - if (NULL != bc->omgh) - { - TALER_MERCHANT_get_private_order_cancel (bc->omgh); - bc->omgh = NULL; - } - if (NULL != bc->hash_ctx) - GNUNET_CRYPTO_hash_context_abort (bc->hash_ctx); - if (NULL != bc->resp) - { - MHD_destroy_response (bc->resp); - bc->resp = NULL; - } - GNUNET_free (bc->existing_order_id); - GNUNET_free (bc->upload); - GNUNET_free (bc); -} - - -/** - * Transmit a payment request for @a order_id on @a connection - * - * @param order_id our backend's order ID - * @param token the claim token generated by the merchant (NULL if - * it wasn't generated). - * @return MHD response to use - */ -static struct MHD_Response * -make_payment_request (const char *order_id, - const struct TALER_ClaimTokenP *token) -{ - struct MHD_Response *resp; - - /* request payment via Taler */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Creating payment request for order `%s'\n", - order_id); - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (resp, - false); - { - char *hdr; - const char *pfx; - char *hn; - struct GNUNET_Buffer hdr_buf = { 0 }; - - if (0 == strncasecmp ("https://", - SH_backend_url, - strlen ("https://"))) - { - pfx = "taler://"; - hn = &SH_backend_url[strlen ("https://")]; - } - else if (0 == strncasecmp ("http://", - SH_backend_url, - strlen ("http://"))) - { - pfx = "taler+http://"; - hn = &SH_backend_url[strlen ("http://")]; - } - else - { - GNUNET_break (0); - MHD_destroy_response (resp); - return NULL; - } - if (0 == strlen (hn)) - { - GNUNET_break (0); - MHD_destroy_response (resp); - return NULL; - } - - GNUNET_buffer_write_str (&hdr_buf, pfx); - GNUNET_buffer_write_str (&hdr_buf, "pay/"); - GNUNET_buffer_write_str (&hdr_buf, hn); - GNUNET_buffer_write_path (&hdr_buf, order_id); - /* No session ID */ - GNUNET_buffer_write_path (&hdr_buf, ""); - if (NULL != token) - { - GNUNET_buffer_write_str (&hdr_buf, "?c="); - GNUNET_buffer_write_data_encoded (&hdr_buf, token, sizeof (*token)); - } - hdr = GNUNET_buffer_reap_str (&hdr_buf); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - "Taler", - hdr)); - GNUNET_free (hdr); - } - return resp; -} - - -/** - * Callbacks of this type are used to serve the result of submitting a - * /contract request to a merchant. - * - * @param cls our `struct BackupContext` - * @param por response details - */ -static void -proposal_cb ( - struct BackupContext *bc, - const struct TALER_MERCHANT_PostPrivateOrdersResponse *por) -{ - enum SYNC_DB_QueryStatus qs; - - bc->po = NULL; - GNUNET_CONTAINER_DLL_remove (bc_head, - bc_tail, - bc); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming connection with order `%s'\n", - bc->order_id); - MHD_resume_connection (bc->con); - SH_trigger_daemon (); - if (MHD_HTTP_OK != por->hr.http_status) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Backend returned status %u/%u\n", - por->hr.http_status, - (unsigned int) por->hr.ec); - GNUNET_break_op (0); - bc->resp = TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec (TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR), - GNUNET_JSON_pack_uint64 ("backend-ec", - por->hr.ec), - GNUNET_JSON_pack_uint64 ("backend-http-status", - por->hr.http_status), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("backend-reply", - (json_t *) por->hr.reply))); - bc->response_code = MHD_HTTP_BAD_GATEWAY; - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Storing payment request for order `%s'\n", - por->details.ok.order_id); - qs = SYNCDB_store_payment_TR ( - &bc->account, - por->details.ok.order_id, - por->details.ok.token, - &SH_annual_fee); - if (0 >= qs) - { - GNUNET_break (0); - bc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "Failed to persist payment request in sync database"); - GNUNET_assert (NULL != bc->resp); - bc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Obtained fresh order `%s'\n", - por->details.ok.order_id); - bc->resp = make_payment_request (por->details.ok.order_id, - por->details.ok.token); - GNUNET_assert (NULL != bc->resp); - bc->response_code = MHD_HTTP_PAYMENT_REQUIRED; -} - - -/** - * Function called on all pending payments for the right - * account. - * - * @param cls closure, our `struct BackupContext` - * @param timestamp for how long have we been waiting - * @param order_id order id in the backend - * @param token claim token to use (or NULL for none) - * @param amount how much is the order for - */ -static void -ongoing_payment_cb (void *cls, - struct GNUNET_TIME_Timestamp timestamp, - const char *order_id, - const struct TALER_ClaimTokenP *token, - const struct TALER_Amount *amount) -{ - struct BackupContext *bc = cls; - - (void) amount; - if (0 != TALER_amount_cmp (amount, - &SH_annual_fee)) - return; /* can't reuse, fees changed */ - if ( (NULL == bc->existing_order_id) || - (GNUNET_TIME_timestamp_cmp (bc->existing_order_timestamp, - <, - timestamp)) ) - { - GNUNET_free (bc->existing_order_id); - bc->existing_order_id = GNUNET_strdup (order_id); - bc->existing_order_timestamp = timestamp; - if (NULL != token) - bc->token = *token; - } -} - - -/** - * Callback to process a GET /check-payment request - * - * @param cls our `struct BackupContext` - * @param osr order status - */ -static void -check_payment_cb (struct BackupContext *bc, - const struct TALER_MERCHANT_GetPrivateOrderResponse *osr) -{ - const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; - - /* refunds are not supported, verify */ - bc->omgh = NULL; - GNUNET_CONTAINER_DLL_remove (bc_head, - bc_tail, - bc); - MHD_resume_connection (bc->con); - SH_trigger_daemon (); - switch (hr->http_status) - { - case 0: - /* Likely timeout, complain! */ - bc->response_code = MHD_HTTP_GATEWAY_TIMEOUT; - bc->resp = TALER_MHD_make_error ( - TALER_EC_SYNC_GENERIC_BACKEND_TIMEOUT, - NULL); - return; - case MHD_HTTP_OK: - break; /* handled below */ - default: - /* Unexpected backend response */ - bc->response_code = MHD_HTTP_BAD_GATEWAY; - bc->resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("code", - TALER_EC_SYNC_GENERIC_BACKEND_ERROR), - GNUNET_JSON_pack_string ("hint", - TALER_ErrorCode_get_hint ( - TALER_EC_SYNC_GENERIC_BACKEND_ERROR)), - GNUNET_JSON_pack_uint64 ("backend-ec", - (json_int_t) hr->ec), - GNUNET_JSON_pack_uint64 ("backend-http-status", - (json_int_t) hr->http_status), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("backend-reply", - (json_t *) hr->reply))); - return; - } - - GNUNET_assert (MHD_HTTP_OK == hr->http_status); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment status checked: %d\n", - osr->details.ok.status); - switch (osr->details.ok.status) - { - case TALER_MERCHANT_OSC_PAID: - { - enum SYNC_DB_QueryStatus qs; - - qs = SYNCDB_increment_lifetime_TR ( - &bc->account, - bc->order_id, - GNUNET_TIME_UNIT_YEARS); /* always annual */ - if (0 <= qs) - return; /* continue as planned */ - GNUNET_break (0); - bc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, - "increment lifetime"); - GNUNET_assert (NULL != bc->resp); - bc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - return; /* continue as planned */ - } - case TALER_MERCHANT_OSC_UNPAID: - case TALER_MERCHANT_OSC_CLAIMED: - break; - } - if (NULL != bc->existing_order_id) - { - /* repeat payment request */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Repeating payment request\n"); - bc->resp = make_payment_request (bc->existing_order_id, - (GNUNET_YES == GNUNET_is_zero (&bc->token)) - ? NULL - : &bc->token); - GNUNET_assert (NULL != bc->resp); - bc->response_code = MHD_HTTP_PAYMENT_REQUIRED; - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Timeout waiting for payment\n"); - bc->resp = TALER_MHD_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT, - "Timeout awaiting promised payment"); - GNUNET_assert (NULL != bc->resp); - bc->response_code = MHD_HTTP_REQUEST_TIMEOUT; -} - - -/** - * Helper function used to ask our backend to await - * a payment for the user's account. - * - * @param bc context to begin payment for. - * @param timeout when to give up trying - * @param order_id which order to check for the payment - */ -static void -await_payment (struct BackupContext *bc, - struct GNUNET_TIME_Relative timeout, - const char *order_id) -{ - GNUNET_CONTAINER_DLL_insert (bc_head, - bc_tail, - bc); - MHD_suspend_connection (bc->con); - bc->order_id = order_id; - bc->omgh = TALER_MERCHANT_get_private_order_create ( - SH_ctx, - SH_backend_url, - order_id); - GNUNET_assert (GNUNET_OK == - TALER_MERCHANT_get_private_order_set_options ( - bc->omgh, - TALER_MERCHANT_get_private_order_option_timeout (timeout))); - GNUNET_assert (TALER_EC_NONE == - TALER_MERCHANT_get_private_order_start ( - bc->omgh, - &check_payment_cb, - bc)); - SH_trigger_curl (); -} - - -/** - * Helper function used to ask our backend to begin - * processing a payment for the user's account. - * May perform asynchronous operations by suspending the connection - * if required. - * - * @param bc context to begin payment for. - * @param pay_req #GNUNET_YES if payment was explicitly requested, - * #GNUNET_NO if payment is needed - * @return MHD status code - */ -static enum MHD_Result -begin_payment (struct BackupContext *bc, - int pay_req) -{ - if (! bc->force_fresh_order) - { - enum GNUNET_DB_QueryStatus qs; - - qs = SYNCDB_lookup_pending_payments_by_account_TR ( - &bc->account, - &ongoing_payment_cb, - bc); - if (qs < 0) - { - struct MHD_Response *resp; - enum MHD_Result ret; - - resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED, - "pending payments"); - ret = MHD_queue_response (bc->con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; - } - if (NULL != bc->existing_order_id) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Have existing order, waiting for `%s' to complete\n", - bc->existing_order_id); - await_payment (bc, - GNUNET_TIME_UNIT_ZERO /* no long polling */, - bc->existing_order_id); - return MHD_YES; - } - } - GNUNET_CONTAINER_DLL_insert (bc_head, - bc_tail, - bc); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending connection while creating order at `%s'\n", - SH_backend_url); - MHD_suspend_connection (bc->con); - { - json_t *order; - - order = GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - &SH_annual_fee), - GNUNET_JSON_pack_string ("summary", - "annual fee for sync service"), - GNUNET_JSON_pack_string ("fulfillment_url", - SH_fulfillment_url)); - bc->po = TALER_MERCHANT_post_private_orders_create (SH_ctx, - SH_backend_url, - order); - json_decref (order); - } - GNUNET_assert (TALER_EC_NONE == - TALER_MERCHANT_post_private_orders_start ( - bc->po, - &proposal_cb, - bc)); - SH_trigger_curl (); - return MHD_YES; -} - - -/** - * We got some query status from the DB. Handle the error cases. - * May perform asynchronous operations by suspending the connection - * if required. - * - * @param bc connection to handle status for - * @param qs query status to handle - * @return #MHD_YES or #MHD_NO - */ -static enum MHD_Result -handle_database_error (struct BackupContext *bc, - enum SYNC_DB_QueryStatus qs) -{ - switch (qs) - { - case SYNC_DB_OLD_BACKUP_MISSING: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Update failed: no existing backup\n"); - return TALER_MHD_reply_with_error (bc->con, - MHD_HTTP_NOT_FOUND, - TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN, - NULL); - case SYNC_DB_OLD_BACKUP_MISMATCH: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Conflict detected, returning existing backup\n"); - return SH_return_backup (bc->con, - &bc->account, - MHD_HTTP_CONFLICT); - case SYNC_DB_PAYMENT_REQUIRED: - { - const char *order_id; - - order_id = MHD_lookup_connection_value (bc->con, - MHD_GET_ARGUMENT_KIND, - "paying"); - if (NULL == order_id) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment required, starting payment process\n"); - return begin_payment (bc, - GNUNET_NO); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment required, awaiting completion of `%s'\n", - order_id); - await_payment (bc, - CHECK_PAYMENT_GENERIC_TIMEOUT, - order_id); - } - return MHD_YES; - case SYNC_DB_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (bc->con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - case SYNC_DB_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (bc->con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - case SYNC_DB_NO_RESULTS: - GNUNET_assert (0); - return MHD_NO; - /* intentional fall-through! */ - case SYNC_DB_ONE_RESULT: - GNUNET_assert (0); - return MHD_NO; - } - GNUNET_break (0); - return MHD_NO; -} - - -enum MHD_Result -SH_backup_post (struct MHD_Connection *connection, - void **con_cls, - const struct SYNC_AccountPublicKeyP *account, - const char *upload_data, - size_t *upload_data_size) -{ - struct BackupContext *bc; - - bc = *con_cls; - if (NULL == bc) - { - /* first call, setup internals */ - bc = GNUNET_new (struct BackupContext); - bc->hc.cc = &cleanup_ctx; - bc->con = connection; - bc->account = *account; - { - const char *fresh; - - fresh = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "fresh"); - if (NULL != fresh) - bc->force_fresh_order = true; - } - *con_cls = bc; - - /* now setup 'bc' */ - { - const char *lens; - unsigned long len; - - lens = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_CONTENT_LENGTH); - if ( (NULL == lens) || - (1 != - sscanf (lens, - "%lu", - &len)) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - (NULL == lens) - ? TALER_EC_SYNC_MALFORMED_CONTENT_LENGTH - : TALER_EC_SYNC_MISSING_CONTENT_LENGTH, - lens); - } - if (len / 1024 / 1024 >= SH_upload_limit_mb) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_PAYLOAD_TOO_LARGE, - TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH, - NULL); - } - bc->upload = GNUNET_malloc_large (len); - if (NULL == bc->upload) - { - GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, - "malloc"); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_PAYLOAD_TOO_LARGE, - TALER_EC_SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH, - NULL); - } - bc->upload_size = (size_t) len; - } - { - const char *im; - - im = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_MATCH); - if (NULL != im) - { - if ( (2 >= strlen (im)) || - ('"' != im[0]) || - ('"' != im[strlen (im) - 1]) || - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (im + 1, - strlen (im) - 2, - &bc->old_backup_hash, - sizeof (bc->old_backup_hash))) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_SYNC_BAD_IF_MATCH, - NULL); - } - } - } - { - const char *sig_s; - - sig_s = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - "Sync-Signature"); - if ( (NULL == sig_s) || - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (sig_s, - strlen (sig_s), - &bc->account_sig, - sizeof (bc->account_sig))) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_SYNC_BAD_SYNC_SIGNATURE, - NULL); - } - } - { - const char *etag; - - etag = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if ( (NULL == etag) || - (2 >= strlen (etag)) || - ('"' != etag[0]) || - ('"' != etag[strlen (etag) - 1]) || - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (etag + 1, - strlen (etag) - 2, - &bc->new_backup_hash, - sizeof (bc->new_backup_hash))) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_SYNC_BAD_IF_NONE_MATCH, - NULL); - } - } - /* validate signature */ - { - struct SYNC_UploadSignaturePS usp = { - .purpose.size = htonl (sizeof (usp)), - .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD), - .old_backup_hash = bc->old_backup_hash, - .new_backup_hash = bc->new_backup_hash - }; - - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD, - &usp, - &bc->account_sig.eddsa_sig, - &account->eddsa_pub)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_SYNC_INVALID_SIGNATURE, - NULL); - } - } - /* get ready to hash (done here as we may go async for payments next) */ - bc->hash_ctx = GNUNET_CRYPTO_hash_context_start (); - - /* Check database to see if the transaction is permissible */ - { - struct GNUNET_HashCode hc; - enum SYNC_DB_QueryStatus qs; - - qs = SYNCDB_lookup_account_TR ( - account, - &hc); - if (qs < 0) - return handle_database_error (bc, - qs); - if (SYNC_DB_NO_RESULTS == qs) - memset (&hc, 0, sizeof (hc)); - if (0 == GNUNET_memcmp (&hc, - &bc->new_backup_hash)) - { - /* Refuse upload: we already have that backup! */ - struct MHD_Response *resp; - enum MHD_Result ret; - - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (resp, - false); - ret = MHD_queue_response (connection, - MHD_HTTP_NOT_MODIFIED, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; - } - if (0 != GNUNET_memcmp (&hc, - &bc->old_backup_hash)) - { - /* Refuse upload: if-none-match failed! */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Conflict detected, returning existing backup\n"); - return SH_return_backup (connection, - account, - MHD_HTTP_CONFLICT); - } - } - /* check if the client insists on paying */ - { - const char *order_req; - - order_req = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "pay"); - if (NULL != order_req) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment requested, starting payment process\n"); - return begin_payment (bc, - GNUNET_YES); - } - } - /* ready to begin! */ - return MHD_YES; - } - /* handle upload */ - if (0 != *upload_data_size) - { - /* check MHD invariant */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing %u bytes of upload data\n", - (unsigned int) *upload_data_size); - GNUNET_assert (bc->upload_off + *upload_data_size <= bc->upload_size); - memcpy (&bc->upload[bc->upload_off], - upload_data, - *upload_data_size); - bc->upload_off += *upload_data_size; - GNUNET_CRYPTO_hash_context_read (bc->hash_ctx, - upload_data, - *upload_data_size); - *upload_data_size = 0; - return MHD_YES; - } - else if ( (0 == bc->upload_off) && - (0 != bc->upload_size) && - (NULL == bc->resp) ) - { - /* wait for upload */ - return MHD_YES; - } - if (NULL != bc->resp) - { - enum MHD_Result ret; - - /* We generated a response asynchronously, queue that */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning asynchronously generated response with HTTP status %u\n", - bc->response_code); - ret = MHD_queue_response (connection, - bc->response_code, - bc->resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (bc->resp); - bc->resp = NULL; - return ret; - } - - /* finished with upload, check hash */ - { - struct GNUNET_HashCode our_hash; - - GNUNET_CRYPTO_hash_context_finish (bc->hash_ctx, - &our_hash); - bc->hash_ctx = NULL; - if (0 != GNUNET_memcmp (&our_hash, - &bc->new_backup_hash)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_SYNC_INVALID_UPLOAD, - NULL); - } - } - - /* store backup to database */ - { - enum SYNC_DB_QueryStatus qs; - - if (GNUNET_YES == GNUNET_is_zero (&bc->old_backup_hash)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Uploading first backup to account\n"); - qs = SYNCDB_store_backup_TR ( - account, - &bc->account_sig, - &bc->new_backup_hash, - bc->upload_size, - bc->upload); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Uploading existing backup of account\n"); - qs = SYNCDB_update_backup_TR ( - account, - &bc->old_backup_hash, - &bc->account_sig, - &bc->new_backup_hash, - bc->upload_size, - bc->upload); - } - if (qs < 0) - return handle_database_error (bc, - qs); - if (0 == qs) - { - /* database says nothing actually changed, 304 (could - theoretically happen if another equivalent upload succeeded - since we last checked!) */ - struct MHD_Response *resp; - enum MHD_Result ret; - - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (resp, - false); - ret = MHD_queue_response (connection, - MHD_HTTP_NOT_MODIFIED, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; - } - } - - /* generate main (204) standard success reply */ - { - struct MHD_Response *resp; - enum MHD_Result ret; - - resp = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (resp, - false); - ret = MHD_queue_response (connection, - MHD_HTTP_NO_CONTENT, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; - } -} diff --git a/src/sync/sync-httpd_config.c b/src/sync/sync-httpd_config.c @@ -1,68 +0,0 @@ -/* - This file is part of Sync - Copyright (C) 2020,2024 Taler Systems SA - - Sync is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - Sync is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - Sync; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync/sync-httpd_config.c - * @brief headers for /config handler - * @author Christian Grothoff - */ -#include "platform.h" -#include "sync-httpd_config.h" -#include <taler/taler_json_lib.h> - - -/* - * Protocol version history: - * - * 0: original design - * 1: adds ?fresh=y to POST backup operation to force fresh contract - * to be created - */ - -enum MHD_Result -SH_handler_config (struct SH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size) -{ - static struct MHD_Response *response; - - (void) connection_cls; - (void) upload_data; - (void) upload_data_size; - if (NULL == response) - { - response = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_string ("name", - "sync"), - GNUNET_JSON_pack_string ("implementation", - "urn:net:taler:specs:sync:c-reference"), - GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes", - SH_upload_limit_mb), - TALER_JSON_pack_amount ("liability_limit", - &SH_insurance), - TALER_JSON_pack_amount ("annual_fee", - &SH_annual_fee), - GNUNET_JSON_pack_string ("version", - "2:3:0")); - } - return MHD_queue_response (connection, - MHD_HTTP_OK, - response); -} - - -/* end of sync-httpd_config.c */ diff --git a/src/sync/sync-httpd_config.h b/src/sync/sync-httpd_config.h @@ -1,45 +0,0 @@ -/* - This file is part of Sync - Copyright (C) 2020 Taler Systems SA - - Sync is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - Sync is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - Sync; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync/sync-httpd_config.h - * @brief headers for /config handler - * @author Christian Grothoff - */ -#ifndef SYNC_HTTPD_CONFIG_H -#define SYNC_HTTPD_CONFIG_H -#include <microhttpd.h> -#include "sync-httpd.h" - -/** - * Manages a /config call. - * - * @param rh context of the handler - * @param connection the MHD connection to handle - * @param[in,out] connection_cls the connection's closure (can be updated) - * @param upload_data upload data - * @param[in,out] upload_data_size number of bytes (left) in @a upload_data - * @return MHD result code - */ -enum MHD_Result -SH_handler_config (struct SH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size); - -#endif - -/* end of sync-httpd_config.h */ diff --git a/src/sync/sync-httpd_mhd.c b/src/sync/sync-httpd_mhd.c @@ -1,66 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014, 2015, 2016 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file sync-httpd_mhd.c - * @brief helpers for MHD interaction; these are TALER_EXCHANGE_handler_ functions - * that generate simple MHD replies that do not require any real operations - * to be performed (error handling, static pages, etc.) - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#include "platform.h" -#include <jansson.h> -#include "sync-httpd_mhd.h" - - -enum MHD_Result -SH_MHD_handler_static_response (struct SH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size) -{ - (void) connection_cls; - (void) upload_data; - (void) upload_data_size; - if (0 == rh->data_size) - rh->data_size = strlen ((const char *) rh->data); - return TALER_MHD_reply_static (connection, - rh->response_code, - rh->mime_type, - (void *) rh->data, - rh->data_size); -} - - -enum MHD_Result -SH_MHD_handler_agpl_redirect (struct SH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size) -{ - (void) rh; - (void) connection_cls; - (void) upload_data; - (void) upload_data_size; - return TALER_MHD_reply_agpl (connection, - "http://www.git.taler.net/sync.git"); -} - - -/* end of sync-httpd_mhd.c */ diff --git a/src/sync/sync-httpd_mhd.h b/src/sync/sync-httpd_mhd.h @@ -1,69 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2014, 2015 GNUnet e.V. and INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ - -/** - * @file sync-httpd_mhd.h - * @brief helpers for MHD interaction, used to generate simple responses - * @author Florian Dold - * @author Benedikt Mueller - * @author Christian Grothoff - */ -#ifndef sync_HTTPD_MHD_H -#define sync_HTTPD_MHD_H -#include <gnunet/gnunet_util_lib.h> -#include <microhttpd.h> -#include "sync-httpd.h" - - -/** - * Function to call to handle the request by sending - * back static data from the @a rh. - * - * @param rh context of the handler - * @param connection the MHD connection to handle - * @param[in,out] connection_cls the connection's closure (can be updated) - * @param upload_data upload data - * @param[in,out] upload_data_size number of bytes (left) in @a upload_data - * @return MHD result code - */ -enum MHD_Result -SH_MHD_handler_static_response (struct SH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size); - - -/** - * Function to call to handle the request by sending - * back a redirect to the AGPL source code. - * - * @param rh context of the handler - * @param connection the MHD connection to handle - * @param[in,out] connection_cls the connection's closure (can be updated) - * @param upload_data upload data - * @param[in,out] upload_data_size number of bytes (left) in @a upload_data - * @return MHD result code - */ -enum MHD_Result -SH_MHD_handler_agpl_redirect (struct SH_RequestHandler *rh, - struct MHD_Connection *connection, - void **connection_cls, - const char *upload_data, - size_t *upload_data_size); - - -#endif diff --git a/src/syncdb/Makefile.sql b/src/syncdb/Makefile.sql @@ -1,4 +1,6 @@ -procedures.sql: procedures.sql.in syncdb_*_TR.sql +# Note: syncdb_*.sql covers the per-operation _TR files as well as the +# shared helpers (syncdb_object_refcounts.sql, syncdb_account_check.sql). +procedures.sql: procedures.sql.in syncdb_*.sql gcc -E -P -undef - < procedures.sql.in 2>/dev/null | sed -e "s/--.*//" | awk 'NF' - >$@ CLEANFILES = \ diff --git a/src/syncdb/meson.build b/src/syncdb/meson.build @@ -22,13 +22,18 @@ libsyncdb_SOURCES = [ 'syncdb_create_tables.c', 'syncdb_preflight.c', 'syncdb_gc.c', - 'syncdb_store_payment_TR.c', - 'syncdb_lookup_pending_payments_by_account_TR.c', - 'syncdb_store_backup_TR.c', - 'syncdb_update_backup_TR.c', + 'syncdb_append_block_TR.c', + 'syncdb_delete_block_TR.c', + 'syncdb_update_block_TR.c', 'syncdb_lookup_account_TR.c', - 'syncdb_lookup_backup_TR.c', + 'syncdb_lookup_block_range_TR.c', + 'syncdb_lookup_block_TR.c', 'syncdb_increment_lifetime_TR.c', + 'syncdb_lookup_pending_payments_by_account.c', + 'syncdb_store_payment_TR.c', + 'syncdb_store_object_TR.c', + 'syncdb_lookup_object.c', + 'syncdb_lookup_object_uid.c', ] libsyncdb = library( 'syncdb', diff --git a/src/syncdb/procedures.sql.in b/src/syncdb/procedures.sql.in @@ -18,9 +18,16 @@ BEGIN; SET search_path TO sync; +#include "syncdb_object_refcounts.sql" +#include "syncdb_account_check.sql" #include "syncdb_increment_lifetime_TR.sql" -#include "syncdb_lookup_account_TR.sql" -#include "syncdb_store_backup_TR.sql" -#include "syncdb_update_backup_TR.sql" +#include "syncdb_store_payment_TR.sql" +#include "syncdb_store_object_TR.sql" +#include "syncdb_append_block_TR.sql" +#include "syncdb_delete_block_TR.sql" +#include "syncdb_update_block_TR.sql" +#include "syncdb_lookup_block_range_TR.sql" +#include "syncdb_lookup_block_TR.sql" +#include "syncdb_gc.sql" COMMIT; diff --git a/src/syncdb/sync-0001.sql b/src/syncdb/sync-0001.sql @@ -33,6 +33,35 @@ CREATE TYPE taler_amount COMMENT ON TYPE taler_amount IS 'Stores an amount, fraction is in units of 1/100000000 of the base value'; +CREATE TYPE sync_do_lookup_block_range_return_type + AS + (out_account_missing BOOLEAN + ,out_block_missing BOOLEAN + ,out_backup_empty BOOLEAN + ,nonce BYTEA + ,block_hash BYTEA + ,prev_nonce BYTEA + ,next_nonce BYTEA + ,data_blob BYTEA + ,upload_sig BYTEA + ,old_hash BYTEA + ,refs_hash BYTEA + ); + +CREATE TYPE sync_do_lookup_block_return_type + AS + (out_account_missing BOOLEAN + ,out_block_missing BOOLEAN + ,out_is_first BOOLEAN + ,out_is_last BOOLEAN + ,nonce BYTEA + ,block_hash BYTEA + ,prev_nonce BYTEA + ,next_nonce BYTEA + ,data_size INT8 + ,refs_hash BYTEA + ); + CREATE TABLE IF NOT EXISTS accounts (account_pub BYTEA PRIMARY KEY CHECK (length(account_pub)=32) ,expiration_date INT8 NOT NULL); @@ -40,26 +69,43 @@ CREATE TABLE IF NOT EXISTS accounts CREATE INDEX IF NOT EXISTS accounts_expire ON accounts (expiration_date); - CREATE TABLE IF NOT EXISTS payments - (account_pub BYTEA CHECK (length(account_pub)=32) + (account_pub BYTEA REFERENCES accounts (account_pub) ON DELETE CASCADE ,order_id TEXT PRIMARY KEY - ,token BYTEA CHECK (length(token)=16) - ,timestamp INT8 NOT NULL + ,claim_token BYTEA CHECK (length(claim_token)=16) + ,order_timestamp INT8 NOT NULL ,amount taler_amount NOT NULL ,paid BOOLEAN NOT NULL DEFAULT FALSE); CREATE INDEX IF NOT EXISTS payments_timestamp ON - payments (paid,timestamp); + payments (paid, order_timestamp); + +CREATE TABLE IF NOT EXISTS blocks + (nonce BYTEA PRIMARY KEY CHECK (length(nonce)=24) + ,account_pub BYTEA REFERENCES accounts (account_pub) ON DELETE CASCADE + ,block_hash BYTEA NOT NULL CHECK (length(block_hash)=64) + ,prev_nonce BYTEA REFERENCES blocks (nonce) ON DELETE RESTRICT + ,next_nonce BYTEA REFERENCES blocks (nonce) ON DELETE RESTRICT + ,data_blob BYTEA NOT NULL + ,upload_sig BYTEA NOT NULL DEFAULT '' + ,old_hash BYTEA NOT NULL DEFAULT '' + ,refs_hash BYTEA NOT NULL DEFAULT ''); +ALTER TABLE IF EXISTS accounts + ADD COLUMN IF NOT EXISTS first_block BYTEA REFERENCES blocks (nonce) ON DELETE CASCADE, + ADD COLUMN IF NOT EXISTS last_block BYTEA REFERENCES blocks (nonce) ON DELETE CASCADE; -CREATE TABLE IF NOT EXISTS backups - (account_pub BYTEA PRIMARY KEY REFERENCES accounts (account_pub) ON DELETE CASCADE - ,account_sig BYTEA NOT NULL CHECK (length(account_sig)=64) - ,prev_hash BYTEA NOT NULL CHECK (length(prev_hash)=64) - ,backup_hash BYTEA NOT NULL CHECK (length(backup_hash)=64) - ,data BYTEA NOT NULL); +CREATE TABLE IF NOT EXISTS objects + (uid BYTEA PRIMARY KEY CHECK (length(uid)=64) + ,account_pub BYTEA REFERENCES accounts (account_pub) ON DELETE CASCADE + ,object_hash BYTEA NOT NULL CHECK (length(object_hash)=64) + ,refcount INT8 NOT NULL DEFAULT 0 CHECK (refcount >= 0) + ,expiration_date INT8 + ,data_blob BYTEA NOT NULL); +CREATE INDEX IF NOT EXISTS objects_expire ON + objects (expiration_date) + WHERE refcount = 0; -- Complete transaction COMMIT; diff --git a/src/syncdb/sync-dbinit.c b/src/syncdb/sync-dbinit.c @@ -54,9 +54,11 @@ run (void *cls, const char *cfgfile, const struct GNUNET_CONFIGURATION_Handle *cfg) { + (void) cls; + (void) args; + (void) cfgfile; if (GNUNET_OK != - SYNCDB_init (cfg, - true)) + SYNCDB_init_admin (cfg)) { fprintf (stderr, "Failed to initialize database.\n"); @@ -82,15 +84,21 @@ run (void *cls, { struct GNUNET_TIME_Absolute now; struct GNUNET_TIME_Absolute ancient; + struct GNUNET_TIME_Absolute recent; now = GNUNET_TIME_absolute_get (); ancient = GNUNET_TIME_absolute_subtract (now, GNUNET_TIME_relative_multiply ( GNUNET_TIME_UNIT_YEARS, 6)); - if (0 > + recent = GNUNET_TIME_absolute_subtract (now, + GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_DAYS, + 1)); + if (GNUNET_OK != SYNCDB_gc (now, - ancient)) + ancient, + recent)) { fprintf (stderr, "Garbage collection failed!\n"); diff --git a/src/syncdb/sync_db_plugin.c b/src/syncdb/sync_db_plugin.c @@ -1,87 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2019 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file syncdb/sync_db_plugin.c - * @brief Logic to load database plugin - * @author Christian Grothoff - * @author Sree Harsha Totakura <sreeharsha@totakura.in> - */ -#include "platform.h" -#include "sync/sync_util.h" -#include "sync/sync_database_lib.h" -#include <ltdl.h> - - -struct SYNC_DatabasePlugin * -SYNC_DB_plugin_load (const struct GNUNET_CONFIGURATION_Handle *cfg, - bool skip_preflight) -{ - char *plugin_name; - char *lib_name; - struct SYNC_DatabasePlugin *plugin; - - if (GNUNET_SYSERR == - GNUNET_CONFIGURATION_get_value_string (cfg, - "sync", - "db", - &plugin_name)) - { - GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, - "sync", - "db"); - return NULL; - } - (void) GNUNET_asprintf (&lib_name, - "libsync_plugin_db_%s", - plugin_name); - GNUNET_free (plugin_name); - plugin = GNUNET_PLUGIN_load (SYNC_project_data (), - lib_name, - (void *) cfg); - if (NULL == plugin) - { - GNUNET_free (lib_name); - return NULL; - } - plugin->library_name = lib_name; - if ( (! skip_preflight) && - (GNUNET_OK != - plugin->preflight (plugin->cls)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database not ready. Try running sync-dbinit!\n"); - SYNC_DB_plugin_unload (plugin); - return NULL; - } - return plugin; -} - - -void -SYNC_DB_plugin_unload (struct SYNC_DatabasePlugin *plugin) -{ - char *lib_name; - - if (NULL == plugin) - return; - lib_name = plugin->library_name; - GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name, - plugin)); - GNUNET_free (lib_name); -} - - -/* end of sync_db_plugin.c */ diff --git a/src/syncdb/syncdb_account_check.sql b/src/syncdb/syncdb_account_check.sql @@ -0,0 +1,55 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +-- +-- Helper to check that an account exists and has a valid (non-expired) payment. +-- Returns TRUE in out_account_missing if the account is missing or expired. +-- + +DROP FUNCTION IF EXISTS sync_check_account_payment; +CREATE FUNCTION sync_check_account_payment( + IN in_account_pub BYTEA) +RETURNS TABLE ( + out_account_missing BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_expiration_date INT8; +BEGIN + out_account_missing := FALSE; + + SELECT expiration_date + INTO my_expiration_date + FROM accounts + WHERE account_pub = in_account_pub; + + IF NOT FOUND + THEN + out_account_missing := TRUE; + RETURN NEXT; + RETURN; + END IF; + + IF my_expiration_date <= (EXTRACT(EPOCH FROM NOW()) * 1000000)::INT8 + THEN + out_account_missing := TRUE; + RETURN NEXT; + RETURN; + END IF; + + RETURN NEXT; + RETURN; +END $$; diff --git a/src/syncdb/syncdb_append_block_TR.c b/src/syncdb/syncdb_append_block_TR.c @@ -0,0 +1,133 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_append_block_TR.c + * @brief append block at the end of DLL + * @author Iván Ávalos + */ +#include "syncdb_pg.h" + +enum SYNC_DB_QueryStatus +SYNCDB_append_block_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *block_nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct GNUNET_HashCode *data_hash, + size_t data_size, + const void *data, + unsigned int refs_length, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *refs_hash, + const struct SYNC_AccountSignatureP *prev_upload_sig, + const struct GNUNET_HashCode *prev_hash, + const struct SYNC_BlockNonce *prev_prev_nonce) +{ + bool account_missing; + bool outdated_append; + bool negative_refcount; + bool refs_missing; + bool inserted; + bool refs_updated; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (block_nonce), + (NULL != prev_nonce) + ? GNUNET_PQ_query_param_auto_from_type (prev_nonce) + : GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_auto_from_type (data_hash), + GNUNET_PQ_query_param_fixed_size (data, + data_size), + GNUNET_PQ_query_param_array_auto_from_type (refs_length, + object_uids, + pg->conn), + /* FIXME: add int16 support to GNUnet */ + GNUNET_PQ_query_param_array_uint16 (refs_length, + (const uint16_t *) object_incs, + pg->conn), + GNUNET_PQ_query_param_auto_from_type (upload_sig), + GNUNET_PQ_query_param_auto_from_type (refs_hash), + (NULL != prev_upload_sig) + ? GNUNET_PQ_query_param_auto_from_type (prev_upload_sig) + : GNUNET_PQ_query_param_null (), + (NULL != prev_hash) + ? GNUNET_PQ_query_param_auto_from_type (prev_hash) + : GNUNET_PQ_query_param_null (), + (NULL != prev_prev_nonce) + ? GNUNET_PQ_query_param_auto_from_type (prev_prev_nonce) + : GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_end, + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_account_missing", + &account_missing), + GNUNET_PQ_result_spec_bool ("out_outdated_append", + &outdated_append), + GNUNET_PQ_result_spec_bool ("out_negative_refcount", + &negative_refcount), + GNUNET_PQ_result_spec_bool ("out_refs_missing", + &refs_missing), + GNUNET_PQ_result_spec_bool ("out_inserted", + &inserted), + GNUNET_PQ_result_spec_bool ("out_refs_updated", + &refs_updated), + GNUNET_PQ_result_spec_end, + }; + enum GNUNET_DB_QueryStatus qs; + + SYNCDB_preflight (); + PREPARE ("do_append_block", + "SELECT" + " out_account_missing" + ",out_outdated_append" + ",out_negative_refcount" + ",out_refs_missing" + ",out_inserted" + ",out_refs_updated" + " FROM sync_do_append_block" + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_append_block", + params, + rs); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (inserted && refs_updated) + return SYNC_DB_ONE_RESULT; + if (account_missing) + return SYNC_DB_PAYMENT_REQUIRED; + if (outdated_append) + return SYNC_DB_OUTDATED_WRITE; + if (negative_refcount || refs_missing) + return SYNC_DB_INCONSISTENT_BACKUP; + /* Nothing else failed, so the INSERT hit the primary key */ + return SYNC_DB_OUTDATED_WRITE; + default: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + } +} diff --git a/src/syncdb/syncdb_append_block_TR.sql b/src/syncdb/syncdb_append_block_TR.sql @@ -0,0 +1,200 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +-- The append relinks the previous tail, so the client uploads the +-- tail's new signature (in_prev_upload_sig). That signature covers the +-- tail as the caller read it outside this transaction, so the whole +-- signed context has to be re-checked here or a concurrent write could +-- leave the tail with a signature over links it no longer has: the +-- transaction re-checks the tail against in_prev_hash and +-- in_prev_prev_nonce. The signed refs_hash needs no separate check, +-- since sync_do_update_block() is the only thing that changes it and it +-- always changes the block hash along with it. +DROP FUNCTION IF EXISTS sync_do_append_block; +CREATE FUNCTION sync_do_append_block ( + IN in_account_pub BYTEA, + IN in_curr_nonce BYTEA, + IN in_prev_nonce BYTEA, + IN in_data_hash BYTEA, + IN in_data_blob BYTEA, + IN ina_object_uids BYTEA[], + IN ina_object_incs INT2[], + IN in_upload_sig BYTEA, + IN in_refs_hash BYTEA, + IN in_prev_upload_sig BYTEA, + IN in_prev_hash BYTEA, + IN in_prev_prev_nonce BYTEA, + OUT out_account_missing BOOLEAN, + OUT out_outdated_append BOOLEAN, + OUT out_negative_refcount BOOLEAN, + OUT out_refs_missing BOOLEAN, + OUT out_inserted BOOLEAN, + OUT out_refs_updated BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_last_block_nonce BYTEA; + my_prev_hash BYTEA; + my_prev_prev_nonce BYTEA; +BEGIN + out_account_missing := FALSE; + out_negative_refcount := FALSE; + out_refs_missing := FALSE; + out_outdated_append := FALSE; + out_inserted := FALSE; + out_refs_updated := FALSE; + + -- Check if account exists and has valid payment + SELECT a.out_account_missing + INTO out_account_missing + FROM sync_check_account_payment(in_account_pub) AS a; + IF out_account_missing + THEN + RETURN; + END IF; + + -- Get current tail of the DLL + SELECT last_block + INTO my_last_block_nonce + FROM accounts + WHERE account_pub = in_account_pub; + + -- Ensure that block is being appended to the end + -- (or beginning if backup is empty) + IF NOT (in_prev_nonce IS NOT DISTINCT FROM my_last_block_nonce) + THEN + out_outdated_append := TRUE; + RETURN; + END IF; + + IF in_prev_nonce IS NOT NULL + THEN + -- The tail must still look the way the relink signature was + -- verified against (see header comment). Its next_nonce needs no + -- check: it is the account's last block, so it has none, and this + -- transaction is what sets it. + SELECT block_hash + ,prev_nonce + INTO my_prev_hash + ,my_prev_prev_nonce + FROM blocks + WHERE account_pub = in_account_pub + AND nonce = in_prev_nonce; + + IF NOT FOUND + OR my_prev_hash IS DISTINCT FROM in_prev_hash + OR my_prev_prev_nonce IS DISTINCT FROM in_prev_prev_nonce + THEN + out_outdated_append := TRUE; + RETURN; + END IF; + END IF; + + -- Check referenced objects exist and no increment underflows, + -- before we touch anything + SELECT c.out_negative_refcount + ,c.out_refs_missing + INTO out_negative_refcount + ,out_refs_missing + FROM sync_check_object_refcounts_pre(in_account_pub, + ina_object_uids, + ina_object_incs) AS c; + IF out_negative_refcount OR out_refs_missing + THEN + RETURN; + END IF; + + -- Try to append the new block + INSERT INTO blocks + (nonce + ,account_pub + ,block_hash + ,prev_nonce + ,data_blob + ,upload_sig + ,old_hash + ,refs_hash) + VALUES + (in_curr_nonce + ,in_account_pub + ,in_data_hash + ,in_prev_nonce + ,in_data_blob + ,in_upload_sig + ,decode(repeat('00', 64), 'hex') + ,in_refs_hash) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + out_inserted := FALSE; + RETURN; + ELSE + out_inserted := TRUE; + END IF; + + -- Update the reference in the previous block; it was checked to be + -- this account's tail above. Store the tail's relink signature with + -- it; a relink never changes the data, so old_hash is set to the + -- current block hash. + IF in_prev_nonce IS NOT NULL + THEN + UPDATE blocks + SET next_nonce = in_curr_nonce + ,upload_sig = in_prev_upload_sig + ,old_hash = block_hash + WHERE account_pub = in_account_pub + AND nonce = in_prev_nonce; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'previous block % vanished for account %', + in_prev_nonce, in_account_pub; + END IF; + END IF; + + -- Update reference to first and last block in the backup + UPDATE accounts + SET last_block = in_curr_nonce + ,first_block = CASE + -- if first block in the backup, update reference + WHEN in_prev_nonce IS NULL + THEN in_curr_nonce + -- otherwise, keep reference intact + ELSE first_block + END + WHERE account_pub = in_account_pub; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'account % vanished', in_account_pub; + END IF; + + -- Update object refcounts and delete unreferenced objects; the + -- pre-check above means this can only fail on a concurrent change + SELECT u.out_negative_refcount + ,u.out_refs_updated + INTO out_negative_refcount + ,out_refs_updated + FROM sync_update_object_refcounts(in_account_pub, + ina_object_uids, + ina_object_incs) AS u; + IF NOT out_refs_updated + THEN + RAISE EXCEPTION 'object refcounts changed concurrently for account %', + in_account_pub; + END IF; +END $$; diff --git a/src/syncdb/syncdb_create_tables.c b/src/syncdb/syncdb_create_tables.c @@ -24,24 +24,27 @@ enum GNUNET_GenericReturnValue SYNCDB_create_tables (void) { - struct GNUNET_PQ_Context *conn; - struct GNUNET_PQ_ExecuteStatement es[] = { - GNUNET_PQ_make_execute ("SET search_path TO sync;"), - GNUNET_PQ_EXECUTE_STATEMENT_END - }; - enum GNUNET_GenericReturnValue ret; - - conn = GNUNET_PQ_connect_with_cfg (pg->cfg, - "syncdb-postgres", - "sync-", - es, - NULL); - if (NULL == conn) + if (GNUNET_SYSERR == + GNUNET_PQ_load_versioning (pg->conn)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_PQ_run_sql (pg->conn, + "sync-")) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_PQ_exec_sql (pg->conn, + "procedures")) + { + GNUNET_break (0); return GNUNET_SYSERR; - ret = GNUNET_PQ_exec_sql (conn, - "procedures"); - GNUNET_PQ_disconnect (conn); - return ret; + } + return GNUNET_OK; } diff --git a/src/syncdb/syncdb_delete_block_TR.c b/src/syncdb/syncdb_delete_block_TR.c @@ -0,0 +1,150 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_delete_block_TR.c + * @brief delete a block in the DLL + * @author Iván Ávalos + */ +#include "syncdb_pg.h" + + +enum SYNC_DB_QueryStatus +SYNCDB_delete_block_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *block_nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *block_hash, + unsigned int refs_length, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_AccountSignatureP *prev_upload_sig, + const struct GNUNET_HashCode *prev_hash, + const struct SYNC_BlockNonce *prev_prev_nonce, + const struct SYNC_AccountSignatureP *next_upload_sig, + const struct GNUNET_HashCode *next_hash, + const struct SYNC_BlockNonce *next_next_nonce) +{ + bool account_missing; + bool block_missing; + bool outdated_write; + bool refs_missing; + bool negative_refcount; + bool deleted; + bool refs_updated; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (block_nonce), + (NULL != prev_nonce) + ? GNUNET_PQ_query_param_auto_from_type (prev_nonce) + : GNUNET_PQ_query_param_null (), + (NULL != next_nonce) + ? GNUNET_PQ_query_param_auto_from_type (next_nonce) + : GNUNET_PQ_query_param_null (), + (NULL != block_hash) + ? GNUNET_PQ_query_param_auto_from_type (block_hash) + : GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_array_auto_from_type (refs_length, + object_uids, + pg->conn), + GNUNET_PQ_query_param_array_uint16 (refs_length, + (const uint16_t *) object_incs, + pg->conn), + (NULL != prev_upload_sig) + ? GNUNET_PQ_query_param_auto_from_type (prev_upload_sig) + : GNUNET_PQ_query_param_null (), + (NULL != prev_hash) + ? GNUNET_PQ_query_param_auto_from_type (prev_hash) + : GNUNET_PQ_query_param_null (), + (NULL != prev_prev_nonce) + ? GNUNET_PQ_query_param_auto_from_type (prev_prev_nonce) + : GNUNET_PQ_query_param_null (), + (NULL != next_upload_sig) + ? GNUNET_PQ_query_param_auto_from_type (next_upload_sig) + : GNUNET_PQ_query_param_null (), + (NULL != next_hash) + ? GNUNET_PQ_query_param_auto_from_type (next_hash) + : GNUNET_PQ_query_param_null (), + (NULL != next_next_nonce) + ? GNUNET_PQ_query_param_auto_from_type (next_next_nonce) + : GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_end, + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_account_missing", + &account_missing), + GNUNET_PQ_result_spec_bool ("out_block_missing", + &block_missing), + GNUNET_PQ_result_spec_bool ("out_outdated_write", + &outdated_write), + GNUNET_PQ_result_spec_bool ("out_refs_missing", + &refs_missing), + GNUNET_PQ_result_spec_bool ("out_negative_refcount", + &negative_refcount), + GNUNET_PQ_result_spec_bool ("out_deleted", + &deleted), + GNUNET_PQ_result_spec_bool ("out_refs_updated", + &refs_updated), + GNUNET_PQ_result_spec_end, + }; + + enum GNUNET_DB_QueryStatus qs; + + SYNCDB_preflight (); + PREPARE ("do_delete_block", + "SELECT" + " out_account_missing" + ",out_block_missing" + ",out_outdated_write" + ",out_refs_missing" + ",out_negative_refcount" + ",out_deleted" + ",out_refs_updated" + " FROM sync_do_delete_block" + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_delete_block", + params, + rs); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (deleted) + return SYNC_DB_ONE_RESULT; + if (account_missing) + return SYNC_DB_PAYMENT_REQUIRED; + if (block_missing) + return SYNC_DB_TARGET_MISSING; + if (outdated_write) + return SYNC_DB_OUTDATED_WRITE; + if (refs_missing || negative_refcount) + return SYNC_DB_INCONSISTENT_BACKUP; + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + default: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + } +} diff --git a/src/syncdb/syncdb_delete_block_TR.sql b/src/syncdb/syncdb_delete_block_TR.sql @@ -0,0 +1,235 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +-- The deletion relinks both neighbours, so the client uploads their new +-- signatures (in_prev_upload_sig, in_next_upload_sig). Those signatures +-- cover the neighbours as the caller read them outside this +-- transaction, so the whole signed context has to be re-checked here or +-- a concurrent write could leave a neighbour with a signature over links +-- it no longer has: the transaction re-checks them against in_prev_hash +-- and in_prev_prev_nonce, resp. in_next_hash and in_next_next_nonce. +-- The signed refs_hash needs no separate check, since +-- sync_do_update_block() is the only thing that changes it and it always +-- changes the block hash along with it. +DROP FUNCTION IF EXISTS sync_do_delete_block; +CREATE FUNCTION sync_do_delete_block ( + IN in_account_pub BYTEA, + IN in_block_nonce BYTEA, + IN in_prev_nonce BYTEA, + IN in_next_nonce BYTEA, + IN in_block_hash BYTEA, + IN ina_object_uids BYTEA[], + IN ina_object_incs INT2[], + IN in_prev_upload_sig BYTEA, + IN in_prev_hash BYTEA, + IN in_prev_prev_nonce BYTEA, + IN in_next_upload_sig BYTEA, + IN in_next_hash BYTEA, + IN in_next_next_nonce BYTEA, + OUT out_account_missing BOOLEAN, + OUT out_block_missing BOOLEAN, + OUT out_outdated_write BOOLEAN, + OUT out_refs_missing BOOLEAN, + OUT out_negative_refcount BOOLEAN, + OUT out_deleted BOOLEAN, + OUT out_refs_updated BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_prev_nonce BYTEA; + my_next_nonce BYTEA; + my_block_hash BYTEA; + my_n_hash BYTEA; + my_n_link BYTEA; +BEGIN + out_account_missing := FALSE; + out_block_missing := FALSE; + out_outdated_write := FALSE; + out_refs_missing := FALSE; + out_negative_refcount := FALSE; + out_deleted := FALSE; + out_refs_updated := FALSE; + + -- Check if account exists and has valid payment + SELECT a.out_account_missing + INTO out_account_missing + FROM sync_check_account_payment(in_account_pub) AS a; + IF out_account_missing + THEN + RETURN; + END IF; + + SELECT prev_nonce + ,next_nonce + ,block_hash + INTO my_prev_nonce + ,my_next_nonce + ,my_block_hash + FROM blocks + WHERE account_pub = in_account_pub + AND nonce = in_block_nonce; + + IF NOT FOUND + THEN + out_block_missing := TRUE; + RETURN; + END IF; + + -- Does existing hash matches expected old hash? + -- Does previous and next nonces match expectations? + IF my_block_hash IS DISTINCT FROM in_block_hash OR + my_prev_nonce IS DISTINCT FROM in_prev_nonce OR + my_next_nonce IS DISTINCT FROM in_next_nonce + THEN + out_outdated_write := TRUE; + RETURN; + END IF; + + -- The neighbours must still look the way the relink signatures were + -- verified against (see header comment). Only the link this + -- transaction does not rewrite is checked; the one it does rewrite is + -- the block being deleted, which was checked above. + IF in_prev_nonce IS NOT NULL + THEN + SELECT block_hash + ,prev_nonce + INTO my_n_hash + ,my_n_link + FROM blocks + WHERE account_pub = in_account_pub + AND nonce = in_prev_nonce; + + IF NOT FOUND + OR my_n_hash IS DISTINCT FROM in_prev_hash + OR my_n_link IS DISTINCT FROM in_prev_prev_nonce + THEN + out_outdated_write := TRUE; + RETURN; + END IF; + END IF; + + IF in_next_nonce IS NOT NULL + THEN + SELECT block_hash + ,next_nonce + INTO my_n_hash + ,my_n_link + FROM blocks + WHERE account_pub = in_account_pub + AND nonce = in_next_nonce; + + IF NOT FOUND + OR my_n_hash IS DISTINCT FROM in_next_hash + OR my_n_link IS DISTINCT FROM in_next_next_nonce + THEN + out_outdated_write := TRUE; + RETURN; + END IF; + END IF; + + -- Check referenced objects exist and no increment underflows, + -- before we touch anything + SELECT c.out_negative_refcount + ,c.out_refs_missing + INTO out_negative_refcount + ,out_refs_missing + FROM sync_check_object_refcounts_pre(in_account_pub, + ina_object_uids, + ina_object_incs) AS c; + IF out_negative_refcount OR out_refs_missing + THEN + RETURN; + END IF; + + -- Update previous block's next reference (if necessary); the + -- neighbours come from the block we are deleting, so they must exist. + -- Store the relink signature with the neighbour; a relink never + -- changes the data, so old_hash is set to the current block hash. + IF my_prev_nonce IS NOT NULL + THEN + UPDATE blocks + SET next_nonce = my_next_nonce + ,upload_sig = in_prev_upload_sig + ,old_hash = block_hash + WHERE account_pub = in_account_pub + AND nonce = my_prev_nonce; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'previous block % vanished for account %', + my_prev_nonce, in_account_pub; + END IF; + END IF; + + -- Update next block's previous reference (if necessary) + IF my_next_nonce IS NOT NULL + THEN + UPDATE blocks + SET prev_nonce = my_prev_nonce + ,upload_sig = in_next_upload_sig + ,old_hash = block_hash + WHERE account_pub = in_account_pub + AND nonce = my_next_nonce; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'next block % vanished for account %', + my_next_nonce, in_account_pub; + END IF; + END IF; + + -- Update first and last block references (if necessary) + UPDATE accounts + SET first_block = CASE + WHEN my_prev_nonce IS NULL + THEN my_next_nonce + ELSE first_block + END + ,last_block = CASE + WHEN my_next_nonce IS NULL + THEN my_prev_nonce + ELSE last_block + END + WHERE account_pub = in_account_pub; + + -- Delete block + DELETE FROM blocks + WHERE account_pub = in_account_pub + AND nonce = in_block_nonce; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'block % vanished for account %', + in_block_nonce, in_account_pub; + END IF; + + -- Update object refcounts and delete unreferenced objects; the + -- pre-check above means this can only fail on a concurrent change + SELECT u.out_negative_refcount + ,u.out_refs_updated + INTO out_negative_refcount + ,out_refs_updated + FROM sync_update_object_refcounts(in_account_pub, + ina_object_uids, + ina_object_incs) AS u; + IF NOT out_refs_updated + THEN + RAISE EXCEPTION 'object refcounts changed concurrently for account %', + in_account_pub; + END IF; + + out_deleted := TRUE; +END $$; diff --git a/src/syncdb/syncdb_drop_tables.c b/src/syncdb/syncdb_drop_tables.c @@ -24,25 +24,8 @@ enum GNUNET_GenericReturnValue SYNCDB_drop_tables (void) { - struct GNUNET_PQ_Context *conn; - enum GNUNET_GenericReturnValue ret; - - if (NULL != pg->conn) - { - GNUNET_PQ_disconnect (pg->conn); - pg->conn = NULL; - } - conn = GNUNET_PQ_connect_with_cfg (pg->cfg, - "syncdb-postgres", - NULL, - NULL, - NULL); - if (NULL == conn) - return GNUNET_SYSERR; - ret = GNUNET_PQ_exec_sql (conn, - "drop"); - GNUNET_PQ_disconnect (conn); - return ret; + return GNUNET_PQ_exec_sql (pg->conn, + "drop"); } diff --git a/src/syncdb/syncdb_gc.c b/src/syncdb/syncdb_gc.c @@ -21,39 +21,34 @@ #include "syncdb_pg.h" -enum GNUNET_DB_QueryStatus +enum GNUNET_GenericReturnValue SYNCDB_gc (struct GNUNET_TIME_Absolute expire_backups, - struct GNUNET_TIME_Absolute expire_pending_payments) + struct GNUNET_TIME_Absolute expire_pending_payments, + struct GNUNET_TIME_Absolute expire_objects) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_absolute_time (&expire_backups), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_QueryParam params2[] = { GNUNET_PQ_query_param_absolute_time (&expire_pending_payments), + GNUNET_PQ_query_param_absolute_time (&expire_objects), GNUNET_PQ_query_param_end }; - enum GNUNET_DB_QueryStatus qs; - - SYNCDB_preflight (); - PREPARE ("gc_accounts", - "DELETE FROM accounts " - "WHERE" - " expiration_date < $1;"); - PREPARE ("gc_pending_payments", - "DELETE FROM payments " - "WHERE" - " paid=FALSE" - " AND" - " timestamp < $1;"); - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "gc_accounts", - params); - if (qs < 0) - return qs; - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "gc_pending_payments", - params2); + + if (GNUNET_OK != + GNUNET_PQ_prepare_anon ( + pg->conn, + "CALL sync.sync_do_gc ($1, $2, $3);")) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 > GNUNET_PQ_eval_prepared_non_select (pg->conn, + "", + params)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; } diff --git a/src/syncdb/syncdb_gc.sql b/src/syncdb/syncdb_gc.sql @@ -0,0 +1,40 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +DROP PROCEDURE IF EXISTS sync_do_gc; +CREATE PROCEDURE sync_do_gc( + in_expire_backups INT8 + ,in_expire_pending_payments INT8 + ,in_expire_objects INT8) +LANGUAGE plpgsql +AS $$ +BEGIN + DELETE + FROM accounts + WHERE expiration_date < in_expire_backups; + DELETE + FROM payments + WHERE paid=FALSE + AND order_timestamp < in_expire_pending_payments; + -- Only unreferenced objects may be collected; matches the + -- "objects_expire" partial index + DELETE + FROM objects + WHERE expiration_date < in_expire_objects + AND refcount = 0; +END $$; +COMMENT ON PROCEDURE sync_do_gc + IS 'Performs garbage collection on sync data'; diff --git a/src/syncdb/syncdb_increment_lifetime_TR.c b/src/syncdb/syncdb_increment_lifetime_TR.c @@ -29,6 +29,7 @@ SYNCDB_increment_lifetime_TR ( { struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); bool no_payment; + bool no_account; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (account_pub), GNUNET_PQ_query_param_string (order_id), @@ -39,6 +40,8 @@ SYNCDB_increment_lifetime_TR ( struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_bool ("out_no_payment", &no_payment), + GNUNET_PQ_result_spec_bool ("out_no_account", + &no_account), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; @@ -47,6 +50,7 @@ SYNCDB_increment_lifetime_TR ( PREPARE ("do_increment_lifetime", "SELECT" " out_no_payment" + ",out_no_account" " FROM sync_do_increment_lifetime" " ($1,$2,$3,$4);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, @@ -65,6 +69,8 @@ SYNCDB_increment_lifetime_TR ( case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: if (no_payment) return SYNC_DB_NO_RESULTS; + if (no_account) + return SYNC_DB_TARGET_MISSING; return SYNC_DB_ONE_RESULT; default: GNUNET_break (0); diff --git a/src/syncdb/syncdb_increment_lifetime_TR.sql b/src/syncdb/syncdb_increment_lifetime_TR.sql @@ -21,13 +21,13 @@ CREATE FUNCTION sync_do_increment_lifetime ( IN in_order_id TEXT, IN in_lifetime_us INT8, IN in_now_us INT8, - OUT out_no_payment BOOLEAN) + OUT out_no_payment BOOLEAN, + OUT out_no_account BOOLEAN) LANGUAGE plpgsql AS $$ -DECLARE - my_expiration INT8; BEGIN - out_no_payment = FALSE; + out_no_payment := FALSE; + out_no_account := FALSE; -- Mark payment as done UPDATE payments @@ -38,29 +38,18 @@ BEGIN IF NOT FOUND THEN - out_no_payment = TRUE; + out_no_payment := TRUE; RETURN; END IF; - -- Get current expiration - SELECT expiration_date - INTO my_expiration - FROM accounts + -- Extend account expiration (account already created by store_payment) + UPDATE accounts + SET expiration_date=expiration_date + in_lifetime_us WHERE account_pub=in_account_pub; IF NOT FOUND THEN - -- No account yet, create one with expiration = now + lifetime - INSERT INTO accounts - (account_pub - ,expiration_date) - VALUES - (in_account_pub - ,in_now_us + in_lifetime_us); - ELSE - -- Account exists, extend expiration - UPDATE accounts - SET expiration_date=my_expiration + in_lifetime_us - WHERE account_pub=in_account_pub; + out_no_account := TRUE; + RETURN; END IF; END $$; diff --git a/src/syncdb/syncdb_lookup_account_TR.c b/src/syncdb/syncdb_lookup_account_TR.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014--2022 Taler Systems SA + (C) 2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -15,7 +15,7 @@ */ /** * @file syncdb/syncdb_lookup_account_TR.c - * @brief lookup account and backup metadata + * @brief look up the status of an account * @author Christian Grothoff */ #include "syncdb_pg.h" @@ -24,52 +24,55 @@ enum SYNC_DB_QueryStatus SYNCDB_lookup_account_TR ( const struct SYNC_AccountPublicKeyP *account_pub, - struct GNUNET_HashCode *backup_hash) + struct GNUNET_TIME_Timestamp *expiration_date, + uint64_t *storage_used_bytes, + uint64_t *block_count) { - bool payment_required; - bool no_backup; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (account_pub), GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("out_backup_hash", - backup_hash), - GNUNET_PQ_result_spec_bool ("out_payment_required", - &payment_required), - GNUNET_PQ_result_spec_bool ("out_no_backup", - &no_backup), + GNUNET_PQ_result_spec_timestamp ("expiration_date", + expiration_date), + GNUNET_PQ_result_spec_uint64 ("storage_used_bytes", + storage_used_bytes), + GNUNET_PQ_result_spec_uint64 ("block_count", + block_count), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; SYNCDB_preflight (); - PREPARE ("do_lookup_account", + /* No stored procedure: this only reads, and an expired account is a + result here rather than an error, so none of the checks the + procedures share apply. */ + PREPARE ("account_select", "SELECT" - " out_backup_hash" - ",out_payment_required" - ",out_no_backup" - " FROM sync_do_lookup_account" - " ($1);"); + " a.expiration_date" + ",(SELECT COALESCE(SUM(octet_length(b.data_blob)),0)" + " FROM blocks b" + " WHERE b.account_pub = a.account_pub) AS storage_used_bytes" + ",(SELECT COUNT(*)" + " FROM blocks b" + " WHERE b.account_pub = a.account_pub) AS block_count" + " FROM accounts a" + " WHERE a.account_pub=$1"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "do_lookup_account", + "account_select", params, rs); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); return SYNC_DB_HARD_ERROR; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); return SYNC_DB_SOFT_ERROR; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; + return SYNC_DB_TARGET_MISSING; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - if (payment_required) - return SYNC_DB_PAYMENT_REQUIRED; - if (no_backup) - return SYNC_DB_NO_RESULTS; return SYNC_DB_ONE_RESULT; default: GNUNET_break (0); diff --git a/src/syncdb/syncdb_lookup_account_TR.sql b/src/syncdb/syncdb_lookup_account_TR.sql @@ -1,54 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA --- --- TALER is free software; you can redistribute it and/or modify it under the --- terms of the GNU General Public License as published by the Free Software --- Foundation; either version 3, or (at your option) any later version. --- --- TALER is distributed in the hope that it will be useful, but WITHOUT ANY --- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR --- A PARTICULAR PURPOSE. See the GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License along with --- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> --- - - -DROP FUNCTION IF EXISTS sync_do_lookup_account; -CREATE FUNCTION sync_do_lookup_account ( - IN in_account_pub BYTEA, - OUT out_backup_hash BYTEA, - OUT out_payment_required BOOLEAN, - OUT out_no_backup BOOLEAN) -LANGUAGE plpgsql -AS $$ -BEGIN - out_payment_required = FALSE; - out_no_backup = FALSE; - - -- Check if backup exists - SELECT backup_hash - INTO out_backup_hash - FROM backups - WHERE account_pub=in_account_pub; - - IF FOUND - THEN - RETURN; - END IF; - - -- No backup; check if account exists - PERFORM 1 - FROM accounts - WHERE account_pub=in_account_pub; - - IF NOT FOUND - THEN - out_payment_required = TRUE; - RETURN; - END IF; - - -- Account exists but no backup - out_no_backup = TRUE; -END $$; diff --git a/src/syncdb/syncdb_lookup_backup_TR.c b/src/syncdb/syncdb_lookup_backup_TR.c @@ -1,84 +0,0 @@ -/* - This file is part of TALER - (C) 2014--2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file syncdb/syncdb_lookup_backup_TR.c - * @brief obtain backup data - * @author Christian Grothoff - */ -#include "syncdb_pg.h" - - -enum SYNC_DB_QueryStatus -SYNCDB_lookup_backup_TR ( - const struct SYNC_AccountPublicKeyP *account_pub, - struct SYNC_AccountSignatureP *account_sig, - struct GNUNET_HashCode *prev_hash, - struct GNUNET_HashCode *backup_hash, - size_t *backup_size, - void **backup) -{ - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("account_sig", - account_sig), - GNUNET_PQ_result_spec_auto_from_type ("prev_hash", - prev_hash), - GNUNET_PQ_result_spec_auto_from_type ("backup_hash", - backup_hash), - GNUNET_PQ_result_spec_variable_size ("data", - backup, - backup_size), - GNUNET_PQ_result_spec_end - }; - - SYNCDB_preflight (); - PREPARE ("backup_select", - "SELECT " - " account_sig" - ",prev_hash" - ",backup_hash" - ",data " - "FROM" - " backups " - "WHERE" - " account_pub=$1;"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "backup_select", - params, - rs); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return SYNC_DB_NO_RESULTS; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return SYNC_DB_ONE_RESULT; - default: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - } -} - - -/* end of syncdb_lookup_backup_TR.c */ diff --git a/src/syncdb/syncdb_lookup_block_TR.c b/src/syncdb/syncdb_lookup_block_TR.c @@ -0,0 +1,112 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_lookup_block_TR.c + * @brief lookup a single block by nonce + * @author Iván Ávalos + */ +#include "syncdb_pg.h" + + +enum SYNC_DB_QueryStatus +SYNCDB_lookup_block_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *nonce, + struct GNUNET_HashCode *block_hash, + struct SYNC_BlockNonce *prev_nonce, + struct SYNC_BlockNonce *next_nonce, + size_t *data_size, + struct GNUNET_HashCode *refs_hash, + bool *is_first, + bool *is_last) +{ + bool account_missing; + bool block_missing; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (nonce), + GNUNET_PQ_query_param_end, + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_account_missing", + &account_missing), + GNUNET_PQ_result_spec_bool ("out_block_missing", + &block_missing), + GNUNET_PQ_result_spec_bool ("out_is_first", + is_first), + GNUNET_PQ_result_spec_bool ("out_is_last", + is_last), + GNUNET_PQ_result_spec_auto_from_type ("block_hash", + block_hash), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("prev_nonce", + prev_nonce), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("next_nonce", + next_nonce), + NULL), + GNUNET_PQ_result_spec_uint64 ("data_size", + data_size), + GNUNET_PQ_result_spec_auto_from_type ("refs_hash", + refs_hash), + GNUNET_PQ_result_spec_end, + }; + enum GNUNET_DB_QueryStatus qs; + + SYNCDB_preflight (); + PREPARE ("block_select", + "SELECT" + " out_account_missing" + ",out_block_missing" + ",out_is_first" + ",out_is_last" + ",nonce" + ",block_hash" + ",prev_nonce" + ",next_nonce" + ",data_size" + ",refs_hash" + " FROM sync_do_lookup_block" + " ($1,$2)"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "block_select", + params, + rs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (account_missing) + return SYNC_DB_PAYMENT_REQUIRED; + if (block_missing) + return SYNC_DB_TARGET_MISSING; + return SYNC_DB_ONE_RESULT; + default: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + } +} + + +/* end of syncdb_lookup_block_TR.c */ diff --git a/src/syncdb/syncdb_lookup_block_TR.sql b/src/syncdb/syncdb_lookup_block_TR.sql @@ -0,0 +1,94 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +DROP FUNCTION IF EXISTS sync_do_lookup_block; +CREATE FUNCTION sync_do_lookup_block ( + IN in_account_pub BYTEA, + IN in_nonce BYTEA) +RETURNS sync_do_lookup_block_return_type +LANGUAGE plpgsql +AS $$ +DECLARE + my_first_block BYTEA; + my_last_block BYTEA; + my_hash BYTEA; + my_prev BYTEA; + my_next BYTEA; + my_data_size INT8; + my_refs_hash BYTEA; +BEGIN + -- Check if account exists + SELECT first_block + ,last_block + INTO my_first_block + ,my_last_block + FROM accounts + WHERE account_pub = in_account_pub; + + IF NOT FOUND + THEN + RETURN (TRUE -- out_account_missing + ,FALSE -- out_block_missing + ,FALSE -- out_is_first + ,FALSE -- out_is_last + ,in_nonce -- nonce + ,decode(repeat('00', 64), 'hex') -- block_hash + ,decode(repeat('00', 24), 'hex') -- prev_nonce + ,decode(repeat('00', 24), 'hex') -- next_nonce + ,0::INT8 -- data_size + ,decode(repeat('00', 64), 'hex')); -- refs_hash + END IF; + + -- Look up the block + SELECT block_hash + ,prev_nonce + ,next_nonce + ,octet_length(data_blob) + ,refs_hash + INTO my_hash + ,my_prev + ,my_next + ,my_data_size + ,my_refs_hash + FROM blocks + WHERE account_pub = in_account_pub + AND nonce = in_nonce; + + IF NOT FOUND + THEN + RETURN (FALSE -- out_account_missing + ,TRUE -- out_block_missing + ,FALSE -- out_is_first + ,FALSE -- out_is_last + ,in_nonce -- nonce + ,decode(repeat('00', 64), 'hex') -- block_hash + ,decode(repeat('00', 24), 'hex') -- prev_nonce + ,decode(repeat('00', 24), 'hex') -- next_nonce + ,0::INT8 -- data_size + ,decode(repeat('00', 64), 'hex')); -- refs_hash + END IF; + + RETURN (FALSE -- out_account_missing + ,FALSE -- out_block_missing + ,in_nonce IS NOT DISTINCT FROM my_first_block -- out_is_first + ,in_nonce IS NOT DISTINCT FROM my_last_block -- out_is_last + ,in_nonce -- nonce + ,my_hash -- block_hash + ,my_prev -- prev_nonce + ,my_next -- next_nonce + ,my_data_size -- data_size + ,my_refs_hash); -- refs_hash +END $$; diff --git a/src/syncdb/syncdb_lookup_block_range_TR.c b/src/syncdb/syncdb_lookup_block_range_TR.c @@ -0,0 +1,228 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_lookup_block_range_TR.c + * @brief lookup a (limited) range of blocks starting at a nonce + * @author Iván Ávalos + */ +#include "syncdb_pg.h" + + +/** + * Closure for the block iterator callback. + */ +struct BlockIteratorContext +{ + /** + * Iterator function to call for each block result. + */ + SYNC_DB_BlockIterator it; + + /** + * Closure for @e it. + */ + void *it_cls; + + /** + * Query status to return. + */ + enum SYNC_DB_QueryStatus qs; + + /** + * True if the account does not exist or is unpaid. + */ + bool out_account_missing; + + /** + * True if the requested start block does not exist. + */ + bool out_block_missing; + + /** + * True if the account has no blocks at all. + */ + bool out_backup_empty; +}; + + +/** + * Helper function for #SYNCDB_lookup_block_range_TR(). + * To be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls closure of type `struct BlockIteratorContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +block_range_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct BlockIteratorContext *bic = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + bool out_account_missing; + bool out_block_missing; + bool out_backup_empty; + bool null_prev_nonce = false; + bool null_next_nonce = false; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode block_hash; + struct SYNC_BlockNonce prev_nonce; + struct SYNC_BlockNonce next_nonce; + size_t data_size = 0; + void *data = NULL; + struct SYNC_AccountSignatureP upload_sig; + struct GNUNET_HashCode old_hash; + struct GNUNET_HashCode refs_hash; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_account_missing", + &out_account_missing), + GNUNET_PQ_result_spec_bool ("out_block_missing", + &out_block_missing), + GNUNET_PQ_result_spec_bool ("out_backup_empty", + &out_backup_empty), + GNUNET_PQ_result_spec_auto_from_type ("nonce", + &nonce), + GNUNET_PQ_result_spec_auto_from_type ("block_hash", + &block_hash), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("prev_nonce", + &prev_nonce), + &null_prev_nonce), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("next_nonce", + &next_nonce), + &null_next_nonce), + GNUNET_PQ_result_spec_variable_size ("data_blob", + &data, + &data_size), + GNUNET_PQ_result_spec_auto_from_type ("upload_sig", + &upload_sig), + GNUNET_PQ_result_spec_auto_from_type ("old_hash", + &old_hash), + GNUNET_PQ_result_spec_auto_from_type ("refs_hash", + &refs_hash), + GNUNET_PQ_result_spec_end, + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + bic->qs = SYNC_DB_SOFT_ERROR; + return; + } + + bic->qs = i + 1; + bic->out_account_missing = out_account_missing; + bic->out_block_missing = out_block_missing; + bic->out_backup_empty = out_backup_empty; + if ( (! out_account_missing) && + (! out_block_missing) && + (! out_backup_empty) ) + bic->it (bic->it_cls, + &nonce, + &block_hash, + null_prev_nonce + ? NULL + : &prev_nonce, + null_next_nonce + ? NULL + : &next_nonce, + data_size, + data, + &upload_sig, + &old_hash, + &refs_hash); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum SYNC_DB_QueryStatus +SYNCDB_lookup_block_range_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *start_nonce, + const int16_t limit, + SYNC_DB_BlockIterator it, + void *it_cls) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + (NULL != start_nonce) + ? GNUNET_PQ_query_param_auto_from_type (start_nonce) + : GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_int16 (&limit), + GNUNET_PQ_query_param_end, + }; + struct BlockIteratorContext bic = { + .it = it, + .it_cls = it_cls, + }; + enum GNUNET_DB_QueryStatus qs; + + SYNCDB_preflight (); + PREPARE ("blocks_select_range", + "SELECT" + " out_account_missing" + ",out_block_missing" + ",out_backup_empty" + ",nonce" + ",block_hash" + ",prev_nonce" + ",next_nonce" + ",data_blob" + ",upload_sig" + ",old_hash" + ",refs_hash" + " FROM sync_do_lookup_block_range" + " ($1,$2,$3)"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "blocks_select_range", + params, + &block_range_cb, + &bic); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* the stored procedure always returns a status row */ + GNUNET_break (0); + return SYNC_DB_NO_RESULTS; + default: + break; + } + /* map the flags of the (single) status row to SYNC_DB codes */ + if (bic.out_account_missing) + return SYNC_DB_PAYMENT_REQUIRED; + if (bic.out_block_missing) + return SYNC_DB_TARGET_MISSING; + if (bic.out_backup_empty) + return SYNC_DB_NO_RESULTS; + return bic.qs; +} + + +/* end of syncdb_lookup_block_range_TR.c */ diff --git a/src/syncdb/syncdb_lookup_block_range_TR.sql b/src/syncdb/syncdb_lookup_block_range_TR.sql @@ -0,0 +1,142 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +DROP FUNCTION IF EXISTS sync_do_lookup_block_range; +CREATE FUNCTION sync_do_lookup_block_range ( + IN in_account_pub BYTEA, + IN in_start_nonce BYTEA, + IN in_limit INT2) +RETURNS SETOF sync_do_lookup_block_range_return_type +LANGUAGE plpgsql +AS $$ +DECLARE + my_first_block BYTEA; + my_curr_block BYTEA; + my_prev_block BYTEA; + my_curr_hash BYTEA; + my_curr_prev BYTEA; + my_curr_next BYTEA; + my_curr_data BYTEA; + my_curr_upload_sig BYTEA; + my_curr_old_hash BYTEA; + my_curr_refs_hash BYTEA; + cnt INT := 0; +BEGIN + + -- Check if account exists + SELECT first_block + INTO my_first_block + FROM accounts + WHERE account_pub = in_account_pub; + + IF NOT FOUND + THEN + RETURN NEXT (TRUE -- out_account_missing + ,FALSE + ,FALSE + ,COALESCE(in_start_nonce, + decode(repeat('00', 24), 'hex')) + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 24), 'hex') + ,decode(repeat('00', 24), 'hex') + ,decode(repeat('00', 0), 'hex') + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 64), 'hex')); + RETURN; + END IF; + + -- Check if backup is empty + IF my_first_block IS NULL + THEN + RETURN NEXT (FALSE + ,FALSE + ,TRUE -- out_backup_empty + ,COALESCE(in_start_nonce, + decode(repeat('00', 24), 'hex')) + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 24), 'hex') + ,decode(repeat('00', 24), 'hex') + ,decode(repeat('00', 0), 'hex') + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 64), 'hex')); + RETURN; + END IF; + + my_curr_block := COALESCE(in_start_nonce, my_first_block); + + IF in_limit <= 0 + THEN + RETURN; + END IF; + + WHILE cnt < in_limit + AND my_curr_block IS NOT NULL + LOOP + my_prev_block := my_curr_block; + + SELECT nonce + ,block_hash + ,prev_nonce + ,next_nonce + ,data_blob + ,upload_sig + ,old_hash + ,refs_hash + INTO my_curr_block + ,my_curr_hash + ,my_curr_prev + ,my_curr_next + ,my_curr_data + ,my_curr_upload_sig + ,my_curr_old_hash + ,my_curr_refs_hash + FROM blocks + WHERE account_pub = in_account_pub + AND nonce = my_curr_block; + + IF NOT FOUND + THEN + RETURN NEXT (FALSE + ,TRUE -- out_block_missing + ,FALSE + ,my_prev_block + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 24), 'hex') + ,decode(repeat('00', 24), 'hex') + ,decode(repeat('00', 0), 'hex') + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 64), 'hex') + ,decode(repeat('00', 64), 'hex')); + RETURN; + END IF; + + RETURN NEXT (FALSE, FALSE, FALSE + ,my_curr_block + ,my_curr_hash + ,my_curr_prev + ,my_curr_next + ,my_curr_data + ,my_curr_upload_sig + ,my_curr_old_hash + ,my_curr_refs_hash); + cnt := cnt + 1; + my_curr_block := my_curr_next; + END LOOP; + + RETURN; +END $$; diff --git a/src/syncdb/syncdb_lookup_object.c b/src/syncdb/syncdb_lookup_object.c @@ -0,0 +1,87 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_lookup_object.c + * @brief lookup object on sync account + * @author Iván Ávalos + */ +#include "syncdb_pg.h" + + +enum SYNC_DB_QueryStatus +SYNCDB_lookup_object ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct GNUNET_HashCode *object_hash, + int64_t *refcount, + struct GNUNET_TIME_Timestamp *expiration_date, + size_t *data_size, + void *data) +{ + void *tmp_data; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (object_hash), + GNUNET_PQ_query_param_end, + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_int64 ("refcount", + refcount), + GNUNET_PQ_result_spec_auto_from_type ("expiration_date", + expiration_date), + GNUNET_PQ_result_spec_variable_size ("data_blob", + &tmp_data, + data_size), + GNUNET_PQ_result_spec_end, + }; + enum GNUNET_DB_QueryStatus qs; + + SYNCDB_preflight (); + PREPARE ("do_lookup_object", + "SELECT" + " refcount" + ",expiration_date" + ",data_blob" + " FROM objects" + " WHERE" + " account_pub=$1" + " AND" + " object_hash=$2"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_lookup_object", + params, + rs); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* no such object; a perfectly normal outcome */ + return SYNC_DB_NO_RESULTS; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (NULL != data) + *(void **) data = tmp_data; + else + GNUNET_free (tmp_data); + return SYNC_DB_ONE_RESULT; + default: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + } +} diff --git a/src/syncdb/syncdb_lookup_object_uid.c b/src/syncdb/syncdb_lookup_object_uid.c @@ -0,0 +1,89 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_lookup_object_uid.c + * @brief lookup object on sync account by UID + * @author Iván Ávalos + */ +#include "syncdb_pg.h" + + +enum SYNC_DB_QueryStatus +SYNCDB_lookup_object_uid ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_ObjectUID *uid, + struct GNUNET_HashCode *object_hash, + int64_t *refcount, + size_t *data_size, + void *data) +{ + void *tmp_data; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (uid), + GNUNET_PQ_query_param_end, + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("object_hash", + object_hash), + GNUNET_PQ_result_spec_int64 ("refcount", + refcount), + GNUNET_PQ_result_spec_variable_size ("data_blob", + &tmp_data, + data_size), + GNUNET_PQ_result_spec_end, + }; + enum GNUNET_DB_QueryStatus qs; + + SYNCDB_preflight (); + PREPARE ("do_lookup_object_uid", + "SELECT" + " object_hash" + ",refcount" + ",data_blob" + " FROM objects" + " WHERE" + " account_pub=$1" + " AND" + " uid=$2"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_lookup_object_uid", + params, + rs); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return SYNC_DB_NO_RESULTS; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (NULL != data) + *(void **) data = tmp_data; + else + GNUNET_free (tmp_data); + return SYNC_DB_ONE_RESULT; + default: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + } +} + + +/* end of syncdb_lookup_object_uid.c */ diff --git a/src/syncdb/syncdb_lookup_pending_payments_by_account.c b/src/syncdb/syncdb_lookup_pending_payments_by_account.c @@ -0,0 +1,150 @@ +/* + This file is part of TALER + (C) 2014--2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_lookup_pending_payments_by_account.c + * @brief lookup pending payments by account + * @author Christian Grothoff + */ +#include "syncdb_pg.h" + + +/** + * Closure for #payment_by_account_cb. + */ +struct PaymentIteratorContext +{ + /** + * Function to call on each result + */ + SYNC_DB_PaymentPendingIterator it; + + /** + * Closure for @e it. + */ + void *it_cls; + + /** + * Query status to return. + */ + enum SYNC_DB_QueryStatus qs; + +}; + + +/** + * Helper function for #SYNCDB_lookup_pending_payments_by_account_TR(). + * To be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls closure of type `struct PaymentIteratorContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +payment_by_account_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct PaymentIteratorContext *pic = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + struct GNUNET_TIME_Timestamp timestamp; + char *order_id; + struct TALER_Amount amount; + struct TALER_ClaimTokenP token; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_timestamp ("order_timestamp", + &timestamp), + GNUNET_PQ_result_spec_string ("order_id", + &order_id), + GNUNET_PQ_result_spec_auto_from_type ("claim_token", + &token), + TALER_PQ_result_spec_amount ("amount", + pg->currency, + &amount), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + pic->qs = SYNC_DB_HARD_ERROR; + return; + } + pic->qs = i + 1; + pic->it (pic->it_cls, + timestamp, + order_id, + &token, + &amount); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum SYNC_DB_QueryStatus +SYNCDB_lookup_pending_payments_by_account_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + SYNC_DB_PaymentPendingIterator it, + void *it_cls) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_end + }; + struct PaymentIteratorContext pic = { + .it = it, + .it_cls = it_cls + }; + enum GNUNET_DB_QueryStatus qs; + + SYNCDB_preflight (); + PREPARE ("payments_select_by_account", + "SELECT" + " order_timestamp" + ",order_id" + ",claim_token" + ",amount" + " FROM payments" + " WHERE" + " paid=FALSE" + " AND" + " account_pub=$1;"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "payments_select_by_account", + params, + &payment_by_account_cb, + &pic); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return SYNC_DB_NO_RESULTS; + default: + return pic.qs; + } +} + + +/* end of syncdb_lookup_pending_payments_by_account.c */ diff --git a/src/syncdb/syncdb_lookup_pending_payments_by_account_TR.c b/src/syncdb/syncdb_lookup_pending_payments_by_account_TR.c @@ -1,142 +0,0 @@ -/* - This file is part of TALER - (C) 2014--2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file syncdb/syncdb_lookup_pending_payments_by_account_TR.c - * @brief lookup pending payments by account - * @author Christian Grothoff - */ -#include "syncdb_pg.h" - - -/** - * Closure for #payment_by_account_cb. - */ -struct PaymentIteratorContext -{ - /** - * Function to call on each result - */ - SYNC_DB_PaymentPendingIterator it; - - /** - * Closure for @e it. - */ - void *it_cls; - - /** - * Query status to return. - */ - enum GNUNET_DB_QueryStatus qs; - -}; - - -/** - * Helper function for #SYNCDB_lookup_pending_payments_by_account_TR(). - * To be called with the results of a SELECT statement - * that has returned @a num_results results. - * - * @param cls closure of type `struct PaymentIteratorContext *` - * @param result the postgres result - * @param num_results the number of results in @a result - */ -static void -payment_by_account_cb (void *cls, - PGresult *result, - unsigned int num_results) -{ - struct PaymentIteratorContext *pic = cls; - - for (unsigned int i = 0; i < num_results; i++) - { - struct GNUNET_TIME_Timestamp timestamp; - char *order_id; - struct TALER_Amount amount; - struct TALER_ClaimTokenP token; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_timestamp ("timestamp", - &timestamp), - GNUNET_PQ_result_spec_string ("order_id", - &order_id), - GNUNET_PQ_result_spec_auto_from_type ("token", - &token), - TALER_PQ_result_spec_amount ("amount", - pg->currency, - &amount), - GNUNET_PQ_result_spec_end - }; - - if (GNUNET_OK != - GNUNET_PQ_extract_result (result, - rs, - i)) - { - GNUNET_break (0); - pic->qs = GNUNET_DB_STATUS_HARD_ERROR; - return; - } - pic->qs = i + 1; - pic->it (pic->it_cls, - timestamp, - order_id, - &token, - &amount); - GNUNET_PQ_cleanup_result (rs); - } -} - - -enum GNUNET_DB_QueryStatus -SYNCDB_lookup_pending_payments_by_account_TR ( - const struct SYNC_AccountPublicKeyP *account_pub, - SYNC_DB_PaymentPendingIterator it, - void *it_cls) -{ - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_end - }; - struct PaymentIteratorContext pic = { - .it = it, - .it_cls = it_cls - }; - enum GNUNET_DB_QueryStatus qs; - - SYNCDB_preflight (); - PREPARE ("payments_select_by_account", - "SELECT" - " timestamp" - ",order_id" - ",token" - ",amount" - " FROM payments" - " WHERE" - " paid=FALSE" - " AND" - " account_pub=$1;"); - qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "payments_select_by_account", - params, - &payment_by_account_cb, - &pic); - if (qs > 0) - return pic.qs; - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); - return qs; -} - - -/* end of syncdb_lookup_pending_payments_by_account_TR.c */ diff --git a/src/syncdb/syncdb_object_refcounts.sql b/src/syncdb/syncdb_object_refcounts.sql @@ -0,0 +1,145 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +-- +-- Helper to update object refcounts for a given account and parallel +-- arrays of object uuids and increments. +-- +-- Increments are aggregated per UID before they are applied, so the +-- outcome does not depend on the order UIDs appear in the arrays, and +-- agrees with sync_check_object_refcounts_pre() by construction. +-- +DROP FUNCTION IF EXISTS sync_update_object_refcounts; +CREATE FUNCTION sync_update_object_refcounts( + IN in_account_pub BYTEA, + IN ina_object_uids BYTEA[], + IN ina_object_incs INT2[]) +RETURNS TABLE ( + out_negative_refcount BOOLEAN, + out_refs_updated BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_rec RECORD; + my_new_refcount INT8; +BEGIN + out_negative_refcount := FALSE; + out_refs_updated := TRUE; + + IF array_length(ina_object_uids,1) IS DISTINCT FROM array_length(ina_object_incs,1) + THEN + RAISE EXCEPTION 'uuid and inc arrays must have same length'; + END IF; + + IF COALESCE(array_length(ina_object_uids, 1), 0) = 0 + THEN + RETURN NEXT; + RETURN; + END IF; + + FOR my_rec IN + SELECT uid, SUM(inc)::INT8 AS total_inc + FROM unnest(ina_object_uids, ina_object_incs) AS t(uid, inc) + GROUP BY uid + LOOP + UPDATE objects o + SET refcount = o.refcount + my_rec.total_inc + WHERE o.uid = my_rec.uid + AND o.account_pub = in_account_pub + RETURNING o.refcount INTO my_new_refcount; + + IF NOT FOUND + THEN + -- No such object for this account + out_refs_updated := FALSE; + RETURN NEXT; + RETURN; + END IF; + + IF my_new_refcount < 0 + THEN + out_negative_refcount := TRUE; + out_refs_updated := FALSE; + RETURN NEXT; + RETURN; + END IF; + + -- Collect the object when its last reference goes away. A + -- freshly uploaded object also sits at refcount 0, hence the + -- decrement check. + IF (my_new_refcount = 0) AND + (my_rec.total_inc < 0) + THEN + DELETE FROM objects + WHERE uid = my_rec.uid + AND account_pub = in_account_pub; + END IF; + END LOOP; + + RETURN NEXT; + RETURN; +END $$; + +-- +-- Checks, without modifying anything, whether the given increments can +-- be applied: every referenced UID must exist for the account, and no +-- aggregated decrement may drive a refcount below zero. To be called +-- before any state is mutated. +-- +DROP FUNCTION IF EXISTS sync_check_object_refcounts_pre; +CREATE FUNCTION sync_check_object_refcounts_pre( + IN in_account_pub BYTEA, + IN ina_object_uids BYTEA[], + IN ina_object_incs INT2[]) +RETURNS TABLE ( + out_negative_refcount BOOLEAN, + out_refs_missing BOOLEAN) +LANGUAGE plpgsql +AS $$ +BEGIN + out_negative_refcount := FALSE; + out_refs_missing := FALSE; + + IF array_length(ina_object_uids,1) IS DISTINCT FROM array_length(ina_object_incs,1) + THEN + RAISE EXCEPTION 'uuid and inc arrays must have same length'; + END IF; + + IF COALESCE(array_length(ina_object_uids, 1), 0) = 0 + THEN + RETURN NEXT; + RETURN; + END IF; + + SELECT + bool_or(o.uid IS NOT NULL AND a.total_inc < 0 AND o.refcount < (- a.total_inc)) + ,bool_or(o.uid IS NULL) + INTO out_negative_refcount + ,out_refs_missing + FROM ( + SELECT uid, SUM(inc)::INT8 AS total_inc + FROM unnest(ina_object_uids, ina_object_incs) AS t(uid, inc) + GROUP BY uid + ) a + LEFT JOIN objects o + ON o.uid = a.uid + AND o.account_pub = in_account_pub; + + out_negative_refcount := COALESCE(out_negative_refcount, FALSE); + out_refs_missing := COALESCE(out_refs_missing, FALSE); + RETURN NEXT; + RETURN; +END $$; diff --git a/src/syncdb/syncdb_pg.c b/src/syncdb/syncdb_pg.c @@ -18,6 +18,9 @@ * @brief shared database state and helpers for sync postgres * @author Christian Grothoff */ +struct PostgresClosure; +#define GNUNET_PQ_RECONNECT_CALLBACK_CLOSURE \ + struct PostgresClosure #include "syncdb_pg.h" #include "sync/sync_util.h" @@ -28,9 +31,44 @@ struct PostgresClosure *pg; -enum GNUNET_GenericReturnValue -SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg, - bool skip_preflight) +/** + * Function called each time we connect or reconnect to the + * database. + * + * @param pg_ database context + * @param pq database connection handle + */ +static void +reconnect_cb (struct PostgresClosure *pg_, + struct GNUNET_PQ_Context *pq) +{ + struct GNUNET_PQ_ExecuteStatement es[] = { + GNUNET_PQ_make_execute ("SET search_path TO sync;"), + GNUNET_PQ_EXECUTE_STATEMENT_END + }; + + if (GNUNET_OK != + GNUNET_PQ_exec_statements (pq, + es)) + { + GNUNET_break (0); + return; + } + pg_->prep_gen++; +} + + +/** + * Initialize the global database closure #pg from @a cfg. + * + * @param cfg configuration to use + * @param check_current true to refuse to start unless the + * database schema is up-to-date + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +do_init (const struct GNUNET_CONFIGURATION_Handle *cfg, + bool check_current) { pg = GNUNET_new (struct PostgresClosure); pg->cfg = cfg; @@ -61,19 +99,46 @@ SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg, pg = NULL; return GNUNET_SYSERR; } - if ( (! skip_preflight) && - (GNUNET_OK != - SYNCDB_preflight ()) ) + pg->conn = GNUNET_PQ_init (cfg, + "syncdb-postgres", + &reconnect_cb, + pg); + if (NULL == pg->conn) + { + SYNCDB_fini (); + return GNUNET_NO; + } + if (check_current && + (GNUNET_OK != + GNUNET_PQ_check_current (pg->conn, + "sync-"))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database not ready. Try running sync-dbinit!\n"); + "Database schema is not up-to-date." + " Try running sync-dbinit!\n"); SYNCDB_fini (); - return GNUNET_SYSERR; + return GNUNET_NO; } return GNUNET_OK; } +enum GNUNET_GenericReturnValue +SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + return do_init (cfg, + true); +} + + +enum GNUNET_GenericReturnValue +SYNCDB_init_admin (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + return do_init (cfg, + false); +} + + void SYNCDB_fini (void) { diff --git a/src/syncdb/syncdb_pg.h b/src/syncdb/syncdb_pg.h @@ -76,7 +76,8 @@ extern struct PostgresClosure *pg; /** * Prepares SQL statement @a sql under @a name for * connection @a pg once. - * Returns with #GNUNET_DB_STATUS_HARD_ERROR on failure. + * Returns with #SYNC_DB_HARD_ERROR on failure, so this may only + * be used in functions returning `enum SYNC_DB_QueryStatus`. * * @param name name to prepare the statement under * @param sql actual SQL text @@ -97,7 +98,7 @@ extern struct PostgresClosure *pg; ps)) \ { \ GNUNET_break (0); \ - return GNUNET_DB_STATUS_HARD_ERROR; \ + return SYNC_DB_HARD_ERROR; \ } \ gen = pg->prep_gen; \ } \ diff --git a/src/syncdb/syncdb_preflight.c b/src/syncdb/syncdb_preflight.c @@ -21,53 +21,6 @@ #include "syncdb_pg.h" -static enum GNUNET_GenericReturnValue -internal_setup (void) -{ - if (NULL == pg->conn) - { -#if AUTO_EXPLAIN - /* Enable verbose logging to see where queries do not - properly use indices */ - struct GNUNET_PQ_ExecuteStatement es[] = { - GNUNET_PQ_make_try_execute ("LOAD 'auto_explain';"), - GNUNET_PQ_make_try_execute ("SET auto_explain.log_min_duration=50;"), - GNUNET_PQ_make_try_execute ("SET auto_explain.log_timing=TRUE;"), - GNUNET_PQ_make_try_execute ("SET auto_explain.log_analyze=TRUE;"), - /* https://wiki.postgresql.org/wiki/Serializable suggests to really - force the default to 'serializable' if SSI is to be used. */ - GNUNET_PQ_make_try_execute ( - "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;"), - GNUNET_PQ_make_try_execute ("SET enable_sort=OFF;"), - GNUNET_PQ_make_try_execute ("SET enable_seqscan=OFF;"), - GNUNET_PQ_make_execute ("SET search_path TO sync;"), - GNUNET_PQ_EXECUTE_STATEMENT_END - }; -#else - struct GNUNET_PQ_ExecuteStatement es[] = { - GNUNET_PQ_make_execute ("SET search_path TO sync;"), - GNUNET_PQ_EXECUTE_STATEMENT_END - }; -#endif - struct GNUNET_PQ_Context *db_conn; - - db_conn = GNUNET_PQ_connect_with_cfg2 (pg->cfg, - "syncdb-postgres", - "sync-", - es, - NULL, - GNUNET_PQ_FLAG_CHECK_CURRENT); - if (NULL == db_conn) - return GNUNET_SYSERR; - pg->conn = db_conn; - pg->prep_gen++; - } - if (NULL == pg->transaction_name) - GNUNET_PQ_reconnect_if_down (pg->conn); - return GNUNET_OK; -} - - enum GNUNET_GenericReturnValue SYNCDB_preflight (void) { @@ -76,22 +29,11 @@ SYNCDB_preflight (void) GNUNET_PQ_EXECUTE_STATEMENT_END }; - if (NULL == pg->conn) - { - if (GNUNET_OK != - internal_setup ()) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to ensure DB is initialized\n"); - return GNUNET_SYSERR; - } - } - else + if (NULL == pg->transaction_name) { GNUNET_PQ_reconnect_if_down (pg->conn); + return GNUNET_OK; } - if (NULL == pg->transaction_name) - return GNUNET_OK; /* all good */ if (GNUNET_OK == GNUNET_PQ_exec_statements (pg->conn, es)) diff --git a/src/syncdb/syncdb_store_backup_TR.c b/src/syncdb/syncdb_store_backup_TR.c @@ -1,100 +0,0 @@ -/* - This file is part of TALER - (C) 2014--2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file syncdb/syncdb_store_backup_TR.c - * @brief store first backup - * @author Christian Grothoff - */ -#include "syncdb_pg.h" - - -enum SYNC_DB_QueryStatus -SYNCDB_store_backup_TR ( - const struct SYNC_AccountPublicKeyP *account_pub, - const struct SYNC_AccountSignatureP *account_sig, - const struct GNUNET_HashCode *backup_hash, - size_t backup_size, - const void *backup) -{ - uint32_t bs = (uint32_t) backup_size; - bool payment_required; - bool old_backup_mismatch; - bool no_change; - bool inserted; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_auto_from_type (account_sig), - GNUNET_PQ_query_param_auto_from_type (backup_hash), - GNUNET_PQ_query_param_uint32 (&bs), - GNUNET_PQ_query_param_fixed_size (backup, - backup_size), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("out_payment_required", - &payment_required), - GNUNET_PQ_result_spec_bool ("out_old_backup_mismatch", - &old_backup_mismatch), - GNUNET_PQ_result_spec_bool ("out_no_change", - &no_change), - GNUNET_PQ_result_spec_bool ("out_inserted", - &inserted), - GNUNET_PQ_result_spec_end - }; - enum GNUNET_DB_QueryStatus qs; - - SYNCDB_preflight (); - PREPARE ("do_store_backup", - "SELECT" - " out_payment_required" - ",out_old_backup_mismatch" - ",out_no_change" - ",out_inserted" - " FROM sync_do_store_backup" - " ($1,$2,$3,$4,$5);"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "do_store_backup", - params, - rs); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - if (inserted) - return SYNC_DB_ONE_RESULT; - if (payment_required) - return SYNC_DB_PAYMENT_REQUIRED; - if (old_backup_mismatch) - return SYNC_DB_OLD_BACKUP_MISMATCH; - if (no_change) - return SYNC_DB_NO_RESULTS; - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - default: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - } -} - - -/* end of syncdb_store_backup_TR.c */ diff --git a/src/syncdb/syncdb_store_backup_TR.sql b/src/syncdb/syncdb_store_backup_TR.sql @@ -1,93 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA --- --- TALER is free software; you can redistribute it and/or modify it under the --- terms of the GNU General Public License as published by the Free Software --- Foundation; either version 3, or (at your option) any later version. --- --- TALER is distributed in the hope that it will be useful, but WITHOUT ANY --- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR --- A PARTICULAR PURPOSE. See the GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License along with --- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> --- - - -DROP FUNCTION IF EXISTS sync_do_store_backup; -CREATE FUNCTION sync_do_store_backup ( - IN in_account_pub BYTEA, - IN in_account_sig BYTEA, - IN in_backup_hash BYTEA, - IN in_backup_size INT4, - IN in_data BYTEA, - OUT out_payment_required BOOLEAN, - OUT out_old_backup_mismatch BOOLEAN, - OUT out_no_change BOOLEAN, - OUT out_inserted BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - my_no_previous_hash BYTEA; - my_existing_hash BYTEA; -BEGIN - out_payment_required = FALSE; - out_old_backup_mismatch = FALSE; - out_no_change = FALSE; - out_inserted = FALSE; - my_no_previous_hash = '\x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; - - -- Check if account exists - PERFORM 1 - FROM accounts - WHERE account_pub=in_account_pub; - - IF NOT FOUND - THEN - out_payment_required = TRUE; - RETURN; - END IF; - - -- Try to insert the backup - INSERT INTO backups - (account_pub - ,account_sig - ,prev_hash - ,backup_hash - ,data) - VALUES - (in_account_pub - ,in_account_sig - ,my_no_previous_hash - ,in_backup_hash - ,in_data) - ON CONFLICT DO NOTHING; - - IF FOUND - THEN - out_inserted = TRUE; - RETURN; - END IF; - - -- Check for existing backup hash - SELECT backup_hash - INTO my_existing_hash - FROM backups - WHERE account_pub=in_account_pub; - - IF NOT FOUND - THEN - -- Shouldn't happen: account exists but insert failed and no backup exists - RAISE EXCEPTION 'unexpected state: account exists but no backup and insert failed'; - END IF; - - IF my_existing_hash <> in_backup_hash - THEN - out_old_backup_mismatch = TRUE; - RETURN; - END IF; - - -- Backup identical to what was provided - out_no_change = TRUE; -END $$; diff --git a/src/syncdb/syncdb_store_object_TR.c b/src/syncdb/syncdb_store_object_TR.c @@ -0,0 +1,89 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_store_object_TR.c + * @brief store object into sync database + * @author Iván Ávalos + */ +#include "syncdb_pg.h" + + +enum SYNC_DB_QueryStatus +SYNCDB_store_object_TR ( + const struct SYNC_ObjectUID *uid, + const struct SYNC_AccountPublicKeyP *account_pub, + const struct GNUNET_HashCode *object_hash, + struct GNUNET_TIME_Relative lifetime, + size_t data_size, + const void *data) +{ + enum GNUNET_DB_QueryStatus qs; + bool out_account_missing; + bool out_inserted; + struct GNUNET_TIME_Timestamp expiration + = GNUNET_TIME_relative_to_timestamp (lifetime); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (uid), + GNUNET_PQ_query_param_auto_from_type (object_hash), + GNUNET_PQ_query_param_timestamp (&expiration), + GNUNET_PQ_query_param_fixed_size (data, + data_size), + GNUNET_PQ_query_param_end, + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_account_missing", + &out_account_missing), + GNUNET_PQ_result_spec_bool ("out_inserted", + &out_inserted), + GNUNET_PQ_result_spec_end, + }; + SYNCDB_preflight (); + PREPARE ("do_store_object", + "SELECT" + " out_account_missing" + ",out_inserted" + " FROM sync_do_store_object" + " ($1,$2,$3,$4,$5)"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_store_object", + params, + rs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (out_account_missing) + return SYNC_DB_PAYMENT_REQUIRED; + if (! out_inserted) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Object with given UID already exists\n"); + return SYNC_DB_ONE_RESULT; + default: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + } +} + + +/* end of syncdb_store_object_TR.c */ diff --git a/src/syncdb/syncdb_store_object_TR.sql b/src/syncdb/syncdb_store_object_TR.sql @@ -0,0 +1,61 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +DROP FUNCTION IF EXISTS sync_do_store_object; +CREATE FUNCTION sync_do_store_object ( + IN in_account_pub BYTEA, + IN in_uid BYTEA, + IN in_object_hash BYTEA, + IN in_expiration_date INT8, + IN in_data_blob BYTEA, + OUT out_account_missing BOOLEAN, + OUT out_inserted BOOLEAN) +LANGUAGE plpgsql +AS $$ +BEGIN + out_account_missing := FALSE; + out_inserted := FALSE; + + SELECT a.out_account_missing + INTO out_account_missing + FROM sync_check_account_payment(in_account_pub) AS a; + IF out_account_missing + THEN + RETURN; + END IF; + + INSERT INTO objects + (uid + ,account_pub + ,object_hash + ,expiration_date + ,data_blob) + VALUES + (in_uid + ,in_account_pub + ,in_object_hash + ,in_expiration_date + ,in_data_blob) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + out_inserted := FALSE; + RETURN; + END IF; + + out_inserted := TRUE; +END $$; diff --git a/src/syncdb/syncdb_store_payment_TR.c b/src/syncdb/syncdb_store_payment_TR.c @@ -26,11 +26,13 @@ SYNCDB_store_payment_TR ( const struct SYNC_AccountPublicKeyP *account_pub, const char *order_id, const struct TALER_ClaimTokenP *token, - const struct TALER_Amount *amount) + const struct TALER_Amount *amount, + struct GNUNET_TIME_Relative lifetime) { - enum GNUNET_DB_QueryStatus qs; - struct TALER_ClaimTokenP tok; struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); + /* all-zero claim token means "no claim token" */ + struct TALER_ClaimTokenP tok = { 0 }; + bool inserted; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (account_pub), GNUNET_PQ_query_param_string (order_id), @@ -38,39 +40,46 @@ SYNCDB_store_payment_TR ( GNUNET_PQ_query_param_timestamp (&now), TALER_PQ_query_param_amount (pg->conn, amount), + GNUNET_PQ_query_param_relative_time (&lifetime), + GNUNET_PQ_query_param_timestamp (&now), GNUNET_PQ_query_param_end }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_inserted", + &inserted), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; - if (NULL == token) - memset (&tok, 0, sizeof (tok)); - else + if (NULL != token) tok = *token; SYNCDB_preflight (); - PREPARE ("payment_insert", - "INSERT INTO payments " - "(account_pub" - ",order_id" - ",token" - ",timestamp" - ",amount" - ") VALUES " - "($1,$2,$3,$4,$5);"); - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "payment_insert", - params); + PREPARE ("do_store_payment", + "SELECT" + " out_inserted" + " FROM sync_do_store_payment" + " ($1,$2,$3,$4,$5,$6,$7)"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_store_payment", + params, + rs); switch (qs) { + case GNUNET_DB_STATUS_HARD_ERROR: + return SYNC_DB_HARD_ERROR; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); return SYNC_DB_SOFT_ERROR; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_break (0); - return SYNC_DB_NO_RESULTS; + return SYNC_DB_HARD_ERROR; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (! inserted) + { + GNUNET_break (0); + return SYNC_DB_NO_RESULTS; + } return SYNC_DB_ONE_RESULT; - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; default: GNUNET_break (0); return SYNC_DB_HARD_ERROR; diff --git a/src/syncdb/syncdb_store_payment_TR.sql b/src/syncdb/syncdb_store_payment_TR.sql @@ -0,0 +1,55 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + + +DROP FUNCTION IF EXISTS sync_do_store_payment; +CREATE FUNCTION sync_do_store_payment ( + IN in_account_pub BYTEA, + IN in_order_id TEXT, + IN in_claim_token BYTEA, + IN in_order_timestamp INT8, + IN in_amount taler_amount, + IN in_lifetime_us INT8, + IN in_now_us INT8, + OUT out_inserted BOOLEAN) +LANGUAGE plpgsql +AS $$ +BEGIN + -- Create account if it doesn't exist yet, so the FK + -- from payments.account_pub is satisfied. + INSERT INTO accounts + (account_pub + ,expiration_date) + VALUES + (in_account_pub + ,in_now_us) + ON CONFLICT DO NOTHING; + + -- Store the payment record + INSERT INTO payments + (account_pub + ,order_id + ,claim_token + ,order_timestamp + ,amount) + VALUES + (in_account_pub + ,in_order_id + ,in_claim_token + ,in_order_timestamp + ,in_amount); + out_inserted := TRUE; +END $$; diff --git a/src/syncdb/syncdb_update_backup_TR.c b/src/syncdb/syncdb_update_backup_TR.c @@ -1,108 +0,0 @@ -/* - This file is part of TALER - (C) 2014--2022 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU Lesser General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ -/** - * @file syncdb/syncdb_update_backup_TR.c - * @brief update existing backup - * @author Christian Grothoff - */ -#include "syncdb_pg.h" - - -enum SYNC_DB_QueryStatus -SYNCDB_update_backup_TR ( - const struct SYNC_AccountPublicKeyP *account_pub, - const struct GNUNET_HashCode *old_backup_hash, - const struct SYNC_AccountSignatureP *account_sig, - const struct GNUNET_HashCode *backup_hash, - size_t backup_size, - const void *backup) -{ - uint32_t bs = (uint32_t) backup_size; - bool updated; - bool payment_required; - bool old_backup_missing; - bool old_backup_mismatch; - bool no_change; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_auto_from_type (old_backup_hash), - GNUNET_PQ_query_param_auto_from_type (account_sig), - GNUNET_PQ_query_param_auto_from_type (backup_hash), - GNUNET_PQ_query_param_uint32 (&bs), - GNUNET_PQ_query_param_fixed_size (backup, - backup_size), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("out_updated", - &updated), - GNUNET_PQ_result_spec_bool ("out_payment_required", - &payment_required), - GNUNET_PQ_result_spec_bool ("out_old_backup_missing", - &old_backup_missing), - GNUNET_PQ_result_spec_bool ("out_old_backup_mismatch", - &old_backup_mismatch), - GNUNET_PQ_result_spec_bool ("out_no_change", - &no_change), - GNUNET_PQ_result_spec_end - }; - enum GNUNET_DB_QueryStatus qs; - - SYNCDB_preflight (); - PREPARE ("do_update_backup", - "SELECT" - " out_updated" - ",out_payment_required" - ",out_old_backup_missing" - ",out_old_backup_mismatch" - ",out_no_change" - " FROM sync_do_update_backup" - " ($1,$2,$3,$4,$5,$6);"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "do_update_backup", - params, - rs); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - if (updated) - return SYNC_DB_ONE_RESULT; - if (payment_required) - return SYNC_DB_PAYMENT_REQUIRED; - if (old_backup_missing) - return SYNC_DB_OLD_BACKUP_MISSING; - if (old_backup_mismatch) - return SYNC_DB_OLD_BACKUP_MISMATCH; - if (no_change) - return SYNC_DB_NO_RESULTS; - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - default: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - } -} - - -/* end of syncdb_update_backup_TR.c */ diff --git a/src/syncdb/syncdb_update_backup_TR.sql b/src/syncdb/syncdb_update_backup_TR.sql @@ -1,97 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA --- --- TALER is free software; you can redistribute it and/or modify it under the --- terms of the GNU General Public License as published by the Free Software --- Foundation; either version 3, or (at your option) any later version. --- --- TALER is distributed in the hope that it will be useful, but WITHOUT ANY --- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR --- A PARTICULAR PURPOSE. See the GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License along with --- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> --- - - -DROP FUNCTION IF EXISTS sync_do_update_backup; -CREATE FUNCTION sync_do_update_backup ( - IN in_account_pub BYTEA, - IN in_old_backup_hash BYTEA, - IN in_account_sig BYTEA, - IN in_backup_hash BYTEA, - IN in_backup_size INT4, - IN in_data BYTEA, - OUT out_updated BOOLEAN, - OUT out_payment_required BOOLEAN, - OUT out_old_backup_missing BOOLEAN, - OUT out_old_backup_mismatch BOOLEAN, - OUT out_no_change BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - my_existing_hash BYTEA; -BEGIN - out_updated = FALSE; - out_payment_required = FALSE; - out_old_backup_missing = FALSE; - out_old_backup_mismatch = FALSE; - out_no_change = FALSE; - - -- Try to update the backup (matches on account_pub AND old backup_hash) - UPDATE backups - SET backup_hash=in_backup_hash - ,account_sig=in_account_sig - ,prev_hash=in_old_backup_hash - ,data=in_data - WHERE account_pub=in_account_pub - AND backup_hash=in_old_backup_hash; - - IF FOUND - THEN - out_updated = TRUE; - RETURN; - END IF; - - -- Update failed; check if account exists - PERFORM 1 - FROM accounts - WHERE account_pub=in_account_pub; - - IF NOT FOUND - THEN - out_payment_required = TRUE; - RETURN; - END IF; - - -- Account exists; check existing backup hash - SELECT backup_hash - INTO my_existing_hash - FROM backups - WHERE account_pub=in_account_pub; - - IF NOT FOUND - THEN - out_old_backup_missing = TRUE; - RETURN; - END IF; - - -- Had an existing backup; is the new hash identical? - IF my_existing_hash = in_backup_hash - THEN - out_no_change = TRUE; - RETURN; - END IF; - - -- Check if existing hash matches expected old hash - IF my_existing_hash = in_old_backup_hash - THEN - -- All constraints satisfied but update failed => hard error - -- (this shouldn't normally happen) - RAISE EXCEPTION 'unexpected state: backup hash matches but update failed'; - END IF; - - -- Previous backup does not match old_backup_hash - out_old_backup_mismatch = TRUE; -END $$; diff --git a/src/syncdb/syncdb_update_block_TR.c b/src/syncdb/syncdb_update_block_TR.c @@ -0,0 +1,139 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file syncdb/syncdb_update_block_TR.c + * @brief update contents of a block in the DLL + * @author Iván Ávalos + */ +#include "syncdb_pg.h" + + +enum SYNC_DB_QueryStatus +SYNCDB_update_block_TR ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *block_nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_data_hash, + const struct GNUNET_HashCode *new_data_hash, + size_t data_size, + const void *data, + unsigned int refs_length, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *refs_hash) +{ + bool account_missing; + bool block_missing; + bool outdated_write; + bool negative_refcount; + bool refs_missing; + bool updated; + bool no_change; + bool refs_updated; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (block_nonce), + (NULL != prev_nonce) + ? GNUNET_PQ_query_param_auto_from_type (prev_nonce) + : GNUNET_PQ_query_param_null (), + (NULL != next_nonce) + ? GNUNET_PQ_query_param_auto_from_type (next_nonce) + : GNUNET_PQ_query_param_null (), + GNUNET_PQ_query_param_auto_from_type (old_data_hash), + GNUNET_PQ_query_param_auto_from_type (new_data_hash), + GNUNET_PQ_query_param_fixed_size (data, + data_size), + GNUNET_PQ_query_param_array_auto_from_type (refs_length, + object_uids, + pg->conn), + GNUNET_PQ_query_param_array_uint16 (refs_length, + (const uint16_t *) object_incs, + pg->conn), + GNUNET_PQ_query_param_auto_from_type (upload_sig), + GNUNET_PQ_query_param_auto_from_type (refs_hash), + GNUNET_PQ_query_param_end, + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_account_missing", + &account_missing), + GNUNET_PQ_result_spec_bool ("out_block_missing", + &block_missing), + GNUNET_PQ_result_spec_bool ("out_outdated_write", + &outdated_write), + GNUNET_PQ_result_spec_bool ("out_negative_refcount", + &negative_refcount), + GNUNET_PQ_result_spec_bool ("out_refs_missing", + &refs_missing), + GNUNET_PQ_result_spec_bool ("out_updated", + &updated), + GNUNET_PQ_result_spec_bool ("out_no_change", + &no_change), + GNUNET_PQ_result_spec_bool ("out_refs_updated", + &refs_updated), + GNUNET_PQ_result_spec_end, + }; + enum GNUNET_DB_QueryStatus qs; + + SYNCDB_preflight (); + PREPARE ("do_update_block", + "SELECT" + " out_account_missing" + ",out_block_missing" + ",out_outdated_write" + ",out_negative_refcount" + ",out_refs_missing" + ",out_updated" + ",out_no_change" + ",out_refs_updated" + " FROM sync_do_update_block" + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_update_block", + params, + rs); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return SYNC_DB_SOFT_ERROR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (updated) + return SYNC_DB_ONE_RESULT; + if (account_missing) + return SYNC_DB_PAYMENT_REQUIRED; + if (block_missing) + return SYNC_DB_TARGET_MISSING; + if (outdated_write) + return SYNC_DB_OUTDATED_WRITE; + if (no_change) + return SYNC_DB_NO_RESULTS; + if (negative_refcount || refs_missing) + return SYNC_DB_INCONSISTENT_BACKUP; + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + default: + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; + } +} diff --git a/src/syncdb/syncdb_update_block_TR.sql b/src/syncdb/syncdb_update_block_TR.sql @@ -0,0 +1,144 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +DROP FUNCTION IF EXISTS sync_do_update_block; +CREATE FUNCTION sync_do_update_block ( + IN in_account_pub BYTEA, + IN in_block_nonce BYTEA, + IN in_prev_nonce BYTEA, + IN in_next_nonce BYTEA, + IN in_old_data_hash BYTEA, + IN in_new_data_hash BYTEA, + IN in_data_blob BYTEA, + IN ina_object_uids BYTEA[], + IN ina_object_incs INT2[], + IN in_upload_sig BYTEA, + IN in_refs_hash BYTEA, + OUT out_account_missing BOOLEAN, + OUT out_block_missing BOOLEAN, + OUT out_outdated_write BOOLEAN, + OUT out_negative_refcount BOOLEAN, + OUT out_refs_missing BOOLEAN, + OUT out_updated BOOLEAN, + OUT out_no_change BOOLEAN, + OUT out_refs_updated BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_prev_nonce BYTEA; + my_next_nonce BYTEA; + my_existing_hash BYTEA; +BEGIN + out_account_missing := FALSE; + out_block_missing := FALSE; + out_outdated_write := FALSE; + out_negative_refcount := FALSE; + out_refs_missing := FALSE; + out_updated := FALSE; + out_no_change := FALSE; + out_refs_updated := FALSE; + + -- Check if account exists and has valid payment + SELECT a.out_account_missing + INTO out_account_missing + FROM sync_check_account_payment(in_account_pub) AS a; + IF out_account_missing + THEN + RETURN; + END IF; + + -- Check if old block exists + SELECT prev_nonce + ,next_nonce + ,block_hash + INTO my_prev_nonce + ,my_next_nonce + ,my_existing_hash + FROM blocks + WHERE account_pub = in_account_pub + AND nonce = in_block_nonce; + + IF NOT FOUND + THEN + out_block_missing := TRUE; + RETURN; + END IF; + + -- Is the new hash identical? + IF my_existing_hash = in_new_data_hash + THEN + out_no_change := TRUE; + RETURN; + END IF; + + -- Does existing hash matches expected old hash? + -- Does previous and next nonces match expectations? + IF my_existing_hash IS DISTINCT FROM in_old_data_hash OR + my_prev_nonce IS DISTINCT FROM in_prev_nonce OR + my_next_nonce IS DISTINCT FROM in_next_nonce + THEN + out_outdated_write := TRUE; + RETURN; + END IF; + + -- Check referenced objects exist and no increment underflows, + -- before we touch anything + SELECT c.out_negative_refcount + ,c.out_refs_missing + INTO out_negative_refcount + ,out_refs_missing + FROM sync_check_object_refcounts_pre(in_account_pub, + ina_object_uids, + ina_object_incs) AS c; + IF out_negative_refcount OR out_refs_missing + THEN + RETURN; + END IF; + + -- Try to update existing block in place; the upload signature and + -- its signed context are stored with the block + UPDATE blocks + SET block_hash = in_new_data_hash + ,data_blob = in_data_blob + ,upload_sig = in_upload_sig + ,old_hash = in_old_data_hash + ,refs_hash = in_refs_hash + WHERE account_pub = in_account_pub + AND nonce = in_block_nonce; + + IF NOT FOUND + THEN + RAISE EXCEPTION 'block % vanished for account %', + in_block_nonce, in_account_pub; + END IF; + + -- Update object refcounts and delete unreferenced objects; the + -- pre-check above means this can only fail on a concurrent change + SELECT u.out_negative_refcount + ,u.out_refs_updated + INTO out_negative_refcount + ,out_refs_updated + FROM sync_update_object_refcounts(in_account_pub, + ina_object_uids, + ina_object_incs) AS u; + IF NOT out_refs_updated + THEN + RAISE EXCEPTION 'object refcounts changed concurrently for account %', + in_account_pub; + END IF; + + out_updated := TRUE; +END $$; diff --git a/src/syncdb/test_sync_db.c b/src/syncdb/test_sync_db.c @@ -1,5 +1,5 @@ /* - This file is part of + This file is part of TALER (C) 2019 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file sync/test_sync_db.c + * @file syncdb/test_sync_db.c * @brief testcase for sync postgres db * @author Christian Grothoff */ @@ -34,12 +34,11 @@ } while (0) #define RND_BLK(ptr) \ - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, ptr, sizeof (* \ - ptr)) + GNUNET_CRYPTO_random_block (ptr, sizeof (*ptr)) /** - * Global return value for the test. Initially -1, set to 0 upon - * completion. Other values indicate some kind of error. + * Global return value for the test. Initially #EXIT_FAILURE, set to + * 0 upon completion. Other values indicate some kind of error. */ static int result; @@ -60,6 +59,9 @@ payment_it (void *cls, const struct TALER_ClaimTokenP *token, const struct TALER_Amount *amount) { + (void) timestamp; + (void) token; + (void) amount; GNUNET_assert (NULL == cls); GNUNET_assert (0 == strcmp (order_id, "fake-order-2")); @@ -67,6 +69,113 @@ payment_it (void *cls, /** + * Iterator used by the #SYNCDB_lookup_block_range_TR() tests, + * simply counts the blocks it is called with. + * + * @param cls an `int *` counter to increment + * @param nonce block nonce + * @param block_hash hash of the block data + * @param prev_nonce previous block nonce (NULL if first) + * @param next_nonce next block nonce (NULL if last) + * @param data_size size of @a data + * @param data encrypted block contents + * @param upload_sig signature stored with the block + * @param old_hash hash of the replaced block data + * @param refs_hash hash over the object references + */ +static void +block_it (void *cls, + const struct SYNC_BlockNonce *nonce, + const struct GNUNET_HashCode *block_hash, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + size_t data_size, + const void *data, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *refs_hash) +{ + (void) upload_sig; + (void) old_hash; + (void) refs_hash; + int *cnt = cls; + + (void) nonce; + (void) block_hash; + (void) prev_nonce; + (void) next_nonce; + (void) data_size; + (void) data; + (*cnt)++; +} + + +/** + * Create a paid account, so that the SQL entry points do not reject + * it with #SYNC_DB_PAYMENT_REQUIRED. + * + * @param account_pub account to activate + * @param order_id (unique) order ID to use for the fake payment + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +activate_account (const struct SYNC_AccountPublicKeyP *account_pub, + const char *order_id) +{ + struct TALER_Amount amount; + struct TALER_ClaimTokenP token; + + memset (&token, 3, sizeof (token)); + if (GNUNET_OK != + TALER_string_to_amount ("EUR:1", + &amount)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (SYNC_DB_ONE_RESULT != + SYNCDB_store_payment_TR (account_pub, + order_id, + &token, + &amount, + GNUNET_TIME_UNIT_MINUTES)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (SYNC_DB_ONE_RESULT != + SYNCDB_increment_lifetime_TR (account_pub, + order_id, + GNUNET_TIME_UNIT_MINUTES)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Count the blocks currently stored for @a account_pub. + * + * @param account_pub account to count blocks of + * @return number of blocks found + */ +static int +count_blocks (const struct SYNC_AccountPublicKeyP *account_pub) +{ + int got = 0; + + SYNCDB_lookup_block_range_TR (account_pub, + NULL, + 100, + &block_it, + &got); + return got; +} + + +/** * Main function that will be run by the scheduler. * * @param cls closure with config @@ -77,21 +186,20 @@ run (void *cls) struct GNUNET_CONFIGURATION_Handle *cfg = cls; struct TALER_Amount amount; struct SYNC_AccountPublicKeyP account_pub; - struct SYNC_AccountSignatureP account_sig; - struct SYNC_AccountSignatureP account_sig2; struct GNUNET_HashCode h; struct GNUNET_HashCode h2; - struct GNUNET_HashCode h3; - struct GNUNET_HashCode r; - struct GNUNET_HashCode r2; struct GNUNET_TIME_Absolute ts; struct TALER_ClaimTokenP token; - size_t bs; - void *b = NULL; + /* P1: fake signature/refs for the DB-level tests (the procedures + store, they do not verify) */ + struct SYNC_AccountSignatureP fake_sig; + struct GNUNET_HashCode empty_refs; + + memset (&fake_sig, 7, sizeof (fake_sig)); + GNUNET_CRYPTO_hash ("", 0, &empty_refs); if (GNUNET_OK != - SYNCDB_init (cfg, - true)) + SYNCDB_init_admin (cfg)) { result = 77; return; @@ -111,127 +219,738 @@ run (void *cls) GNUNET_assert (GNUNET_OK == SYNCDB_preflight ()); memset (&account_pub, 1, sizeof (account_pub)); - memset (&account_sig, 2, sizeof (account_sig)); memset (&token, 3, sizeof (token)); GNUNET_CRYPTO_hash ("data", 4, &h); GNUNET_CRYPTO_hash ("DATA", 4, &h2); - GNUNET_CRYPTO_hash ("ATAD", 4, &h3); GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:1", &amount)); FAILIF (SYNC_DB_ONE_RESULT != - SYNCDB_store_payment_TR (&account_pub, - "fake-order", - &token, - &amount)); - FAILIF (SYNC_DB_ONE_RESULT != - SYNCDB_increment_lifetime_TR (&account_pub, - "fake-order", - GNUNET_TIME_UNIT_MINUTES)); + SYNCDB_store_payment_TR ( + &account_pub, + "fake-order", + &token, + &amount, + GNUNET_TIME_UNIT_MINUTES)); FAILIF (SYNC_DB_ONE_RESULT != - SYNCDB_store_backup_TR (&account_pub, - &account_sig, - &h, - 4, - "data")); - FAILIF (SYNC_DB_NO_RESULTS != - SYNCDB_store_backup_TR (&account_pub, - &account_sig, - &h, - 4, - "data")); - FAILIF (SYNC_DB_ONE_RESULT != - SYNCDB_update_backup_TR (&account_pub, - &h, - &account_sig, - &h2, - 4, - "DATA")); - FAILIF (SYNC_DB_OLD_BACKUP_MISMATCH != - SYNCDB_update_backup_TR (&account_pub, - &h, - &account_sig, - &h3, - 4, - "ATAD")); - FAILIF (SYNC_DB_NO_RESULTS != - SYNCDB_update_backup_TR (&account_pub, - &h, - &account_sig, - &h2, - 4, - "DATA")); - FAILIF (SYNC_DB_ONE_RESULT != - SYNCDB_lookup_account_TR (&account_pub, - &r)); - FAILIF (0 != GNUNET_memcmp (&r, - &h2)); - FAILIF (SYNC_DB_ONE_RESULT != - SYNCDB_lookup_backup_TR (&account_pub, - &account_sig2, - &r, - &r2, - &bs, - &b)); - FAILIF (0 != GNUNET_memcmp (&r, - &h)); - FAILIF (0 != GNUNET_memcmp (&r2, - &h2)); - FAILIF (0 != GNUNET_memcmp (&account_sig2, - &account_sig)); - FAILIF (bs != 4); - FAILIF (0 != memcmp (b, - "DATA", - 4)); - GNUNET_free (b); - b = NULL; + SYNCDB_increment_lifetime_TR ( + &account_pub, + "fake-order", + GNUNET_TIME_UNIT_MINUTES)); + FAILIF (0 != - SYNCDB_lookup_pending_payments_by_account_TR (&account_pub, - &payment_it, - NULL)); + SYNCDB_lookup_pending_payments_by_account_TR ( + &account_pub, + &payment_it, + NULL)); memset (&account_pub, 2, sizeof (account_pub)); FAILIF (SYNC_DB_ONE_RESULT != - SYNCDB_store_payment_TR (&account_pub, - "fake-order-2", - &token, - &amount)); + SYNCDB_store_payment_TR ( + &account_pub, + "fake-order-2", + &token, + &amount, + GNUNET_TIME_UNIT_MINUTES)); FAILIF (1 != - SYNCDB_lookup_pending_payments_by_account_TR (&account_pub, - &payment_it, - NULL)); - FAILIF (SYNC_DB_PAYMENT_REQUIRED != - SYNCDB_store_backup_TR (&account_pub, - &account_sig, - &h, - 4, - "data")); + SYNCDB_lookup_pending_payments_by_account_TR ( + &account_pub, + &payment_it, + NULL)); FAILIF (SYNC_DB_ONE_RESULT != - SYNCDB_increment_lifetime_TR (&account_pub, - "fake-order-2", - GNUNET_TIME_UNIT_MINUTES)); - FAILIF (SYNC_DB_OLD_BACKUP_MISSING != - SYNCDB_update_backup_TR (&account_pub, - &h, - &account_sig, - &h2, - 4, - "DATA")); + SYNCDB_increment_lifetime_TR ( + &account_pub, + "fake-order-2", + GNUNET_TIME_UNIT_MINUTES)); + /* --- DLL tests: append three blocks A->B->C and validate lookup --- */ + { + struct SYNC_BlockNonce nA; + struct SYNC_BlockNonce nB; + struct SYNC_BlockNonce nC; + struct GNUNET_HashCode hA; + struct GNUNET_HashCode hB; + struct GNUNET_HashCode hC; + char dataA[] = "A"; + char dataB[] = "B"; + char dataC[] = "C"; + int got; + + RND_BLK (&nA); + RND_BLK (&nB); + RND_BLK (&nC); + GNUNET_CRYPTO_hash (dataA, sizeof dataA - 1, &hA); + GNUNET_CRYPTO_hash (dataB, sizeof dataB - 1, &hB); + GNUNET_CRYPTO_hash (dataC, sizeof dataC - 1, &hC); + + /* Append A (prev NULL) */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_append_block_TR ( + &account_pub, + &nA, + NULL, + &hA, + sizeof dataA - 1, + dataA, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + + + /* Append B (prev A) */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_append_block_TR ( + &account_pub, + &nB, + &nA, + &hB, + sizeof dataB - 1, + dataB, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs, + &fake_sig, + &hA, + NULL)); + + + /* Append C (prev B) */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_append_block_TR ( + &account_pub, + &nC, + &nB, + &hC, + sizeof dataC - 1, + dataC, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs, + &fake_sig, + &hB, + &nA)); + + /* Verify lookup returns 3 blocks */ + got = 0; + FAILIF (3 != + SYNCDB_lookup_block_range_TR ( + &account_pub, + &nA, + 10, + &block_it, + &got)); + FAILIF (got != 3); + + /* The relink signature covers the neighbour's other link, so an + append whose view of it went stale must be refused rather than + stored (the caller reads the neighbour outside the transaction) */ + { + struct SYNC_BlockNonce nS; + struct GNUNET_HashCode hS; + char dataS[] = "S"; + + RND_BLK (&nS); + GNUNET_CRYPTO_hash (dataS, sizeof dataS - 1, &hS); + FAILIF (SYNC_DB_OUTDATED_WRITE != + SYNCDB_append_block_TR ( + &account_pub, + &nS, + &nC, + &hS, + sizeof dataS - 1, + dataS, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs, + &fake_sig, + &hC, + /* C's predecessor is B, not A */ &nA)); + FAILIF (3 != count_blocks (&account_pub)); + } + + /* Delete middle block B (use its prev and next) */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_delete_block_TR ( + &account_pub, + &nB, + &nA, + &nC, + &hB, + 0, + NULL, + NULL, + &fake_sig, + &hA, + NULL, + &fake_sig, + &hC, + NULL)); + + /* Update block C (test update path) */ + { + struct GNUNET_HashCode newC; + char newDataC[] = "C-new"; + + GNUNET_CRYPTO_hash (newDataC, sizeof newDataC - 1, &newC); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_update_block_TR ( + &account_pub, + &nC, + /* prev */ &nA, + /* next */ NULL, + &hC, + &newC, + sizeof newDataC - 1, + newDataC, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs)); + hC = newC; + } + + /* --- Outdated-write tests --- */ + { + struct SYNC_BlockNonce nD; + struct GNUNET_HashCode hD; + char dataD[] = "D"; + struct GNUNET_HashCode bogus_old; + char newDataC2[] = "C-newer"; + struct GNUNET_HashCode newC2; + + RND_BLK (&nD); + GNUNET_CRYPTO_hash (dataD, sizeof dataD - 1, &hD); + + /* Appending with NULL prev when backup is non-empty should be outdated */ + FAILIF (SYNC_DB_OUTDATED_WRITE != + SYNCDB_append_block_TR ( + &account_pub, + &nD, + /* prev */ NULL, + &hD, + sizeof dataD - 1, + dataD, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + + /* Updating C with wrong old-data-hash should be outdated */ + GNUNET_CRYPTO_hash ("bogus", 5, &bogus_old); + GNUNET_CRYPTO_hash (newDataC2, sizeof newDataC2 - 1, &newC2); + FAILIF (SYNC_DB_OUTDATED_WRITE != + SYNCDB_update_block_TR ( + &account_pub, + &nC, + /* prev */ &nA, + /* next */ NULL, + &bogus_old, + &newC2, + sizeof newDataC2 - 1, + newDataC2, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs)); + + /* Deleting C with wrong prev nonce should be outdated */ + FAILIF (SYNC_DB_OUTDATED_WRITE != + SYNCDB_delete_block_TR ( + &account_pub, + &nC, + /* prev (wrong) */ &nB, + /* next */ NULL, + &hC, + 0, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL)); + } + } + + /* --- Account status lookup --- */ + { + struct SYNC_AccountPublicKeyP acc; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode bh; + char data[] = "account-status-block"; + struct GNUNET_TIME_Timestamp expiration; + uint64_t storage_used_bytes; + uint64_t block_count; + + memset (&acc, 20, sizeof (acc)); + + /* An account nobody has heard of is missing, not expired */ + FAILIF (SYNC_DB_TARGET_MISSING != + SYNCDB_lookup_account_TR (&acc, + &expiration, + &storage_used_bytes, + &block_count)); + + FAILIF (GNUNET_OK != + activate_account (&acc, + "fake-order-account-status")); + + /* A fresh account has an expiration and no storage in use */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_lookup_account_TR (&acc, + &expiration, + &storage_used_bytes, + &block_count)); + FAILIF (GNUNET_TIME_absolute_is_zero (expiration.abs_time)); + FAILIF (0 != storage_used_bytes); + FAILIF (0 != block_count); + + /* ... and the block it stores is reflected in both counters */ + RND_BLK (&nonce); + GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_append_block_TR (&acc, + &nonce, + NULL, + &bh, + sizeof data - 1, + data, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_lookup_account_TR (&acc, + &expiration, + &storage_used_bytes, + &block_count)); + FAILIF (sizeof data - 1 != storage_used_bytes); + FAILIF (1 != block_count); + + /* The blocks of other accounts must not be counted here */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_lookup_account_TR (&account_pub, + &expiration, + &storage_used_bytes, + &block_count)); + FAILIF (1 == block_count); + } + + /* --- Object store/lookup tests --- */ + { + struct SYNC_ObjectUID obj_uid; + struct GNUNET_HashCode obj_hash; + char obj_data[] = "objdata"; + int64_t refcount = 0; + struct GNUNET_TIME_Timestamp exp; + size_t data_sz = 0; + + RND_BLK (&obj_uid); + GNUNET_CRYPTO_hash (obj_data, sizeof obj_data - 1, &obj_hash); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_store_object_TR ( + &obj_uid, + &account_pub, + &obj_hash, + GNUNET_TIME_UNIT_DAYS, + sizeof obj_data - 1, + obj_data)); + + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_lookup_object ( + &account_pub, + &obj_hash, + &refcount, + &exp, + &data_sz, + NULL)); + FAILIF (refcount < 0); + + /* A missing object is a normal outcome, not a failure */ + { + struct GNUNET_HashCode bogus_hash; + + GNUNET_CRYPTO_hash ("no-such-object", + 14, + &bogus_hash); + FAILIF (SYNC_DB_NO_RESULTS != + SYNCDB_lookup_object (&account_pub, + &bogus_hash, + &refcount, + &exp, + &data_sz, + NULL)); + } + } + + /* --- Referencing an object that was never uploaded --- */ + { + struct SYNC_AccountPublicKeyP acc; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode bh; + struct SYNC_ObjectUID unknown_uid; + const int16_t inc = 1; + char data[] = "orphan"; + + memset (&acc, 10, sizeof (acc)); + FAILIF (GNUNET_OK != + activate_account (&acc, + "fake-order-refs-missing")); + RND_BLK (&nonce); + RND_BLK (&unknown_uid); + GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh); + FAILIF (SYNC_DB_INCONSISTENT_BACKUP != + SYNCDB_append_block_TR (&acc, + &nonce, + NULL, + &bh, + sizeof data - 1, + data, + 1, + &unknown_uid, + &inc, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + /* the rejected append must be a no-op */ + FAILIF (0 != count_blocks (&acc)); + } + + /* --- A nonce that is already taken is a conflict --- */ + { + struct SYNC_AccountPublicKeyP acc_a; + struct SYNC_AccountPublicKeyP acc_b; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode bh; + char data[] = "shared-nonce"; + + memset (&acc_a, 11, sizeof (acc_a)); + memset (&acc_b, 12, sizeof (acc_b)); + FAILIF (GNUNET_OK != + activate_account (&acc_a, + "fake-order-nonce-a")); + FAILIF (GNUNET_OK != + activate_account (&acc_b, + "fake-order-nonce-b")); + RND_BLK (&nonce); + GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_append_block_TR (&acc_a, + &nonce, + NULL, + &bh, + sizeof data - 1, + data, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + FAILIF (SYNC_DB_OUTDATED_WRITE != + SYNCDB_append_block_TR (&acc_b, + &nonce, + NULL, + &bh, + sizeof data - 1, + data, + 0, + NULL, + NULL, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + /* the rejected append must not have touched account B */ + FAILIF (0 != count_blocks (&acc_b)); + } + + /* --- Refcount lifecycle --- */ + { + struct SYNC_AccountPublicKeyP acc; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode bh; + struct SYNC_ObjectUID uid; + struct GNUNET_HashCode oh; + const int16_t inc = 1; + const int16_t dec = -1; + const int16_t dec2 = -2; + char data[] = "refcounted"; + char obj[] = "payload"; + int64_t refcount = 0; + struct GNUNET_TIME_Timestamp exp; + size_t data_sz = 0; + + memset (&acc, 13, sizeof (acc)); + FAILIF (GNUNET_OK != + activate_account (&acc, + "fake-order-refcount")); + RND_BLK (&nonce); + RND_BLK (&uid); + GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh); + GNUNET_CRYPTO_hash (obj, sizeof obj - 1, &oh); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_store_object_TR (&uid, + &acc, + &oh, + GNUNET_TIME_UNIT_DAYS, + sizeof obj - 1, + obj)); + /* re-uploading the same UID is idempotent, not an error */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_store_object_TR (&uid, + &acc, + &oh, + GNUNET_TIME_UNIT_DAYS, + sizeof obj - 1, + obj)); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_append_block_TR (&acc, + &nonce, + NULL, + &bh, + sizeof data - 1, + data, + 1, + &uid, + &inc, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_lookup_object (&acc, + &oh, + &refcount, + &exp, + &data_sz, + NULL)); + FAILIF (1 != refcount); + + /* decrementing below zero is refused, refcount untouched */ + FAILIF (SYNC_DB_INCONSISTENT_BACKUP != + SYNCDB_delete_block_TR (&acc, + &nonce, + NULL, + NULL, + &bh, + 1, + &uid, + &dec2, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL)); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_lookup_object (&acc, + &oh, + &refcount, + &exp, + &data_sz, + NULL)); + FAILIF (1 != refcount); + /* ... and the block must still be there */ + FAILIF (1 != count_blocks (&acc)); + + /* the matching decrement drops it to 0, deleting the object */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_delete_block_TR (&acc, + &nonce, + NULL, + NULL, + &bh, + 1, + &uid, + &dec, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL)); + FAILIF (SYNC_DB_NO_RESULTS != + SYNCDB_lookup_object (&acc, + &oh, + &refcount, + &exp, + &data_sz, + NULL)); + FAILIF (0 != count_blocks (&acc)); + } + + /* --- Increments for the same UID are aggregated --- */ + { + struct SYNC_AccountPublicKeyP acc; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode bh; + struct SYNC_ObjectUID uids[2]; + struct GNUNET_HashCode oh; + const int16_t incs[2] = { -1, 1 }; + char data[] = "net-zero"; + char obj[] = "aggregated"; + int64_t refcount = 0; + struct GNUNET_TIME_Timestamp exp; + size_t data_sz = 0; + + memset (&acc, 14, sizeof (acc)); + FAILIF (GNUNET_OK != + activate_account (&acc, + "fake-order-aggregate")); + RND_BLK (&nonce); + RND_BLK (&uids[0]); + uids[1] = uids[0]; + GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh); + GNUNET_CRYPTO_hash (obj, sizeof obj - 1, &oh); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_store_object_TR (&uids[0], + &acc, + &oh, + GNUNET_TIME_UNIT_DAYS, + sizeof obj - 1, + obj)); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_append_block_TR (&acc, + &nonce, + NULL, + &bh, + sizeof data - 1, + data, + 2, + uids, + incs, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + /* refcount unchanged, and the object still exists */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_lookup_object (&acc, + &oh, + &refcount, + &exp, + &data_sz, + NULL)); + FAILIF (0 != refcount); + } + + /* --- GC must not remove still-referenced objects --- */ + { + struct SYNC_AccountPublicKeyP acc; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode bh; + struct SYNC_ObjectUID kept_uid; + struct SYNC_ObjectUID swept_uid; + struct GNUNET_HashCode kept_oh; + struct GNUNET_HashCode swept_oh; + const int16_t inc = 1; + char data[] = "gc-block"; + char kept[] = "still-referenced"; + char swept[] = "unreferenced"; + int64_t refcount = 0; + struct GNUNET_TIME_Timestamp exp; + size_t data_sz = 0; + struct GNUNET_TIME_Absolute future; + + memset (&acc, 15, sizeof (acc)); + FAILIF (GNUNET_OK != + activate_account (&acc, + "fake-order-gc")); + RND_BLK (&nonce); + RND_BLK (&kept_uid); + RND_BLK (&swept_uid); + GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh); + GNUNET_CRYPTO_hash (kept, sizeof kept - 1, &kept_oh); + GNUNET_CRYPTO_hash (swept, sizeof swept - 1, &swept_oh); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_store_object_TR (&kept_uid, + &acc, + &kept_oh, + GNUNET_TIME_UNIT_DAYS, + sizeof kept - 1, + kept)); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_store_object_TR (&swept_uid, + &acc, + &swept_oh, + GNUNET_TIME_UNIT_DAYS, + sizeof swept - 1, + swept)); + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_append_block_TR (&acc, + &nonce, + NULL, + &bh, + sizeof data - 1, + data, + 1, + &kept_uid, + &inc, + &fake_sig, + &empty_refs, + NULL, + NULL, + NULL)); + /* Leave accounts and payments alone, only sweep objects */ + future = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_YEARS); + FAILIF (GNUNET_OK != + SYNCDB_gc (GNUNET_TIME_UNIT_ZERO_ABS, + GNUNET_TIME_UNIT_ZERO_ABS, + future)); + /* the referenced object survives ... */ + FAILIF (SYNC_DB_ONE_RESULT != + SYNCDB_lookup_object (&acc, + &kept_oh, + &refcount, + &exp, + &data_sz, + NULL)); + FAILIF (1 != refcount); + /* ... the unreferenced one is collected */ + FAILIF (SYNC_DB_NO_RESULTS != + SYNCDB_lookup_object (&acc, + &swept_oh, + &refcount, + &exp, + &data_sz, + NULL)); + } + ts = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_YEARS); - FAILIF (0 > + FAILIF (GNUNET_OK != SYNCDB_gc (ts, + ts, ts)); - memset (&account_pub, 1, sizeof (account_pub)); - FAILIF (SYNC_DB_NO_RESULTS != - SYNCDB_lookup_backup_TR (&account_pub, - &account_sig2, - &r, - &r2, - &bs, - &b)); - result = 0; drop: - GNUNET_free (b); GNUNET_break (GNUNET_OK == SYNCDB_drop_tables ()); SYNCDB_fini (); @@ -244,39 +963,36 @@ main (int argc, { const char *plugin_name; char *config_filename; - char *testname; struct GNUNET_CONFIGURATION_Handle *cfg; + (void) argc; result = EXIT_FAILURE; + /* the database backend is the suffix of our own binary name */ if (NULL == (plugin_name = strrchr (argv[0], (int) '-'))) { GNUNET_break (0); return EXIT_FAILURE; } + plugin_name++; GNUNET_log_setup (argv[0], "DEBUG", NULL); - plugin_name++; - (void) GNUNET_asprintf (&testname, - "%s", - plugin_name); - (void) GNUNET_asprintf (&config_filename, - "test_sync_db_%s.conf", - testname); + GNUNET_asprintf (&config_filename, + "test_sync_db_%s.conf", + plugin_name); cfg = GNUNET_CONFIGURATION_create (SYNC_project_data ()); if (GNUNET_OK != GNUNET_CONFIGURATION_parse (cfg, config_filename)) { GNUNET_break (0); + GNUNET_CONFIGURATION_destroy (cfg); GNUNET_free (config_filename); - GNUNET_free (testname); return EXIT_NOTCONFIGURED; } GNUNET_SCHEDULER_run (&run, cfg); GNUNET_CONFIGURATION_destroy (cfg); GNUNET_free (config_filename); - GNUNET_free (testname); return result; } diff --git a/src/testing/meson.build b/src/testing/meson.build @@ -2,15 +2,21 @@ libsynctesting = library( 'synctesting', [ - 'testing_api_cmd_backup_download.c', - 'testing_api_cmd_backup_upload.c', 'testing_api_traits.c', + 'testing_api_cmd_block_common.c', + 'testing_api_cmd_block_append.c', + 'testing_api_cmd_block_update.c', + 'testing_api_cmd_block_delete.c', + 'testing_api_cmd_block_list.c', + 'testing_api_cmd_object_upload.c', + 'testing_api_cmd_object_fetch.c', ], soversion: solibversions['libsynctesting']['soversion'], version: solibversions['libsynctesting']['soversion'], install_rpath: rpath_option, dependencies: [ libsync_dep, + libsyncutil_dep, talermerchant_dep, talerutil_dep, talerexchange_dep, @@ -41,6 +47,7 @@ test_sync_api = executable( dependencies: [ libsync_dep, libsynctesting_dep, + libsyncutil_dep, talerutil_dep, talerjson_dep, talermerchant_dep, @@ -71,3 +78,4 @@ configure_file( ) subdir('test_sync_api_home' / '.local' / 'share' / 'taler' / 'exchange-offline') + diff --git a/src/testing/test_sync_api.c b/src/testing/test_sync_api.c @@ -17,7 +17,7 @@ <http://www.gnu.org/licenses/> */ /** - * @file lib/test_sync_api.c + * @file testing/test_sync_api.c * @brief testcase to test sync's HTTP API interface * @author Christian Grothoff */ @@ -35,6 +35,8 @@ #include <taler/taler_error_codes.h> #include "sync/sync_service.h" #include "sync/sync_testing_lib.h" +#include "sync_util.h" + /** * Configuration file we use. One (big) configuration is used @@ -43,16 +45,6 @@ #define CONFIG_FILE "test_sync_api.conf" /** - * Exchange base URL. Could also be taken from config. - */ -#define EXCHANGE_URL "http://localhost:8081/" - -/** - * Account number of the exchange at the bank. - */ -#define EXCHANGE_ACCOUNT_NAME "2" - -/** * Account number of some user. */ #define USER_ACCOUNT_NAME "62" @@ -68,11 +60,6 @@ static struct TALER_FullPayto payer_payto; /** - * Payto URI of the exchange (escrow account). - */ -static struct TALER_FullPayto exchange_payto; - -/** * Payto URI of the merchant (receiver). */ static struct TALER_FullPayto merchant_payto; @@ -131,133 +118,467 @@ cmd_transfer_to_exchange (const char *label, * run. * * @param cls closure + * @param is interpreter state */ static void run (void *cls, struct TALER_TESTING_Interpreter *is) { - struct TALER_TESTING_Command commands[] = { - TALER_TESTING_cmd_system_start ("start-taler", - CONFIG_FILE, - "-efms", - "-u", "exchange-account-exchange", - "-r", "merchant-exchange-default", - NULL), - TALER_TESTING_cmd_get_exchange ("get-exchange", - cred.cfg, - NULL, - true, - true), - TALER_TESTING_cmd_merchant_post_instances ("instance-create-admin", - merchant_url, - "admin", - MHD_HTTP_NO_CONTENT), - TALER_TESTING_cmd_merchant_post_account ( - "instance-create-default-account", - merchant_url, - merchant_payto, - NULL, NULL, - MHD_HTTP_OK), - - /** - * Move money to the exchange's bank account. - */ - cmd_transfer_to_exchange ("create-reserve-1", - "EUR:10.02"), - /** - * Make a reserve exist, according to the previous - * transfer. - */ - cmd_exec_wirewatch ("wirewatch-1"), - TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-1", - "create-reserve-1", - "EUR:5", - 0, - MHD_HTTP_OK), - TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2", - "create-reserve-1", - "EUR:5", - 0, - MHD_HTTP_OK), - /* Failed download: no backup exists */ - SYNC_TESTING_cmd_backup_nx ("backup-download-nx", - sync_url), - /* Failed upload: need to pay */ - SYNC_TESTING_cmd_backup_upload ("backup-upload-1", - sync_url, - NULL /* prev upload */, - NULL /* last upload */, - SYNC_TESTING_UO_NONE, - MHD_HTTP_PAYMENT_REQUIRED, - "Test-1", - strlen ("Test-1")), - /* what would we have to pay? */ - TALER_TESTING_cmd_merchant_claim_order ("fetch-proposal", + /* Payload of the blocks we upload. data[0] is the initial content of + block A, which is later updated to data[1]; hashes[] holds the + hashes of the *final* content of blocks A, C and D. */ + static const char *const data[] = { + "Hello World", + "Block B data", + "Block C data", + "Block D data" + }; + static const char obj_data_A[] = "object-A-data"; + static const char obj_data_B[] = "object-B-data"; + /* Nonces of blocks A, C and D, in chain order. */ + struct SYNC_BlockNonce nonces[3]; + /* Hashes of blocks A, C and D, in chain order. */ + struct GNUNET_HashCode hashes[3]; + /* Nonce of a block that is never successfully uploaded. */ + struct SYNC_BlockNonce nonce_stale; + /* Nonce that never exists on the server. */ + struct SYNC_BlockNonce bogus_nonce; + /* Nonces of the two blocks used for the refcount tests. */ + struct SYNC_BlockNonce ref_nonce; + struct SYNC_BlockNonce ref_nonce2; + /* Nonce of a block whose append is rejected for a bad object ref. */ + struct SYNC_BlockNonce orphan_nonce; + /* Nonce of the block appended after that rejected append. */ + struct SYNC_BlockNonce tail_nonce; + /* UIDs of the two objects used for the refcount tests. */ + struct SYNC_ObjectUID obj_uids[2]; + /* UID of an object that is never uploaded. */ + struct SYNC_ObjectUID unknown_uid; + const int16_t ref_incs[2] = { 1, 1 }; + const int16_t ref_decs[2] = { -1, -1 }; + const int16_t ref_inc1[1] = { 1 }; + const int16_t ref_dec2[1] = { -2 }; + + (void) cls; + for (unsigned int i = 0; i < 3; i++) + SYNC_block_nonce_generate (&nonces[i]); + SYNC_block_nonce_generate (&nonce_stale); + SYNC_block_nonce_generate (&bogus_nonce); + SYNC_block_nonce_generate (&ref_nonce); + SYNC_block_nonce_generate (&ref_nonce2); + SYNC_block_nonce_generate (&orphan_nonce); + SYNC_block_nonce_generate (&tail_nonce); + for (unsigned int i = 0; i < 2; i++) + GNUNET_CRYPTO_random_block (&obj_uids[i], + sizeof (obj_uids[i])); + GNUNET_CRYPTO_random_block (&unknown_uid, + sizeof (unknown_uid)); + for (unsigned int i = 0; i < 3; i++) + GNUNET_CRYPTO_hash (data[i + 1], + strlen (data[i + 1]), + &hashes[i]); + + { + struct TALER_TESTING_Command commands[] = { + TALER_TESTING_cmd_system_start ("start-taler", + CONFIG_FILE, + "-efms", + "-u", "exchange-account-exchange", + "-r", "merchant-exchange-default", + NULL), + TALER_TESTING_cmd_get_exchange ("get-exchange", + cred.cfg, + NULL, + true, + true), + TALER_TESTING_cmd_merchant_post_instances ("instance-create-admin", + merchant_url, + "admin", + MHD_HTTP_NO_CONTENT), + TALER_TESTING_cmd_merchant_post_account ( + "instance-create-default-account", + merchant_url, + merchant_payto, + NULL, NULL, + MHD_HTTP_OK), + + /** + * Move money to the exchange's bank account. + */ + cmd_transfer_to_exchange ("create-reserve-1", + "EUR:10.02"), + /** + * Make a reserve exist, according to the previous + * transfer. + */ + cmd_exec_wirewatch ("wirewatch-1"), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-1", + "create-reserve-1", + "EUR:5", + 0, + MHD_HTTP_OK), + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2", + "create-reserve-1", + "EUR:5", + 0, + MHD_HTTP_OK), + + /* Upload first block; no lifetime yet → 402 Payment Required */ + SYNC_TESTING_cmd_block_append ("upload-block-1", + sync_url, + NULL, + &nonces[0], + MHD_HTTP_PAYMENT_REQUIRED, + data[0], + strlen (data[0]), + true, + 0, + NULL, + NULL), + /* Claim the order from the merchant */ + TALER_TESTING_cmd_merchant_claim_order ("fetch-proposal", + merchant_url, + MHD_HTTP_OK, + "upload-block-1", + NULL), + /* Pay with one withdrawn coin */ + TALER_TESTING_cmd_merchant_pay_order ("pay-account", merchant_url, MHD_HTTP_OK, - "backup-upload-1", - NULL), - /* make the payment */ - TALER_TESTING_cmd_merchant_pay_order ("pay-account", - merchant_url, - MHD_HTTP_OK, - "fetch-proposal", - "withdraw-coin-1", - "EUR:5", - "EUR:4.99", /* must match ANNUAL_FEE in config! */ - "session-id"), - /* now upload should succeed */ - SYNC_TESTING_cmd_backup_upload ("backup-upload-2", - sync_url, - "backup-upload-1", - NULL, - SYNC_TESTING_UO_NONE, - MHD_HTTP_NO_CONTENT, - "Test-1", - strlen ("Test-1")), - /* now updated upload should succeed */ - SYNC_TESTING_cmd_backup_upload ("backup-upload-3", - sync_url, - "backup-upload-2", - NULL, - SYNC_TESTING_UO_NONE, - MHD_HTTP_NO_CONTENT, - "Test-3", - strlen ("Test-3")), - /* Test download: succeeds! */ - SYNC_TESTING_cmd_backup_download ("download-3", + "fetch-proposal", + "withdraw-coin-1", + "EUR:5", + "EUR:4.99", + "session-id"), + /* Retry same upload; server finds paid order → 204 */ + SYNC_TESTING_cmd_block_append ("upload-block-2", + sync_url, + "upload-block-1", + &nonces[0], + MHD_HTTP_NO_CONTENT, + data[0], + strlen (data[0]), + true, + 0, + NULL, + NULL), + /* Update the existing block with different content */ + SYNC_TESTING_cmd_block_update ("upload-block-B", + sync_url, + "upload-block-2", + &nonces[0], + NULL, + MHD_HTTP_NO_CONTENT, + data[1], + strlen (data[1]), + 0, + NULL, + NULL), + /* Append a new block C at the end of block B */ + SYNC_TESTING_cmd_block_append ("upload-block-C", + sync_url, + "upload-block-B", + &nonces[1], + MHD_HTTP_NO_CONTENT, + data[2], + strlen (data[2]), + false, + 0, + NULL, + NULL), + /* List blocks for the account */ + SYNC_TESTING_cmd_block_list ("list-blocks", + sync_url, + "upload-block-2", + 10, + NULL, + MHD_HTTP_OK, + 2, + nonces, + hashes), + + /* Append with stale prev_nonce (nonces[0]), + but last_block is now nonces[1] → 409 */ + SYNC_TESTING_cmd_block_append ("append-stale-prev", + sync_url, + "upload-block-B", + &nonce_stale, + MHD_HTTP_CONFLICT, + data[0], + strlen (data[0]), + false, + 0, + NULL, + NULL), + /* Update with stale old_data_hash + (references upload-block-2 whose curr_hash is hash("Hello World"), + but block now has hash("Block B data")) → 409 */ + SYNC_TESTING_cmd_block_update ("update-conflict", + sync_url, + "upload-block-2", + &nonces[0], + NULL, + MHD_HTTP_CONFLICT, + data[2], + strlen (data[2]), + 0, + NULL, + NULL), + /* Re-upload same data → 304 Not Modified */ + SYNC_TESTING_cmd_block_append ("reupload-same-data", + sync_url, + "upload-block-B", + &nonces[0], + MHD_HTTP_NOT_MODIFIED, + data[1], + strlen (data[1]), + true, + 0, + NULL, + NULL), + /* List with non-existent start_nonce → 404 */ + SYNC_TESTING_cmd_block_list ("list-bogus-start", + sync_url, + "upload-block-2", + 10, + &bogus_nonce, + MHD_HTTP_NOT_FOUND, + 0, + NULL, + NULL), + /* Final list confirms error cases left state intact */ + SYNC_TESTING_cmd_block_list ("list-blocks-final", + sync_url, + "upload-block-2", + 10, + NULL, + MHD_HTTP_OK, + 2, + nonces, + hashes), + + /* ========== Delete block tests ========== */ + + /* Upload a third block D */ + SYNC_TESTING_cmd_block_append ("upload-block-D", + sync_url, + "upload-block-C", + &nonces[2], + MHD_HTTP_NO_CONTENT, + data[3], + strlen (data[3]), + false, + 0, + NULL, + NULL), + /* List all 3 blocks */ + SYNC_TESTING_cmd_block_list ("list-3-blocks", + sync_url, + "upload-block-D", + 10, + NULL, + MHD_HTTP_OK, + 3, + nonces, + hashes), + /* Delete block D */ + SYNC_TESTING_cmd_block_delete ("delete-block-D", + sync_url, + "upload-block-D", + MHD_HTTP_NO_CONTENT, + 0, + NULL, + NULL), + /* List: only A and B remain */ + SYNC_TESTING_cmd_block_list ("list-after-delete-D", + sync_url, + "upload-block-2", + 10, + NULL, + MHD_HTTP_OK, + 2, + nonces, + hashes), + /* Delete block D again → 404 (already gone) */ + SYNC_TESTING_cmd_block_delete ("delete-block-D-again", + sync_url, + "upload-block-D", + MHD_HTTP_NOT_FOUND, + 0, + NULL, + NULL), + /* Final list confirms all is well */ + SYNC_TESTING_cmd_block_list ("list-blocks-end", + sync_url, + "upload-block-2", + 10, + NULL, + MHD_HTTP_OK, + 2, + nonces, + hashes), + + /* ========== Object store and refcount tests ========== */ + + /* Upload object A */ + SYNC_TESTING_cmd_object_upload ("upload-obj-A", sync_url, - MHD_HTTP_OK, - "backup-upload-3"), - /* now updated upload should fail (conflict) */ - SYNC_TESTING_cmd_backup_upload ("backup-upload-3b", - sync_url, - "backup-upload-2", - "backup-upload-3", - SYNC_TESTING_UO_NONE, - MHD_HTTP_CONFLICT, - "Test-3b", - strlen ("Test-3b")), - /* now updated upload should fail (payment requested) */ - SYNC_TESTING_cmd_backup_upload ("backup-upload-4", - sync_url, - "backup-upload-3", - "backup-upload-3", - SYNC_TESTING_UO_REQUEST_PAYMENT, - MHD_HTTP_PAYMENT_REQUIRED, - "Test-4", - strlen ("Test-4")), - /* Test download: previous did NOT change the data on the server! */ - SYNC_TESTING_cmd_backup_download ("download-3b", + "upload-block-2", + &obj_uids[0], + MHD_HTTP_NO_CONTENT, + obj_data_A, + sizeof obj_data_A - 1), + /* Upload object B */ + SYNC_TESTING_cmd_object_upload ("upload-obj-B", sync_url, - MHD_HTTP_OK, - "backup-upload-3"), + "upload-block-2", + &obj_uids[1], + MHD_HTTP_NO_CONTENT, + obj_data_B, + sizeof obj_data_B - 1), - TALER_TESTING_cmd_end () - }; + /* Fetch object A and verify content and hash */ + SYNC_TESTING_cmd_object_fetch ("fetch-obj-A", + sync_url, + "upload-block-2", + &obj_uids[0], + MHD_HTTP_OK, + obj_data_A, + sizeof obj_data_A - 1), + + /* Fetch object B and verify content and hash */ + SYNC_TESTING_cmd_object_fetch ("fetch-obj-B", + sync_url, + "upload-block-2", + &obj_uids[1], + MHD_HTTP_OK, + obj_data_B, + sizeof obj_data_B - 1), + + /* Try uploading a duplicate UID → idempotent (204) */ + SYNC_TESTING_cmd_object_upload ("upload-obj-A-dup", + sync_url, + "upload-block-2", + &obj_uids[0], + MHD_HTTP_NO_CONTENT, + obj_data_B, + sizeof obj_data_B - 1), + + /* Upload a block that references objects A (+1) and B (+1) */ + SYNC_TESTING_cmd_block_append ("upload-block-refs", + sync_url, + "upload-block-C", + &ref_nonce, + MHD_HTTP_NO_CONTENT, + data[0], + strlen (data[0]), + false, + 2, + obj_uids, + ref_incs), + + /* Delete the referencing block with matching refs (decrement). + Objects reach refcount 0 and are auto-deleted from the DB. */ + SYNC_TESTING_cmd_block_delete ("delete-block-refs", + sync_url, + "upload-block-refs", + MHD_HTTP_NO_CONTENT, + 2, + obj_uids, + ref_decs), + + /* Re-upload object A (it was deleted when refcount hit 0) */ + SYNC_TESTING_cmd_object_upload ("upload-obj-A-again", + sync_url, + "upload-block-2", + &obj_uids[0], + MHD_HTTP_NO_CONTENT, + obj_data_A, + sizeof obj_data_A - 1), + + /* Upload another block referencing A (+1) */ + SYNC_TESTING_cmd_block_append ("upload-block-refs2", + sync_url, + "upload-block-C", + &ref_nonce2, + MHD_HTTP_NO_CONTENT, + data[0], + strlen (data[0]), + false, + 1, + obj_uids, + ref_inc1), + + /* Try to delete with refs that would underflow → 409 conflict */ + SYNC_TESTING_cmd_block_delete ("delete-block-refs-underflow", + sync_url, + "upload-block-refs2", + MHD_HTTP_CONFLICT, + 1, + obj_uids, + ref_dec2), + /* The rejected delete left the block listed ... */ + SYNC_TESTING_cmd_block_list ("list-after-underflow", + sync_url, + "upload-block-2", + 10, + NULL, + MHD_HTTP_OK, + 0, + NULL, + NULL), + /* ... and the object it references is still fetchable */ + SYNC_TESTING_cmd_object_fetch ("fetch-obj-A-after-underflow", + sync_url, + "upload-block-2", + &obj_uids[0], + MHD_HTTP_OK, + obj_data_A, + sizeof obj_data_A - 1), + + /* ========== Object and reference error paths ========== */ + + /* Fetching an object that was never uploaded → 404 */ + SYNC_TESTING_cmd_object_fetch ("fetch-obj-unknown", + sync_url, + "upload-block-2", + &unknown_uid, + MHD_HTTP_NOT_FOUND, + NULL, + 0), + + /* Referencing an object that was never uploaded → 409 ... */ + SYNC_TESTING_cmd_block_append ("append-unknown-object-ref", + sync_url, + "upload-block-refs2", + &orphan_nonce, + MHD_HTTP_CONFLICT, + data[0], + strlen (data[0]), + false, + 1, + &unknown_uid, + ref_inc1), + /* ... and must not have moved the tail, so the next append + onto upload-block-refs2 still succeeds */ + SYNC_TESTING_cmd_block_append ("append-after-rejected-ref", + sync_url, + "upload-block-refs2", + &tail_nonce, + MHD_HTTP_NO_CONTENT, + data[2], + strlen (data[2]), + false, + 0, + NULL, + NULL), + + TALER_TESTING_cmd_end () + }; - TALER_TESTING_run (is, - commands); + TALER_TESTING_run (is, + commands); + } } @@ -269,9 +590,6 @@ main (int argc, payer_payto.full_payto = (char *) "payto://x-taler-bank/localhost/" USER_ACCOUNT_NAME "?receiver-name=user"; - exchange_payto.full_payto = - (char *) "payto://x-taler-bank/localhost/" EXCHANGE_ACCOUNT_NAME - "?receiver-name=exchange"; merchant_payto.full_payto = (char *) "payto://x-taler-bank/localhost/" MERCHANT_ACCOUNT_NAME "?receiver-name=merchant"; diff --git a/src/testing/test_sync_api.conf b/src/testing/test_sync_api.conf @@ -30,6 +30,9 @@ CURRENCY = EUR PORT = 8084 PAYMENT_BACKEND_URL = "http://localhost:8080/" ANNUAL_FEE = EUR:4.99 +# Must be in CURRENCY; the packaged default in config.d/sync.conf +# is TESTKUDOS, which would be served as the /config liability_limit. +INSURANCE = EUR:0.0 UPLOAD_LIMIT_MB = 1 SERVE = tcp diff --git a/src/testing/testing_api_cmd_backup_download.c b/src/testing/testing_api_cmd_backup_download.c @@ -1,283 +0,0 @@ -/* - This file is part of SYNC - Copyright (C) 2014-2019 Taler Systems SA - - SYNC is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 3, or - (at your option) any later version. - - SYNC is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public - License along with SYNC; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file testing/testing_api_cmd_backup_download.c - * @brief command to download data to the sync backend service. - * @author Christian Grothoff - */ -#include "platform.h" -#include "sync/sync_service.h" -#include "sync/sync_testing_lib.h" -#include <taler/taler_util.h> -#include <taler/taler_testing_lib.h> - -/** - * State for a "backup download" CMD. - */ -struct BackupDownloadState -{ - - /** - * Eddsa public key. - */ - struct SYNC_AccountPublicKeyP sync_pub; - - /** - * Hash of the upload (all zeros if there was no upload). - */ - const struct GNUNET_HashCode *upload_hash; - - /** - * Hash of the previous upload (all zeros if there was no previous upload). - */ - const struct GNUNET_HashCode *prev_upload_hash; - - /** - * The /backups POST operation handle. - */ - struct SYNC_DownloadOperation *download; - - /** - * URL of the sync backend. - */ - const char *sync_url; - - /** - * The interpreter state. - */ - struct TALER_TESTING_Interpreter *is; - - /** - * Reference to upload command we expect to download. - */ - const char *upload_reference; - - /** - * Expected status code. - */ - unsigned int http_status; - -}; - - -/** - * Function called with the results of a #SYNC_download(). - * - * @param cls closure - * @param dd details about the download operation - */ -static void -backup_download_cb (void *cls, - const struct SYNC_DownloadDetails *dd) -{ - struct BackupDownloadState *bds = cls; - - bds->download = NULL; - if (dd->http_status != bds->http_status) - { - TALER_TESTING_unexpected_status (bds->is, - dd->http_status, - bds->http_status); - return; - } - if (NULL != bds->upload_reference) - { - if ( (MHD_HTTP_OK == dd->http_status) && - (0 != GNUNET_memcmp (&dd->details.ok.curr_backup_hash, - bds->upload_hash)) ) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bds->is); - return; - } - if ( (MHD_HTTP_OK == dd->http_status) && - (0 != GNUNET_memcmp (&dd->details.ok.prev_backup_hash, - bds->prev_upload_hash)) ) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bds->is); - return; - } - } - TALER_TESTING_interpreter_next (bds->is); -} - - -/** - * Run a "backup download" CMD. - * - * @param cls closure. - * @param cmd command currently being run. - * @param is interpreter state. - */ -static void -backup_download_run (void *cls, - const struct TALER_TESTING_Command *cmd, - struct TALER_TESTING_Interpreter *is) -{ - struct BackupDownloadState *bds = cls; - - bds->is = is; - if (NULL != bds->upload_reference) - { - const struct TALER_TESTING_Command *upload_cmd; - const struct SYNC_AccountPublicKeyP *sync_pub; - - upload_cmd = TALER_TESTING_interpreter_lookup_command - (is, - bds->upload_reference); - if (NULL == upload_cmd) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bds->is); - return; - } - if (GNUNET_OK != - TALER_TESTING_get_trait_curr_hash (upload_cmd, - &bds->upload_hash)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bds->is); - return; - } - if (GNUNET_OK != - TALER_TESTING_get_trait_prev_hash (upload_cmd, - &bds->prev_upload_hash)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bds->is); - return; - } - if (GNUNET_OK != - TALER_TESTING_get_trait_sync_account_pub (upload_cmd, - &sync_pub)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bds->is); - return; - } - bds->sync_pub = *sync_pub; - } - bds->download = SYNC_download (TALER_TESTING_interpreter_get_context (is), - bds->sync_url, - &bds->sync_pub, - &backup_download_cb, - bds); - if (NULL == bds->download) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bds->is); - return; - } -} - - -/** - * Free the state of a "backup download" CMD, and possibly - * cancel it if it did not complete. - * - * @param cls closure. - * @param cmd command being freed. - */ -static void -backup_download_cleanup (void *cls, - const struct TALER_TESTING_Command *cmd) -{ - struct BackupDownloadState *bds = cls; - - if (NULL != bds->download) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Command '%s' did not complete (backup download)\n", - cmd->label); - SYNC_download_cancel (bds->download); - bds->download = NULL; - } - GNUNET_free (bds); -} - - -/** - * Make the "backup download" command. - * - * @param label command label - * @param sync_url base URL of the sync serving - * the policy store request. - * @param http_status expected HTTP status. - * @param upload_ref reference to upload command - * @return the command - */ -struct TALER_TESTING_Command -SYNC_TESTING_cmd_backup_download (const char *label, - const char *sync_url, - unsigned int http_status, - const char *upload_ref) -{ - struct BackupDownloadState *bds; - - GNUNET_assert (NULL != upload_ref); - bds = GNUNET_new (struct BackupDownloadState); - bds->http_status = http_status; - bds->sync_url = sync_url; - bds->upload_reference = upload_ref; - { - struct TALER_TESTING_Command cmd = { - .cls = bds, - .label = label, - .run = &backup_download_run, - .cleanup = &backup_download_cleanup - }; - - return cmd; - } -} - - -/** - * Make the "backup download" command for a non-existent upload. - * - * @param label command label - * @param sync_url base URL of the sync serving - * the policy store request. - * @return the command - */ -struct TALER_TESTING_Command -SYNC_TESTING_cmd_backup_nx (const char *label, - const char *sync_url) -{ - struct BackupDownloadState *bds; - struct GNUNET_CRYPTO_EddsaPrivateKey priv; - - bds = GNUNET_new (struct BackupDownloadState); - bds->http_status = MHD_HTTP_NOT_FOUND; - bds->sync_url = sync_url; - GNUNET_CRYPTO_eddsa_key_create (&priv); - GNUNET_CRYPTO_eddsa_key_get_public (&priv, - &bds->sync_pub.eddsa_pub); - { - struct TALER_TESTING_Command cmd = { - .cls = bds, - .label = label, - .run = &backup_download_run, - .cleanup = &backup_download_cleanup - }; - - return cmd; - } -} diff --git a/src/testing/testing_api_cmd_backup_upload.c b/src/testing/testing_api_cmd_backup_upload.c @@ -1,458 +0,0 @@ -/* - This file is part of SYNC - Copyright (C) 2014-2023 Taler Systems SA - - SYNC is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 3, or - (at your option) any later version. - - SYNC is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public - License along with SYNC; see the file COPYING. If not, see - <http://www.gnu.org/licenses/> -*/ -/** - * @file testing/testing_api_cmd_backup_upload.c - * @brief command to upload data to the sync backend service. - * @author Christian Grothoff - */ -#include "platform.h" -#include "sync/sync_service.h" -#include "sync/sync_testing_lib.h" -#include <taler/taler_util.h> -#include <taler/taler_testing_lib.h> -#include <taler/taler_merchant_service.h> -#include "sync/sync_testing_lib.h" - -/** - * State for a "backup upload" CMD. - */ -struct BackupUploadState -{ - - /** - * Eddsa private key. - */ - struct SYNC_AccountPrivateKeyP sync_priv; - - /** - * Eddsa public key. - */ - struct SYNC_AccountPublicKeyP sync_pub; - - /** - * Hash of the previous upload (maybe bogus if - * #SYNC_TESTING_UO_PREV_HASH_WRONG is set in @e uo). - * Maybe all zeros if there was no previous upload. - */ - struct GNUNET_HashCode prev_hash; - - /** - * Hash of the current upload. - */ - struct GNUNET_HashCode curr_hash; - - /** - * The /backups POST operation handle. - */ - struct SYNC_UploadOperation *uo; - - /** - * URL of the sync backend. - */ - const char *sync_url; - - /** - * Previous upload, or NULL for none. Used to calculate what THIS - * upload is based on. - */ - const char *prev_upload; - - /** - * Last upload, or NULL for none, usually same as @e prev_upload. - * Used to check the response on #MHD_HTTP_CONFLICT. - */ - const char *last_upload; - - /** - * Payment order ID we got back, if any. Otherwise NULL. - */ - const char *payment_order_id; - - /** - * Claim token we got back, if any. Otherwise all zeros. - */ - struct TALER_ClaimTokenP token; - - /** - * Payment order ID we are to provide in the request, may be NULL. - */ - const char *payment_order_req; - - /** - * The interpreter state. - */ - struct TALER_TESTING_Interpreter *is; - - /** - * The backup data we are uploading. - */ - const void *backup; - - /** - * Number of bytes in @e backup. - */ - size_t backup_size; - - /** - * Expected status code. - */ - unsigned int http_status; - - /** - * Options for how we are supposed to do the upload. - */ - enum SYNC_TESTING_UploadOption uopt; - -}; - - -/** - * Function called with the results of a #SYNC_upload(). - * - * @param cls closure - * @param ud details about the upload operation - */ -static void -backup_upload_cb (void *cls, - const struct SYNC_UploadDetails *ud) -{ - struct BackupUploadState *bus = cls; - - bus->uo = NULL; - if (ud->http_status != bus->http_status) - { - TALER_TESTING_unexpected_status (bus->is, - ud->http_status, - bus->http_status); - return; - } - switch (ud->us) - { - case SYNC_US_SUCCESS: - if (0 != GNUNET_memcmp ( - &bus->curr_hash, - ud->details.success.curr_backup_hash)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - break; - case SYNC_US_PAYMENT_REQUIRED: - { - struct TALER_MERCHANT_PayUriData pd; - - if (GNUNET_OK != - TALER_MERCHANT_parse_pay_uri ( - ud->details.payment_required.payment_request, - &pd)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - bus->payment_order_id = GNUNET_strdup (pd.order_id); - if (NULL != pd.claim_token) - bus->token = *pd.claim_token; - TALER_MERCHANT_parse_pay_uri_free (&pd); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order ID from Sync service is `%s'\n", - bus->payment_order_id); - memset (&bus->curr_hash, - 0, - sizeof (struct GNUNET_HashCode)); - } - break; - case SYNC_US_CONFLICTING_BACKUP: - { - const struct TALER_TESTING_Command *ref; - const struct GNUNET_HashCode *h; - - ref = TALER_TESTING_interpreter_lookup_command - (bus->is, - bus->last_upload); - GNUNET_assert (NULL != ref); - GNUNET_assert (GNUNET_OK == - TALER_TESTING_get_trait_curr_hash (ref, - &h)); - if (0 != GNUNET_memcmp (h, - &ud->details.recovered_backup. - existing_backup_hash)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - } - case SYNC_US_HTTP_ERROR: - break; - case SYNC_US_CLIENT_ERROR: - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - case SYNC_US_SERVER_ERROR: - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - TALER_TESTING_interpreter_next (bus->is); -} - - -/** - * Run a "backup upload" CMD. - * - * @param cls closure. - * @param cmd command currently being run. - * @param is interpreter state. - */ -static void -backup_upload_run (void *cls, - const struct TALER_TESTING_Command *cmd, - struct TALER_TESTING_Interpreter *is) -{ - struct BackupUploadState *bus = cls; - - bus->is = is; - if (NULL != bus->prev_upload) - { - const struct TALER_TESTING_Command *ref; - - ref = TALER_TESTING_interpreter_lookup_command ( - is, - bus->prev_upload); - if (NULL == ref) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - { - const struct GNUNET_HashCode *h; - - if (GNUNET_OK == - TALER_TESTING_get_trait_curr_hash (ref, - &h)) - { - bus->prev_hash = *h; - } - } - { - const struct SYNC_AccountPrivateKeyP *priv; - - if (GNUNET_OK != - TALER_TESTING_get_trait_sync_account_priv (ref, - &priv)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - bus->sync_priv = *priv; - } - { - const struct SYNC_AccountPublicKeyP *pub; - - if (GNUNET_OK != - TALER_TESTING_get_trait_sync_account_pub (ref, - &pub)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - bus->sync_pub = *pub; - } - if (0 != (SYNC_TESTING_UO_REFERENCE_ORDER_ID & bus->uopt)) - { - const char *order_id; - - if (GNUNET_OK != - TALER_TESTING_get_trait_order_id (ref, - &order_id)) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - bus->payment_order_req = order_id; - if (NULL == bus->payment_order_req) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } - } - } - else - { - GNUNET_CRYPTO_eddsa_key_create (&bus->sync_priv.eddsa_priv); - GNUNET_CRYPTO_eddsa_key_get_public (&bus->sync_priv.eddsa_priv, - &bus->sync_pub.eddsa_pub); - } - if (0 != (SYNC_TESTING_UO_PREV_HASH_WRONG & bus->uopt)) - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, - &bus->prev_hash, - sizeof (struct GNUNET_HashCode)); - GNUNET_CRYPTO_hash (bus->backup, - bus->backup_size, - &bus->curr_hash); - bus->uo = SYNC_upload (TALER_TESTING_interpreter_get_context (is), - bus->sync_url, - &bus->sync_priv, - ( ( (NULL != bus->prev_upload) && - (GNUNET_NO == GNUNET_is_zero ( - &bus->prev_hash)) ) || - (0 != (SYNC_TESTING_UO_PREV_HASH_WRONG - & bus->uopt)) ) - ? &bus->prev_hash - : NULL, - bus->backup_size, - bus->backup, - (0 != (SYNC_TESTING_UO_REQUEST_PAYMENT & bus->uopt)) - ? SYNC_PO_FORCE_PAYMENT - : SYNC_PO_NONE, - bus->payment_order_req, - &backup_upload_cb, - bus); - if (NULL == bus->uo) - { - GNUNET_break (0); - TALER_TESTING_interpreter_fail (bus->is); - return; - } -} - - -/** - * Free the state of a "backup upload" CMD, and possibly - * cancel it if it did not complete. - * - * @param cls closure. - * @param cmd command being freed. - */ -static void -backup_upload_cleanup (void *cls, - const struct TALER_TESTING_Command *cmd) -{ - struct BackupUploadState *bus = cls; - - if (NULL != bus->uo) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Command '%s' did not complete (backup upload)\n", - cmd->label); - SYNC_upload_cancel (bus->uo); - bus->uo = NULL; - } - GNUNET_free (bus); -} - - -/** - * Offer internal data to other commands. - * - * @param cls closure - * @param[out] ret result (could be anything) - * @param trait name of the trait - * @param index index number of the object to extract. - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -backup_upload_traits (void *cls, - const void **ret, - const char *trait, - unsigned int index) -{ - struct BackupUploadState *bus = cls; - struct TALER_TESTING_Trait straits[] = { - TALER_TESTING_make_trait_curr_hash (&bus->curr_hash), - TALER_TESTING_make_trait_prev_hash (&bus->prev_hash), - TALER_TESTING_make_trait_claim_token (&bus->token), - TALER_TESTING_make_trait_sync_account_pub (&bus->sync_pub), - TALER_TESTING_make_trait_sync_account_priv (&bus->sync_priv), - TALER_TESTING_make_trait_order_id (bus->payment_order_id), - TALER_TESTING_trait_end () - }; - struct TALER_TESTING_Trait ftraits[] = { - TALER_TESTING_make_trait_claim_token (&bus->token), - TALER_TESTING_make_trait_sync_account_pub (&bus->sync_pub), - TALER_TESTING_make_trait_sync_account_priv (&bus->sync_priv), - TALER_TESTING_make_trait_order_id (bus->payment_order_id), - TALER_TESTING_trait_end () - }; - - - return TALER_TESTING_get_trait ((NULL != bus->payment_order_req) - ? ftraits - : straits, - ret, - trait, - index); -} - - -/** - * Make the "backup upload" command. - * - * @param label command label - * @param sync_url base URL of the sync serving - * the policy store request. - * @param prev_upload reference to a previous upload we are - * supposed to update, NULL for none - * @param last_upload reference to the last upload for the - * same account, used to check result on MHD_HTTP_CONFLICT - * @param uo upload options - * @param http_status expected HTTP status. - * @param backup_data data to upload - * @param backup_data_size number of bytes in @a backup_data - * @return the command - */ -struct TALER_TESTING_Command -SYNC_TESTING_cmd_backup_upload (const char *label, - const char *sync_url, - const char *prev_upload, - const char *last_upload, - enum SYNC_TESTING_UploadOption uo, - unsigned int http_status, - const void *backup_data, - size_t backup_data_size) -{ - struct BackupUploadState *bus; - - bus = GNUNET_new (struct BackupUploadState); - bus->http_status = http_status; - bus->prev_upload = prev_upload; - bus->last_upload = last_upload; - bus->uopt = uo; - bus->sync_url = sync_url; - bus->backup = backup_data; - bus->backup_size = backup_data_size; - { - struct TALER_TESTING_Command cmd = { - .cls = bus, - .label = label, - .run = &backup_upload_run, - .cleanup = &backup_upload_cleanup, - .traits = &backup_upload_traits - }; - - return cmd; - } -} diff --git a/src/testing/testing_api_cmd_block_append.c b/src/testing/testing_api_cmd_block_append.c @@ -0,0 +1,241 @@ +/* + This file is part of SYNC + Copyright (C) 2014-2026 Taler Systems SA + + SYNC is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + SYNC is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with SYNC; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_block_append.c + * @brief command to append a block to the sync backend service. + * @author Iván Ávalos + */ +#include "testing_api_cmd_block_common.h" +#include <stdbool.h> + + +/** + * Process a reference for a retry upload: inherit keys, nonce + * (if not explicitly set), and old_data_hash from the ref's + * prev_hash trait. + * + * @param[in,out] bus state to populate + * @param ref reference command to inherit from + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ +static enum GNUNET_GenericReturnValue +append_retry_process_ref ( + struct BlockUploadState *bus, + const struct TALER_TESTING_Command *ref) +{ + if (GNUNET_OK != + SYNC_TESTING_inherit_account_keys_ (bus, + ref)) + return GNUNET_SYSERR; + + /* Inherit block nonce if not explicitly set */ + if (GNUNET_is_zero (&bus->nonce)) + { + const struct SYNC_BlockNonce *nonce; + + if (GNUNET_OK == + TALER_TESTING_get_trait_block_nonce (ref, + &nonce)) + bus->nonce = *nonce; + } + + /* Inherit old_data_hash from prev_hash for retry */ + if (GNUNET_is_zero (&bus->old_data_hash)) + { + const struct GNUNET_HashCode *h; + + if (GNUNET_OK == + TALER_TESTING_get_trait_prev_hash (ref, + &h)) + bus->old_data_hash = *h; + } + + return GNUNET_OK; +} + + +/** + * Process a reference for an append-after: inherit keys, set + * prev_nonce from the ref's block_nonce (to chain the new block after + * the referenced one), and collect the tail's data for its relink + * signature. + * + * @param[in,out] bus state to populate + * @param ref reference command to inherit from + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ +static enum GNUNET_GenericReturnValue +append_after_process_ref ( + struct BlockUploadState *bus, + const struct TALER_TESTING_Command *ref) +{ + const struct SYNC_BlockNonce *nonce; + const struct SYNC_BlockNonce *pn; + const struct GNUNET_HashCode *h; + const struct GNUNET_HashCode *rh; + + if (GNUNET_OK != + SYNC_TESTING_inherit_account_keys_ (bus, + ref)) + return GNUNET_SYSERR; + /* Set prev_nonce to chain after the referenced block. If ref has + no nonce trait, prev_nonce stays zero (meaning no predecessor — + allowed by the API) */ + if (GNUNET_OK == + TALER_TESTING_get_trait_block_nonce (ref, + &nonce)) + bus->prev_nonce = *nonce; + + /* The tail's data for its relink signature */ + if (GNUNET_OK == + TALER_TESTING_get_trait_curr_hash (ref, + &h)) + { + bus->prev_entry.nonce = bus->prev_nonce; + bus->prev_entry.hash = *h; + bus->have_relink_target = true; + } + if (GNUNET_OK == + TALER_TESTING_get_trait_block_refs_hash (ref, + &rh)) + bus->prev_entry.refs_hash = *rh; + if (GNUNET_OK == + TALER_TESTING_get_trait_prev_nonce (ref, + &pn)) + { + bus->prev_entry.prev_nonce = *pn; + bus->prev_entry.have_prev_nonce = ! GNUNET_is_zero (pn); + } + + return GNUNET_OK; +} + + +/** + * Start a block append operation. + * + * @param ctx for HTTP client request processing + * @param sync_url base URL of the Sync server + * @param sync_priv private key of the account + * @param nonce nonce identifying the block + * @param prev_nonce nonce of the preceding block, or NULL + * @param next_nonce unused, appends are always at the tail + * @param old_data_hash unused, appends have no previous data + * @param block_data_size number of bytes in @a block_data + * @param block_data the block data + * @param refs_len number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by this block + * @param object_incs array of reference count adjustments + * @param prev_entry the tail's entry, for its relink signature, or NULL + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle for the operation + */ +static struct SYNC_BlockOperation * +append_start_upload ( + struct GNUNET_CURL_Context *ctx, + const char *sync_url, + const struct SYNC_AccountPrivateKeyP *sync_priv, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_data_hash, + size_t block_data_size, + const void *block_data, + size_t refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_BlockListEntry *prev_entry, + SYNC_BlockOperationCallback cb, + void *cb_cls) +{ + struct SYNC_BlockOperationArgs args = { + .nonce = nonce, + .prev_nonce = prev_nonce, + .next_nonce = NULL, + .old_data_hash = NULL, + .block_data_size = block_data_size, + .block_data = block_data, + .refs_len = refs_len, + .object_uids = object_uids, + .object_incs = object_incs, + .prev_entry = prev_entry, + }; + + (void) next_nonce; + (void) old_data_hash; + return SYNC_block_append (ctx, + sync_url, + sync_priv, + &args, + cb, + cb_cls); +} + + +struct TALER_TESTING_Command +SYNC_TESTING_cmd_block_append (const char *label, + const char *sync_url, + const char *upload_reference, + const struct SYNC_BlockNonce *nonce, + unsigned int http_status, + const void *block_data, + size_t block_data_size, + bool retry_upload, + unsigned int refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs) +{ + struct BlockUploadState *bus; + + GNUNET_assert (refs_len <= MAX_BLOCK_OBJECT_REFS); + bus = GNUNET_new (struct BlockUploadState); + bus->sync_url = sync_url; + bus->upload_reference = upload_reference; + if (NULL != nonce) + bus->nonce = *nonce; + bus->http_status = http_status; + bus->block_data = block_data; + bus->block_data_size = block_data_size; + bus->refs_len = refs_len; + if (0 < refs_len) + { + GNUNET_memcpy (bus->object_uids, + object_uids, + refs_len * sizeof (struct SYNC_ObjectUID)); + GNUNET_memcpy (bus->object_incs, + object_incs, + refs_len * sizeof (int16_t)); + } + bus->process_ref = retry_upload + ? &append_retry_process_ref + : &append_after_process_ref; + bus->start_upload = &append_start_upload; + { + struct TALER_TESTING_Command cmd = { + .cls = bus, + .label = label, + .run = &SYNC_TESTING_block_upload_run_, + .cleanup = &SYNC_TESTING_block_upload_cleanup_, + .traits = &SYNC_TESTING_block_upload_traits_ + }; + + return cmd; + } +} diff --git a/src/testing/testing_api_cmd_block_common.c b/src/testing/testing_api_cmd_block_common.c @@ -0,0 +1,224 @@ +/* + This file is part of SYNC + Copyright (C) 2014-2026 Taler Systems SA + + SYNC is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + SYNC is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with SYNC; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_block_common.c + * @brief shared logic for block upload commands. + * @author Iván Ávalos + */ +#include <sync/sync_util.h> +#include "testing_api_cmd_block_common.h" + + +/** + * Function called when we're done processing the + * block upload. + * + * @param cls the `struct BlockUploadState` + * @param ud details about the upload operation + */ +static void +block_upload_finished (void *cls, + const struct SYNC_BlockOperationDetails *ud) +{ + struct BlockUploadState *bus = cls; + + bus->bop = NULL; + if (ud->http_status != bus->http_status) + { + TALER_TESTING_unexpected_status (bus->is, + ud->http_status, + bus->http_status); + return; + } + if (MHD_HTTP_PAYMENT_REQUIRED == ud->http_status) + { + struct TALER_MERCHANT_PayUriData pd; + + if (GNUNET_OK != + TALER_MERCHANT_parse_pay_uri (ud->pay_uri, + &pd)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (bus->is); + return; + } + bus->payment_order_id = GNUNET_strdup (pd.order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order ID from Sync service is `%s'\n", + bus->payment_order_id); + if (NULL != pd.claim_token) + bus->claim_token = *pd.claim_token; + TALER_MERCHANT_parse_pay_uri_free (&pd); + } + TALER_TESTING_interpreter_next (bus->is); +} + + +enum GNUNET_GenericReturnValue +SYNC_TESTING_inherit_account_keys_ ( + struct BlockUploadState *bus, + const struct TALER_TESTING_Command *ref) +{ + const struct SYNC_AccountPrivateKeyP *priv; + const struct SYNC_AccountPublicKeyP *pub; + + if (GNUNET_OK != + TALER_TESTING_get_trait_sync_account_priv (ref, + &priv)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_TESTING_get_trait_sync_account_pub (ref, + &pub)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + bus->sync_priv = *priv; + bus->sync_pub = *pub; + return GNUNET_OK; +} + + +void +SYNC_TESTING_block_upload_run_ ( + void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct BlockUploadState *bus = cls; + + (void) cmd; + bus->is = is; + + if (NULL != bus->upload_reference) + { + const struct TALER_TESTING_Command *ref; + + ref = TALER_TESTING_interpreter_lookup_command (is, + bus->upload_reference); + if (NULL == ref) + goto fail; + if (GNUNET_OK != + bus->process_ref (bus, ref)) + goto fail; + } + else + { + GNUNET_CRYPTO_eddsa_key_create (&bus->sync_priv.eddsa_priv); + GNUNET_CRYPTO_eddsa_key_get_public (&bus->sync_priv.eddsa_priv, + &bus->sync_pub.eddsa_pub); + } + + /* Generate random nonce if still all zeros */ + if (GNUNET_is_zero (&bus->nonce)) + SYNC_block_nonce_generate (&bus->nonce); + + /* Compute hash of block data for traits */ + GNUNET_CRYPTO_hash (bus->block_data, + bus->block_data_size, + &bus->new_data_hash); + + /* P1: hash over the object references (covered by the signature) */ + if (GNUNET_OK != + SYNC_object_refs_hash (bus->refs_len, + bus->object_uids, + bus->object_incs, + &bus->refs_hash)) + { + GNUNET_break (0); + goto fail; + } + + bus->bop = bus->start_upload ( + TALER_TESTING_interpreter_get_context (is), + bus->sync_url, + &bus->sync_priv, + &bus->nonce, + GNUNET_is_zero (&bus->prev_nonce) ? NULL : &bus->prev_nonce, + GNUNET_is_zero (&bus->next_nonce) ? NULL : &bus->next_nonce, + GNUNET_is_zero (&bus->old_data_hash) ? NULL : &bus->old_data_hash, + bus->block_data_size, + bus->block_data, + bus->refs_len, + bus->object_uids, + bus->object_incs, + bus->have_relink_target ? &bus->prev_entry : NULL, + &block_upload_finished, + bus); + if (NULL == bus->bop) + goto fail; + + return; + +fail: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); +} + + +void +SYNC_TESTING_block_upload_cleanup_ ( + void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct BlockUploadState *bus = cls; + + if (NULL != bus->bop) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (block upload)\n", + cmd->label); + SYNC_block_operation_cancel (bus->bop); + bus->bop = NULL; + } + GNUNET_free (bus->payment_order_id); + GNUNET_free (bus); +} + + +enum GNUNET_GenericReturnValue +SYNC_TESTING_block_upload_traits_ ( + void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct BlockUploadState *bus = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_sync_account_pub (&bus->sync_pub), + TALER_TESTING_make_trait_sync_account_priv (&bus->sync_priv), + TALER_TESTING_make_trait_curr_hash (&bus->new_data_hash), + TALER_TESTING_make_trait_prev_hash (&bus->old_data_hash), + TALER_TESTING_make_trait_block_nonce (&bus->nonce), + TALER_TESTING_make_trait_prev_nonce (&bus->prev_nonce), + TALER_TESTING_make_trait_next_nonce (&bus->next_nonce), + TALER_TESTING_make_trait_block_refs_hash (&bus->refs_hash), + TALER_TESTING_make_trait_order_id (bus->payment_order_id), + TALER_TESTING_make_trait_claim_token (&bus->claim_token), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} diff --git a/src/testing/testing_api_cmd_block_common.h b/src/testing/testing_api_cmd_block_common.h @@ -0,0 +1,259 @@ +/* + This file is part of SYNC + Copyright (C) 2014-2026 Taler Systems SA + + SYNC is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + SYNC is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with SYNC; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_block_common.h + * @brief shared internal state for block upload commands + * @author Iván Ávalos + */ +#ifndef TESTING_API_CMD_BLOCK_COMMON_H +#define TESTING_API_CMD_BLOCK_COMMON_H + +#include "platform.h" +#include "sync/sync_service.h" +#include "sync/sync_testing_lib.h" +#include <taler/taler_util.h> +#include <taler/taler_signatures.h> +#include <taler/taler_testing_lib.h> +#include <taler/merchant/common.h> +#include <gnunet/gnunet_curl_lib.h> + + +/** + * Maximum number of object references a block command can carry. + */ +#define MAX_BLOCK_OBJECT_REFS 32 + + +/** + * State for a "block upload" CMD. + */ +struct BlockUploadState +{ + + /** + * Eddsa private key. + */ + struct SYNC_AccountPrivateKeyP sync_priv; + + /** + * Eddsa public key. + */ + struct SYNC_AccountPublicKeyP sync_pub; + + /** + * Nonce for the block. + */ + struct SYNC_BlockNonce nonce; + + /** + * Nonce of the preceding block, all zeros for none. + */ + struct SYNC_BlockNonce prev_nonce; + + /** + * Nonce of the succeeding block, all zeros for none. + */ + struct SYNC_BlockNonce next_nonce; + + /** + * Hash of old data, all zeros for append. + */ + struct GNUNET_HashCode old_data_hash; + + /** + * Hash of new block data (exposed as curr_hash trait). + */ + struct GNUNET_HashCode new_data_hash; + + /** + * URL of the sync backend. + */ + const char *sync_url; + + /** + * Reference to a command that can provide account keys, + * NULL to generate fresh keys. + */ + const char *upload_reference; + + /** + * Function to process a reference command: inherit keys, + * nonce, old_data_hash, and/or set prev_nonce depending + * on the upload mode (retry, append-after, update, etc.). + * + * @param[in,out] bus state to populate + * @param ref reference command to inherit from + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ + enum GNUNET_GenericReturnValue + (*process_ref)( + struct BlockUploadState *bus, + const struct TALER_TESTING_Command *ref); + + /** + * Function to start the upload operation. + */ + struct SYNC_BlockOperation * + (*start_upload)( + struct GNUNET_CURL_Context *ctx, + const char *sync_url, + const struct SYNC_AccountPrivateKeyP *sync_priv, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_data_hash, + size_t block_data_size, + const void *block_data, + size_t refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_BlockListEntry *prev_entry, + SYNC_BlockOperationCallback cb, + void *cb_cls); + + /** + * Block data to upload. + */ + const void *block_data; + + /** + * Number of bytes in @e block_data. + */ + size_t block_data_size; + + /** + * Number of entries in @e object_uids and @e object_incs. + */ + unsigned int refs_len; + + /** + * Array of object UIDs referenced by this block (array of + * @e refs_len, up to #MAX_BLOCK_OBJECT_REFS). + */ + struct SYNC_ObjectUID object_uids[MAX_BLOCK_OBJECT_REFS]; + + /** + * Array of reference count adjustments (array of @e refs_len, + * up to #MAX_BLOCK_OBJECT_REFS). + */ + int16_t object_incs[MAX_BLOCK_OBJECT_REFS]; + + /** + * Hash over the object references of this block. + */ + struct GNUNET_HashCode refs_hash; + + /** + * Entry of the block at @e prev_nonce (the tail on an append), + * used to build its relink signature; NULL if there is no such + * block. + */ + struct SYNC_BlockListEntry prev_entry; + + /** + * True if the relink target entry is available. + */ + bool have_relink_target; + + /** + * Expected HTTP status code. + */ + unsigned int http_status; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Payment order ID extracted from the 402 taler://pay URI. + */ + char *payment_order_id; + + /** + * Claim token extracted from the 402 taler://pay URI. + */ + struct TALER_ClaimTokenP claim_token; + + /** + * Handle for the library block upload operation. + */ + struct SYNC_BlockOperation *bop; +}; + + +/** + * Copy the account key pair offered by @a ref into @a bus. + * + * @param[in,out] bus state to populate + * @param ref reference command to inherit the keys from + * @return #GNUNET_OK on success, #GNUNET_SYSERR if @a ref + * does not offer the account key traits + */ +enum GNUNET_GenericReturnValue +SYNC_TESTING_inherit_account_keys_ ( + struct BlockUploadState *bus, + const struct TALER_TESTING_Command *ref); + + +/** + * Run a "block upload" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +void +SYNC_TESTING_block_upload_run_ ( + void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is); + + +/** + * Free the state of a "block upload" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +void +SYNC_TESTING_block_upload_cleanup_ ( + void *cls, + const struct TALER_TESTING_Command *cmd); + + +/** + * Offer the internal data of a "block upload" CMD to other commands. + * + * @param cls closure + * @param[out] ret result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +SYNC_TESTING_block_upload_traits_ ( + void *cls, + const void **ret, + const char *trait, + unsigned int index); + + +#endif diff --git a/src/testing/testing_api_cmd_block_delete.c b/src/testing/testing_api_cmd_block_delete.c @@ -0,0 +1,442 @@ +/* + This file is part of SYNC + Copyright (C) 2014-2026 Taler Systems SA + + SYNC is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + SYNC is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with SYNC; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_block_delete.c + * @brief command to delete a block from the sync backend service. + * @author Iván Ávalos + */ +#include "platform.h" +#include "sync/sync_service.h" +#include "sync/sync_testing_lib.h" +#include <taler/taler_util.h> +#include <taler/taler_signatures.h> +#include <taler/taler_testing_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "testing_api_cmd_block_common.h" + + +/** + * State for a "block delete" CMD. + */ +struct BlockDeleteState +{ + + /** + * URL of the sync backend. + */ + const char *sync_url; + + /** + * Reference to a command that provides account keys, + * block nonce and block hash. + */ + const char *account_reference; + + /** + * Eddsa private key. + */ + struct SYNC_AccountPrivateKeyP sync_priv; + + /** + * Eddsa public key. + */ + struct SYNC_AccountPublicKeyP sync_pub; + + /** + * Nonce of the block to delete. + */ + struct SYNC_BlockNonce nonce; + + /** + * Prev nonce (zero if first block). + */ + struct SYNC_BlockNonce prev_nonce; + + /** + * Next nonce (zero if last block). + */ + struct SYNC_BlockNonce next_nonce; + + /** + * Hash of the block to delete. + */ + struct GNUNET_HashCode block_hash; + + /** + * Expected HTTP status code. + */ + unsigned int http_status; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Handle for the library block delete operation. + */ + struct SYNC_BlockOperation *bop; + + /** + * Handle for the library block list operation (first phase). + */ + struct SYNC_BlockListOperation *blop; + + /** + * Number of entries in @e object_uids and @e object_incs. + */ + unsigned int refs_len; + + /** + * Array of object UIDs referenced by this block (array of + * @e refs_len, up to #MAX_BLOCK_OBJECT_REFS). + */ + struct SYNC_ObjectUID object_uids[MAX_BLOCK_OBJECT_REFS]; + + /** + * Array of reference count adjustments (array of @e refs_len, + * up to #MAX_BLOCK_OBJECT_REFS). + */ + int16_t object_incs[MAX_BLOCK_OBJECT_REFS]; + + /** + * Entry of the preceding neighbour (for its relink signature). + */ + struct SYNC_BlockListEntry prev_entry; + + /** + * True if @e prev_entry is set. + */ + bool have_prev_entry; + + /** + * Entry of the succeeding neighbour (for its relink signature). + */ + struct SYNC_BlockListEntry next_entry; + + /** + * True if @e next_entry is set. + */ + bool have_next_entry; + +}; + + +/** + * Function called when we're done processing the + * block delete. + * + * @param cls the `struct BlockDeleteState` + * @param ud details about the delete operation + */ +static void +block_delete_finished (void *cls, + const struct SYNC_BlockOperationDetails *ud) +{ + struct BlockDeleteState *bds = cls; + + bds->bop = NULL; + if (ud->http_status != bds->http_status) + { + TALER_TESTING_unexpected_status (bds->is, + ud->http_status, + bds->http_status); + return; + } + TALER_TESTING_interpreter_next (bds->is); +} + + +/** + * Function called when we're done processing the + * block list that precedes the delete. + * + * @param cls the `struct BlockDeleteState` + * @param ld details about the list operation + */ +static void +block_delete_list_finished (void *cls, + const struct SYNC_BlockListDetails *ld) +{ + struct BlockDeleteState *bds = cls; + + bds->blop = NULL; + if (MHD_HTTP_OK != ld->http_status) + { + GNUNET_break_op (0); + TALER_TESTING_interpreter_fail (bds->is); + return; + } + + /* Find the block to delete and its neighbours in the list */ + { + const struct SYNC_BlockListEntry *prev = NULL; + const struct SYNC_BlockListEntry *next = NULL; + + for (unsigned int i = 0; i < ld->num_entries; i++) + { + const struct SYNC_BlockListEntry *e = &ld->entries[i]; + + if ( (0 == GNUNET_memcmp (&e->nonce, + &bds->prev_nonce)) || + (0 == GNUNET_memcmp (&e->nonce, + &bds->next_nonce)) ) + { + if (0 == GNUNET_memcmp (&e->nonce, + &bds->prev_nonce)) + prev = e; + else + next = e; + } + } + + if (NULL != prev) + { + bds->prev_entry = *prev; + bds->have_prev_entry = true; + } + if (NULL != next) + { + bds->next_entry = *next; + bds->have_next_entry = true; + } + } + + { + struct SYNC_BlockOperationArgs args = { + .nonce = &bds->nonce, + .prev_nonce = GNUNET_is_zero (&bds->prev_nonce) + ? NULL + : &bds->prev_nonce, + .next_nonce = GNUNET_is_zero (&bds->next_nonce) + ? NULL + : &bds->next_nonce, + .block_hash = &bds->block_hash, + .refs_len = bds->refs_len, + .object_uids = bds->object_uids, + .object_incs = bds->object_incs, + .prev_entry = bds->have_prev_entry + ? &bds->prev_entry + : NULL, + .next_entry = bds->have_next_entry + ? &bds->next_entry + : NULL, + }; + + bds->bop = SYNC_block_delete ( + TALER_TESTING_interpreter_get_context (bds->is), + bds->sync_url, + &bds->sync_priv, + &args, + &block_delete_finished, + bds); + if (NULL == bds->bop) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (bds->is); + } + } +} + + +/** + * Run a "block delete" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +block_delete_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct BlockDeleteState *bds = cls; + + (void) cmd; + bds->is = is; + + { + const struct TALER_TESTING_Command *ref; + + ref = TALER_TESTING_interpreter_lookup_command (is, + bds->account_reference); + if (NULL == ref) + { + GNUNET_break (0); + goto fail; + } + { + const struct SYNC_AccountPrivateKeyP *priv; + const struct SYNC_AccountPublicKeyP *pub; + + if ( (GNUNET_OK != + TALER_TESTING_get_trait_sync_account_priv (ref, + &priv)) || + (GNUNET_OK != + TALER_TESTING_get_trait_sync_account_pub (ref, + &pub)) ) + { + GNUNET_break (0); + goto fail; + } + bds->sync_priv = *priv; + bds->sync_pub = *pub; + } + { + const struct SYNC_BlockNonce *nonce; + + if (GNUNET_OK != + TALER_TESTING_get_trait_block_nonce (ref, + &nonce)) + { + GNUNET_break (0); + goto fail; + } + bds->nonce = *nonce; + } + { + const struct SYNC_BlockNonce *pn; + + if (GNUNET_OK == + TALER_TESTING_get_trait_prev_nonce (ref, + &pn)) + { + bds->prev_nonce = *pn; + } + } + { + const struct SYNC_BlockNonce *nn; + + if (GNUNET_OK == + TALER_TESTING_get_trait_next_nonce (ref, + &nn)) + { + bds->next_nonce = *nn; + } + } + { + const struct GNUNET_HashCode *hash; + + if (GNUNET_OK != + TALER_TESTING_get_trait_curr_hash (ref, + &hash)) + { + GNUNET_break (0); + goto fail; + } + bds->block_hash = *hash; + } + } + + /* Phase 1: fetch the linked list to learn the current state of the + block and its neighbours before deleting (their entries are needed + for the relink signatures). */ + bds->blop = SYNC_block_list ( + TALER_TESTING_interpreter_get_context (is), + bds->sync_url, + &bds->sync_pub, + 10000, + NULL, + &block_delete_list_finished, + bds); + if (NULL == bds->blop) + goto fail; + + return; + +fail: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); +} + + +/** + * Free the state of a "block delete" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +block_delete_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct BlockDeleteState *bds = cls; + + if (NULL != bds->bop) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (block delete)\n", + cmd->label); + SYNC_block_operation_cancel (bds->bop); + bds->bop = NULL; + } + if (NULL != bds->blop) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (block list)\n", + cmd->label); + SYNC_block_list_cancel (bds->blop); + bds->blop = NULL; + } + GNUNET_free (bds); +} + + +struct TALER_TESTING_Command +SYNC_TESTING_cmd_block_delete (const char *label, + const char *sync_url, + const char *account_reference, + unsigned int http_status, + unsigned int refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs) +{ + struct BlockDeleteState *bds; + + GNUNET_assert (refs_len <= MAX_BLOCK_OBJECT_REFS); + bds = GNUNET_new (struct BlockDeleteState); + bds->sync_url = sync_url; + bds->account_reference = account_reference; + bds->http_status = http_status; + bds->refs_len = refs_len; + if (0 < refs_len) + { + GNUNET_memcpy (bds->object_uids, + object_uids, + refs_len * sizeof (struct SYNC_ObjectUID)); + GNUNET_memcpy (bds->object_incs, + object_incs, + refs_len * sizeof (int16_t)); + } + { + struct TALER_TESTING_Command cmd = { + .cls = bds, + .label = label, + .run = &block_delete_run, + .cleanup = &block_delete_cleanup, + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_block_delete.c */ diff --git a/src/testing/testing_api_cmd_block_list.c b/src/testing/testing_api_cmd_block_list.c @@ -0,0 +1,445 @@ +/* + This file is part of SYNC + Copyright (C) 2026 Taler Systems SA + + SYNC is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + SYNC is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with SYNC; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_block_list.c + * @brief command to list blocks from the sync backend service. + * @author Iván Ávalos + */ +#include "platform.h" +#include "sync/sync_service.h" +#include "sync/sync_signatures.h" +#include "sync/sync_testing_lib.h" +#include <taler/taler_util.h> +#include <taler/taler_testing_lib.h> +#include <gnunet/gnunet_curl_lib.h> + + +/** + * Maximum number of expected entries in block list. + */ +#define MAX_BLOCK_LIST_ENTRIES 256 + + +/** + * State for a "block list" CMD. + */ +struct BlockListState +{ + + /** + * URL of the sync backend. + */ + const char *sync_url; + + /** + * Reference to a command that provides the account public key. + */ + const char *account_reference; + + /** + * Account public key to query. + */ + struct SYNC_AccountPublicKeyP account_pub; + + /** + * Maximum number of blocks to return. + */ + int16_t limit; + + /** + * Optional starting nonce. + */ + struct SYNC_BlockNonce start_nonce; + + /** + * True if @e start_nonce should be used. + */ + bool have_start_nonce; + + /** + * Expected HTTP status code. + */ + unsigned int http_status; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Handle for the library block list operation. + */ + struct SYNC_BlockListOperation *blop; + + /** + * Result details to expose as trait. + */ + struct SYNC_BlockListDetails details; + + /** + * Number of expected entries. + */ + unsigned int num_expected; + + /** + * Expected nonces (array of @e num_expected, up to + * #MAX_BLOCK_LIST_ENTRIES). + */ + struct SYNC_BlockNonce expected_nonces[MAX_BLOCK_LIST_ENTRIES]; + + /** + * Expected block hashes (array of @e num_expected, up to + * #MAX_BLOCK_LIST_ENTRIES). + */ + struct GNUNET_HashCode expected_hashes[MAX_BLOCK_LIST_ENTRIES]; + +}; + + +/** + * Function called when we're done processing the + * block list. + * + * @param cls the `struct BlockListState` + * @param ld details about the list operation + */ +static void +handle_block_list_finished (void *cls, + const struct SYNC_BlockListDetails *ld) +{ + struct BlockListState *bls = cls; + bool mismatch = false; + + bls->blop = NULL; + bls->details.http_status = ld->http_status; + bls->details.ec = ld->ec; + if (ld->http_status != bls->http_status) + { + TALER_TESTING_unexpected_status (bls->is, + ld->http_status, + bls->http_status); + return; + } + /* Whatever the server returns must carry a signature that verifies + against the stored context, or a client has no way to trust the + backup it is about to restore */ + for (unsigned int i = 0; i < ld->num_entries; i++) + { + const struct SYNC_BlockListEntry *e = &ld->entries[i]; + + if (GNUNET_OK != + SYNC_block_upload_verify (&bls->account_pub, + &e->nonce, + e->have_prev_nonce + ? &e->prev_nonce + : NULL, + e->have_next_nonce + ? &e->next_nonce + : NULL, + &e->old_hash, + &e->hash, + &e->refs_hash, + &e->upload_sig)) + { + char *nonce_s; + + mismatch = true; + nonce_s = GNUNET_STRINGS_data_to_string_alloc ( + &e->nonce, + sizeof (struct SYNC_BlockNonce)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Block list entry %u (%s): stored signature does not" + " verify against the stored context\n", + i, + nonce_s); + GNUNET_free (nonce_s); + } + } + + if (bls->num_expected > 0) + { + if (ld->num_entries != bls->num_expected) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Expected %u entries in block list, got %u\n", + bls->num_expected, + ld->num_entries); + TALER_TESTING_interpreter_fail (bls->is); + return; + } + for (unsigned int i = 0; i < bls->num_expected; i++) + { + if (0 != GNUNET_memcmp (&ld->entries[i].nonce, + &bls->expected_nonces[i])) + { + char *nonce_a; + char *nonce_b; + + mismatch = true; + nonce_a = GNUNET_STRINGS_data_to_string_alloc ( + &ld->entries[i].nonce, + sizeof (struct SYNC_BlockNonce)); + nonce_b = GNUNET_STRINGS_data_to_string_alloc ( + &bls->expected_nonces[i], + sizeof (struct SYNC_BlockNonce)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Block list entry %u nonce mismatch\n(%s vs.\n %s)\n", + i, + nonce_a, + nonce_b); + GNUNET_free (nonce_a); + GNUNET_free (nonce_b); + } + if (0 != GNUNET_memcmp (&ld->entries[i].hash, + &bls->expected_hashes[i])) + { + char *hash_a; + char *hash_b; + + mismatch = true; + hash_a = GNUNET_STRINGS_data_to_string_alloc ( + &ld->entries[i].hash, + sizeof (struct GNUNET_HashCode)); + hash_b = GNUNET_STRINGS_data_to_string_alloc ( + &bls->expected_hashes[i], + sizeof (struct GNUNET_HashCode)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Block list entry %u hash mismatch\n(%s vs.\n %s)\n", + i, + hash_a, + hash_b); + GNUNET_free (hash_a); + GNUNET_free (hash_b); + } + /* The payload must hash to the hash reported for the block */ + { + struct GNUNET_HashCode dh; + + GNUNET_CRYPTO_hash (ld->entries[i].data, + ld->entries[i].data_size, + &dh); + if (0 != GNUNET_memcmp (&dh, + &ld->entries[i].hash)) + { + mismatch = true; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Block list entry %u: data of %u bytes does not" + " hash to the reported block hash\n", + i, + (unsigned int) ld->entries[i].data_size); + } + } + } + } + + if (mismatch) + { + GNUNET_break_op (0); + TALER_TESTING_interpreter_fail (bls->is); + return; + } + + TALER_TESTING_interpreter_next (bls->is); +} + + +/** + * Run a "block list" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +block_list_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct BlockListState *bls = cls; + + (void) cmd; + bls->is = is; + + if (NULL != bls->account_reference) + { + const struct TALER_TESTING_Command *ref; + + ref = TALER_TESTING_interpreter_lookup_command (is, + bls->account_reference); + if (NULL == ref) + { + GNUNET_break (0); + goto fail; + } + { + const struct SYNC_AccountPublicKeyP *pub; + + if (GNUNET_OK != + TALER_TESTING_get_trait_sync_account_pub (ref, + &pub)) + goto fail; + bls->account_pub = *pub; + } + } + else + goto fail; + + bls->blop = SYNC_block_list ( + TALER_TESTING_interpreter_get_context (is), + bls->sync_url, + &bls->account_pub, + bls->limit, + bls->have_start_nonce ? &bls->start_nonce : NULL, + &handle_block_list_finished, + bls); + if (NULL == bls->blop) + goto fail; + + return; + +fail: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); +} + + +/** + * Free the state of a "block list" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +block_list_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct BlockListState *bls = cls; + + if (NULL != bls->blop) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (block list)\n", + cmd->label); + SYNC_block_list_cancel (bls->blop); + bls->blop = NULL; + } + GNUNET_free (bls); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param[out] ret result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +block_list_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct BlockListState *bls = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_block_list_result (&bls->details), + TALER_TESTING_trait_end () + }; + const struct TALER_TESTING_Trait *t; + enum GNUNET_GenericReturnValue res; + + res = TALER_TESTING_get_trait (traits, + ret, + trait, + index); + if (GNUNET_OK == res) + return res; + if (0 < bls->details.num_entries) + { + /* Expose the last entry's signed context to the block write + commands (needed for relink signatures). */ + const struct SYNC_BlockListEntry *last = + &bls->details.entries[bls->details.num_entries - 1]; + struct TALER_TESTING_Trait st[] = { + TALER_TESTING_make_trait_block_nonce (&last->nonce), + TALER_TESTING_make_trait_prev_nonce (&last->prev_nonce), + TALER_TESTING_make_trait_next_nonce (&last->next_nonce), + TALER_TESTING_make_trait_block_refs_hash (&last->refs_hash), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (st, + ret, + trait, + index); + } + return res; +} + + +struct TALER_TESTING_Command +SYNC_TESTING_cmd_block_list (const char *label, + const char *sync_url, + const char *account_reference, + int16_t limit, + const struct SYNC_BlockNonce *start_nonce, + unsigned int http_status, + unsigned int num_expected, + const struct SYNC_BlockNonce *expected_nonces, + const struct GNUNET_HashCode *expected_hashes) +{ + struct BlockListState *bls; + + GNUNET_assert (num_expected <= MAX_BLOCK_LIST_ENTRIES); + bls = GNUNET_new (struct BlockListState); + bls->sync_url = sync_url; + bls->account_reference = account_reference; + bls->limit = limit; + if (NULL != start_nonce) + { + bls->start_nonce = *start_nonce; + bls->have_start_nonce = true; + } + bls->http_status = http_status; + bls->num_expected = num_expected; + if (0 < num_expected) + { + GNUNET_memcpy (bls->expected_nonces, + expected_nonces, + num_expected * sizeof (struct SYNC_BlockNonce)); + GNUNET_memcpy (bls->expected_hashes, + expected_hashes, + num_expected * sizeof (struct GNUNET_HashCode)); + } + { + struct TALER_TESTING_Command cmd = { + .cls = bls, + .label = label, + .run = &block_list_run, + .cleanup = &block_list_cleanup, + .traits = &block_list_traits + }; + + return cmd; + } +} diff --git a/src/testing/testing_api_cmd_block_update.c b/src/testing/testing_api_cmd_block_update.c @@ -0,0 +1,181 @@ +/* + This file is part of SYNC + Copyright (C) 2014-2026 Taler Systems SA + + SYNC is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + SYNC is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with SYNC; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_block_update.c + * @brief command to update a block in the sync backend service. + * @author Iván Ávalos + */ +#include "testing_api_cmd_block_common.h" + + +/** + * Process a reference for an update: inherit keys, nonce + * (if not explicitly set), and old_data_hash from the ref's + * curr_hash trait. + * + * @param[in,out] bus state to populate + * @param ref reference command to inherit from + * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure + */ +static enum GNUNET_GenericReturnValue +update_process_ref ( + struct BlockUploadState *bus, + const struct TALER_TESTING_Command *ref) +{ + if (GNUNET_OK != + SYNC_TESTING_inherit_account_keys_ (bus, + ref)) + return GNUNET_SYSERR; + + /* Inherit block nonce if not explicitly set */ + if (GNUNET_is_zero (&bus->nonce)) + { + const struct SYNC_BlockNonce *nonce; + + if (GNUNET_OK == + TALER_TESTING_get_trait_block_nonce (ref, + &nonce)) + bus->nonce = *nonce; + } + + /* Inherit old_data_hash from curr_hash for update */ + if (GNUNET_is_zero (&bus->old_data_hash)) + { + const struct GNUNET_HashCode *h; + + if (GNUNET_OK == + TALER_TESTING_get_trait_curr_hash (ref, + &h)) + bus->old_data_hash = *h; + } + + return GNUNET_OK; +} + + +/** + * Start a block update operation. + * + * @param ctx for HTTP client request processing + * @param sync_url base URL of the Sync server + * @param sync_priv private key of the account + * @param nonce nonce identifying the block + * @param prev_nonce nonce of the preceding block, or NULL + * @param next_nonce nonce of the succeeding block, or NULL + * @param old_data_hash hash of the existing block data + * @param block_data_size number of bytes in @a block_data + * @param block_data the block data + * @param refs_len number of entries in @a object_uids and @a object_incs + * @param object_uids array of object UIDs referenced by this block + * @param object_incs array of reference count adjustments + * @param prev_entry unused for updates (the links do not change) + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle for the operation + */ +static struct SYNC_BlockOperation * +update_start_upload ( + struct GNUNET_CURL_Context *ctx, + const char *sync_url, + const struct SYNC_AccountPrivateKeyP *sync_priv, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_data_hash, + size_t block_data_size, + const void *block_data, + size_t refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + const struct SYNC_BlockListEntry *prev_entry, + SYNC_BlockOperationCallback cb, + void *cb_cls) +{ + struct SYNC_BlockOperationArgs args = { + .nonce = nonce, + .prev_nonce = prev_nonce, + .next_nonce = next_nonce, + .old_data_hash = old_data_hash, + .block_data_size = block_data_size, + .block_data = block_data, + .refs_len = refs_len, + .object_uids = object_uids, + .object_incs = object_incs, + }; + + (void) prev_entry; + return SYNC_block_update (ctx, + sync_url, + sync_priv, + &args, + cb, + cb_cls); +} + + +struct TALER_TESTING_Command +SYNC_TESTING_cmd_block_update (const char *label, + const char *sync_url, + const char *upload_reference, + const struct SYNC_BlockNonce *nonce, + const struct GNUNET_HashCode *old_data_hash, + unsigned int http_status, + const void *block_data, + size_t block_data_size, + unsigned int refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs) +{ + struct BlockUploadState *bus; + + GNUNET_assert (refs_len <= MAX_BLOCK_OBJECT_REFS); + bus = GNUNET_new (struct BlockUploadState); + bus->sync_url = sync_url; + bus->upload_reference = upload_reference; + if (NULL != nonce) + bus->nonce = *nonce; + if (NULL != old_data_hash) + bus->old_data_hash = *old_data_hash; + bus->http_status = http_status; + bus->block_data = block_data; + bus->block_data_size = block_data_size; + bus->refs_len = refs_len; + if (0 < refs_len) + { + GNUNET_memcpy (bus->object_uids, + object_uids, + refs_len * sizeof (struct SYNC_ObjectUID)); + GNUNET_memcpy (bus->object_incs, + object_incs, + refs_len * sizeof (int16_t)); + } + bus->process_ref = &update_process_ref; + bus->start_upload = &update_start_upload; + { + struct TALER_TESTING_Command cmd = { + .cls = bus, + .label = label, + .run = &SYNC_TESTING_block_upload_run_, + .cleanup = &SYNC_TESTING_block_upload_cleanup_, + .traits = &SYNC_TESTING_block_upload_traits_ + }; + + return cmd; + } +} diff --git a/src/testing/testing_api_cmd_object_fetch.c b/src/testing/testing_api_cmd_object_fetch.c @@ -0,0 +1,270 @@ +/* + This file is part of SYNC + Copyright (C) 2026 Taler Systems SA + + SYNC is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + SYNC is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with SYNC; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_object_fetch.c + * @brief command to fetch an object from the sync backend service. + * @author Iván Ávalos + */ +#include "platform.h" +#include "sync/sync_service.h" +#include "sync/sync_testing_lib.h" +#include <sync/sync_util.h> +#include <taler/taler_util.h> +#include <taler/taler_signatures.h> +#include <taler/taler_testing_lib.h> +#include <gnunet/gnunet_curl_lib.h> + + +/** + * State for an "object fetch" CMD. + */ +struct ObjectFetchState +{ + + /** + * URL of the sync backend. + */ + const char *sync_url; + + /** + * Reference to a command that provides account keys. + */ + const char *account_reference; + + /** + * Eddsa public key. + */ + struct SYNC_AccountPublicKeyP sync_pub; + + /** + * UID of the object to fetch. + */ + struct SYNC_ObjectUID uid; + + /** + * Expected object data. + */ + const void *expected_data; + + /** + * Number of bytes in @e expected_data. + */ + size_t expected_data_size; + + /** + * Expected HTTP status code. + */ + unsigned int http_status; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Handle for the library object fetch operation. + */ + struct SYNC_ObjectFetchOperation *fop; + +}; + + +/** + * Function called when we're done processing the + * object fetch. + * + * @param cls the `struct ObjectFetchState` + * @param http_status HTTP status code + * @param ec error code + * @param uid UID of the fetched object + * @param data_size number of bytes in @a data + * @param data object data + */ +static void +object_fetch_finished (void *cls, + unsigned int http_status, + enum TALER_ErrorCode ec, + const struct SYNC_ObjectUID *uid, + size_t data_size, + const void *data) +{ + struct ObjectFetchState *ofs = cls; + + ofs->fop = NULL; + if (http_status != ofs->http_status) + { + TALER_TESTING_unexpected_status (ofs->is, + http_status, + ofs->http_status); + return; + } + if (MHD_HTTP_OK == http_status) + { + if (data_size != ofs->expected_data_size) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Object fetch data size mismatch: expected %zu, got %zu\n", + ofs->expected_data_size, + data_size); + GNUNET_break_op (0); + TALER_TESTING_interpreter_fail (ofs->is); + return; + } + if (0 != memcmp (data, + ofs->expected_data, + ofs->expected_data_size)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Object fetch data content mismatch\n"); + GNUNET_break_op (0); + TALER_TESTING_interpreter_fail (ofs->is); + return; + } + if (NULL != uid) + { + if (0 != GNUNET_memcmp (uid, + &ofs->uid)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Object fetch UID mismatch\n"); + GNUNET_break_op (0); + TALER_TESTING_interpreter_fail (ofs->is); + return; + } + } + } + TALER_TESTING_interpreter_next (ofs->is); +} + + +/** + * Run an "object fetch" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +object_fetch_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct ObjectFetchState *ofs = cls; + + (void) cmd; + ofs->is = is; + + { + const struct TALER_TESTING_Command *ref; + const struct SYNC_AccountPublicKeyP *pub; + + ref = TALER_TESTING_interpreter_lookup_command (is, + ofs->account_reference); + if (NULL == ref) + { + GNUNET_break (0); + goto fail; + } + if (GNUNET_OK != + TALER_TESTING_get_trait_sync_account_pub (ref, + &pub)) + { + GNUNET_break (0); + goto fail; + } + ofs->sync_pub = *pub; + } + + ofs->fop = SYNC_object_fetch ( + TALER_TESTING_interpreter_get_context (is), + ofs->sync_url, + &ofs->sync_pub, + &ofs->uid, + &object_fetch_finished, + ofs); + if (NULL == ofs->fop) + goto fail; + + return; + +fail: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); +} + + +/** + * Free the state of an "object fetch" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +object_fetch_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct ObjectFetchState *ofs = cls; + + if (NULL != ofs->fop) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (object fetch)\n", + cmd->label); + SYNC_object_fetch_cancel (ofs->fop); + ofs->fop = NULL; + } + GNUNET_free (ofs); +} + + +struct TALER_TESTING_Command +SYNC_TESTING_cmd_object_fetch (const char *label, + const char *sync_url, + const char *account_reference, + const struct SYNC_ObjectUID *uid, + unsigned int http_status, + const void *expected_data, + size_t expected_data_size) +{ + struct ObjectFetchState *ofs; + + ofs = GNUNET_new (struct ObjectFetchState); + ofs->sync_url = sync_url; + ofs->account_reference = account_reference; + if (NULL != uid) + ofs->uid = *uid; + ofs->http_status = http_status; + ofs->expected_data = expected_data; + ofs->expected_data_size = expected_data_size; + { + struct TALER_TESTING_Command cmd = { + .cls = ofs, + .label = label, + .run = &object_fetch_run, + .cleanup = &object_fetch_cleanup, + .traits = NULL + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_object_fetch.c */ diff --git a/src/testing/testing_api_cmd_object_upload.c b/src/testing/testing_api_cmd_object_upload.c @@ -0,0 +1,238 @@ +/* + This file is part of SYNC + Copyright (C) 2014-2026 Taler Systems SA + + SYNC is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + SYNC is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with SYNC; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_object_upload.c + * @brief command to upload an object to the sync backend service. + * @author Iván Ávalos + */ +#include "platform.h" +#include "sync/sync_service.h" +#include "sync/sync_testing_lib.h" +#include <taler/taler_util.h> +#include <taler/taler_signatures.h> +#include <taler/taler_testing_lib.h> +#include <gnunet/gnunet_curl_lib.h> + + +/** + * State for an "object upload" CMD. + */ +struct ObjectUploadState +{ + + /** + * URL of the sync backend. + */ + const char *sync_url; + + /** + * Reference to a command that provides account keys. + */ + const char *account_reference; + + /** + * Eddsa private key. + */ + struct SYNC_AccountPrivateKeyP sync_priv; + + /** + * Eddsa public key. + */ + struct SYNC_AccountPublicKeyP sync_pub; + + /** + * UID of the object. + */ + struct SYNC_ObjectUID uid; + + /** + * Object data. + */ + const void *data; + + /** + * Number of bytes in @e data. + */ + size_t data_size; + + /** + * Expected HTTP status code. + */ + unsigned int http_status; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Handle for the library object upload operation. + */ + struct SYNC_BlockOperation *bop; + +}; + + +/** + * Function called when we're done processing the + * object upload. + * + * @param cls the `struct ObjectUploadState` + * @param ud details about the upload operation + */ +static void +object_upload_finished (void *cls, + const struct SYNC_BlockOperationDetails *ud) +{ + struct ObjectUploadState *ous = cls; + + ous->bop = NULL; + if (ud->http_status != ous->http_status) + { + TALER_TESTING_unexpected_status (ous->is, + ud->http_status, + ous->http_status); + return; + } + TALER_TESTING_interpreter_next (ous->is); +} + + +/** + * Run an "object upload" CMD. + * + * @param cls closure. + * @param cmd command currently being run. + * @param is interpreter state. + */ +static void +object_upload_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct ObjectUploadState *ous = cls; + + (void) cmd; + ous->is = is; + + { + const struct TALER_TESTING_Command *ref; + const struct SYNC_AccountPrivateKeyP *priv; + const struct SYNC_AccountPublicKeyP *pub; + + ref = TALER_TESTING_interpreter_lookup_command (is, + ous->account_reference); + if (NULL == ref) + { + GNUNET_break (0); + goto fail; + } + if ( (GNUNET_OK != + TALER_TESTING_get_trait_sync_account_priv (ref, + &priv)) || + (GNUNET_OK != + TALER_TESTING_get_trait_sync_account_pub (ref, + &pub)) ) + { + GNUNET_break (0); + goto fail; + } + ous->sync_priv = *priv; + ous->sync_pub = *pub; + } + + ous->bop = SYNC_object_upload ( + TALER_TESTING_interpreter_get_context (is), + ous->sync_url, + &ous->sync_priv, + &ous->uid, + ous->data_size, + ous->data, + &object_upload_finished, + ous); + if (NULL == ous->bop) + goto fail; + + return; + +fail: + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); +} + + +/** + * Free the state of an "object upload" CMD, and possibly + * cancel it if it did not complete. + * + * @param cls closure. + * @param cmd command being freed. + */ +static void +object_upload_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct ObjectUploadState *ous = cls; + + if (NULL != ous->bop) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command '%s' did not complete (object upload)\n", + cmd->label); + SYNC_block_operation_cancel (ous->bop); + ous->bop = NULL; + } + GNUNET_free (ous); +} + + +struct TALER_TESTING_Command +SYNC_TESTING_cmd_object_upload (const char *label, + const char *sync_url, + const char *account_reference, + const struct SYNC_ObjectUID *uid, + unsigned int http_status, + const void *data, + size_t data_size) +{ + struct ObjectUploadState *ous; + + ous = GNUNET_new (struct ObjectUploadState); + ous->sync_url = sync_url; + ous->account_reference = account_reference; + if (NULL != uid) + ous->uid = *uid; + ous->http_status = http_status; + ous->data = data; + ous->data_size = data_size; + { + struct TALER_TESTING_Command cmd = { + .cls = ous, + .label = label, + .run = &object_upload_run, + .cleanup = &object_upload_cleanup, + .traits = NULL + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_object_upload.c */ diff --git a/src/util/meson.build b/src/util/meson.build @@ -10,11 +10,17 @@ configure_file( libsyncutil = library( 'syncutil', - ['os_installation.c'], + ['os_installation.c', 'sync_signatures.c', 'sync_block.c', 'sync_object.c'], soversion: solibversions['libsyncutil']['soversion'], version: solibversions['libsyncutil']['soversion'], install_rpath: rpath_option, - dependencies: [gnunetutil_dep], + dependencies: [ + gnunetutil_dep, + talerutil_dep, + gnunetjson_dep, + talerjson_dep, + json_dep, + ], include_directories: [incdir, configuration_inc], install: true, install_dir: get_option('libdir'), @@ -36,3 +42,27 @@ executable( ) +test_sync_signatures = executable( + 'test_sync_signatures', + ['test_sync_signatures.c'], + install_rpath: rpath_option, + dependencies: [ + libsyncutil_dep, + talerutil_dep, + gnunetutil_dep, + gnunetjson_dep, + json_dep, + ], + include_directories: [incdir, configuration_inc], + install: false, +) +# Not in 'installcheck', which the default test setup excludes: this +# test needs neither an installed tree nor a database. +test( + 'test_sync_signatures', + test_sync_signatures, + workdir: meson.current_build_dir(), + suite: ['util'], +) + + diff --git a/src/util/os_installation.c b/src/util/os_installation.c @@ -19,7 +19,7 @@ */ /** - * @file os_installation.c + * @file util/os_installation.c * @brief initialize libgnunet OS subsystem for Sync. * @author Christian Grothoff */ diff --git a/src/util/sync_block.c b/src/util/sync_block.c @@ -0,0 +1,129 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file util/sync_block.c + * @brief shared logic for block JSON parsing and serialization + * @author Iván Ávalos + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <taler/taler_json_lib.h> +#include <sync/sync_util.h> + + +void +SYNC_block_nonce_generate ( + struct SYNC_BlockNonce *nonce) +{ + GNUNET_CRYPTO_random_block (nonce, + sizeof (struct SYNC_BlockNonce)); +} + + +enum GNUNET_GenericReturnValue +SYNC_block_entry_parse ( + json_t *input, + struct SYNC_BlockListEntry *entry) +{ + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_fixed_auto ("nonce", + &entry->nonce), + GNUNET_JSON_spec_fixed_auto ("block_hash", + &entry->hash), + GNUNET_JSON_spec_varsize ("data", + &entry->data, + &entry->data_size), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("prev_nonce", + &entry->prev_nonce), + &entry->have_prev_nonce), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("next_nonce", + &entry->next_nonce), + &entry->have_next_nonce), + GNUNET_JSON_spec_fixed_auto ("upload_sig", + &entry->upload_sig), + GNUNET_JSON_spec_fixed_auto ("old_hash", + &entry->old_hash), + GNUNET_JSON_spec_fixed_auto ("refs_hash", + &entry->refs_hash), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + res = GNUNET_JSON_parse (input, + espec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse block entry at field %s\n", + ename); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +void +SYNC_block_entry_clear ( + struct SYNC_BlockListEntry *entry) +{ + GNUNET_free (entry->data); + entry->data = NULL; + entry->data_size = 0; +} + + +json_t * +SYNC_block_entry_serialize ( + const struct SYNC_BlockNonce *nonce, + const struct GNUNET_HashCode *hash, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + size_t data_size, + const void *data, + const struct SYNC_AccountSignatureP *upload_sig, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *refs_hash) +{ + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("nonce", + nonce), + GNUNET_JSON_pack_data_auto ("block_hash", + hash), + GNUNET_JSON_pack_data_varsize ("data", + data, + data_size), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("prev_nonce", + prev_nonce)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("next_nonce", + next_nonce)), + GNUNET_JSON_pack_data_auto ("upload_sig", + upload_sig), + GNUNET_JSON_pack_data_auto ("old_hash", + old_hash), + GNUNET_JSON_pack_data_auto ("refs_hash", + refs_hash)); +} diff --git a/src/util/sync_object.c b/src/util/sync_object.c @@ -0,0 +1,276 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file util/sync_object.c + * @brief shared logic for object JSON parsing and serialization + * @author Iván Ávalos + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <taler/taler_json_lib.h> +#include <sync/sync_util.h> + + +enum GNUNET_GenericReturnValue +SYNC_object_entry_parse ( + json_t *input, + struct SYNC_ObjectEntry *entry) +{ + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_fixed_auto ("uid", + &entry->uid), + GNUNET_JSON_spec_varsize ("data", + &entry->data, + &entry->data_size), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + res = GNUNET_JSON_parse (input, + espec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse object entry at field %s\n", + ename); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +void +SYNC_object_entry_clear ( + struct SYNC_ObjectEntry *entry) +{ + GNUNET_free (entry->data); + entry->data = NULL; + entry->data_size = 0; +} + + +json_t * +SYNC_object_entry_serialize ( + const struct SYNC_ObjectUID *uid, + size_t data_size, + const void *data) +{ + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("uid", + uid), + GNUNET_JSON_pack_data_varsize ("data", + data, + data_size)); +} + + +json_t * +SYNC_object_refs_serialize ( + size_t refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs) +{ + json_t *refs; + + if (0 == refs_len) + return NULL; + refs = json_object (); + GNUNET_assert (NULL != refs); + for (size_t i = 0; i < refs_len; i++) + { + char *key; + + key = GNUNET_STRINGS_data_to_string_alloc (&object_uids[i], + sizeof (object_uids[i])); + GNUNET_assert (0 == + json_object_set_new (refs, + key, + json_integer (object_incs[i]))); + GNUNET_free (key); + } + return refs; +} + + +GNUNET_NETWORK_STRUCT_BEGIN + +/** + * One object reference, as hashed for the signatures. + */ +struct SYNC_ObjectRefP +{ + /** + * UID of the referenced object. + */ + struct SYNC_ObjectUID uid; + + /** + * Reference count adjustment, in network byte order. + */ + int16_t inc GNUNET_PACKED; +}; + +GNUNET_NETWORK_STRUCT_END + + +/** + * Compare two object references by UID, for #qsort(). + * + * @param a first `struct SYNC_ObjectRefP` + * @param b second `struct SYNC_ObjectRefP` + * @return negative/zero/positive as @a a sorts before/equal/after @a b + */ +static int +ref_cmp (const void *a, + const void *b) +{ + const struct SYNC_ObjectRefP *ra = a; + const struct SYNC_ObjectRefP *rb = b; + + return memcmp (&ra->uid, + &rb->uid, + sizeof (ra->uid)); +} + + +enum GNUNET_GenericReturnValue +SYNC_object_refs_hash ( + size_t refs_len, + const struct SYNC_ObjectUID *object_uids, + const int16_t *object_incs, + struct GNUNET_HashCode *refs_hash) +{ + struct SYNC_ObjectRefP *refs; + + if (0 == refs_len) + { + GNUNET_CRYPTO_hash ("", + 0, + refs_hash); + return GNUNET_OK; + } + refs = GNUNET_new_array (refs_len, + struct SYNC_ObjectRefP); + for (size_t i = 0; i < refs_len; i++) + { + refs[i].uid = object_uids[i]; + refs[i].inc = htons ((uint16_t) object_incs[i]); + } + qsort (refs, + refs_len, + sizeof (struct SYNC_ObjectRefP), + &ref_cmp); + for (size_t i = 1; i < refs_len; i++) + { + if (0 == ref_cmp (&refs[i - 1], + &refs[i])) + { + GNUNET_break (0); + GNUNET_free (refs); + return GNUNET_SYSERR; + } + } + GNUNET_CRYPTO_hash (refs, + refs_len * sizeof (struct SYNC_ObjectRefP), + refs_hash); + GNUNET_free (refs); + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +SYNC_object_refs_parse ( + const json_t *refs, + size_t *refs_len, + struct SYNC_ObjectUID **object_uids, + int16_t **object_incs) +{ + struct SYNC_ObjectUID *uids; + int16_t *incs; + size_t len; + size_t i; + const char *key; + json_t *value; + + if (NULL == refs) + { + *refs_len = 0; + *object_uids = NULL; + *object_incs = NULL; + return GNUNET_OK; + } + if (! json_is_object (refs)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + len = json_object_size (refs); + if (0 == len) + { + *refs_len = 0; + *object_uids = NULL; + *object_incs = NULL; + return GNUNET_OK; + } + uids = GNUNET_new_array (len, + struct SYNC_ObjectUID); + incs = GNUNET_new_array (len, + int16_t); + i = 0; + json_object_foreach ((json_t *) refs, key, value) + { + json_int_t inc; + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (key, + strlen (key), + &uids[i], + sizeof (uids[i]))) + { + GNUNET_break_op (0); + goto fail; + } + if (! json_is_integer (value)) + { + GNUNET_break_op (0); + goto fail; + } + inc = json_integer_value (value); + if ( (inc < INT16_MIN) || + (inc > INT16_MAX) ) + { + GNUNET_break_op (0); + goto fail; + } + incs[i] = (int16_t) inc; + i++; + } + *refs_len = len; + *object_uids = uids; + *object_incs = incs; + return GNUNET_OK; +fail: + GNUNET_free (uids); + GNUNET_free (incs); + return GNUNET_SYSERR; +} diff --git a/src/util/sync_signatures.c b/src/util/sync_signatures.c @@ -0,0 +1,260 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file util/sync_signatures.c + * @brief Utility functions for sync signatures + * @author Iván Ávalos + */ +#include "platform.h" +#include <taler/taler_signatures.h> +#include <sync/sync_signatures.h> + +/* FIXME: implement in exchange */ +#ifndef TALER_SIGNATURE_SYNC_BLOCK_UPLOAD +#define TALER_SIGNATURE_SYNC_BLOCK_UPLOAD 1452 +#endif +#ifndef TALER_SIGNATURE_SYNC_BLOCK_DELETE +#define TALER_SIGNATURE_SYNC_BLOCK_DELETE 1453 +#endif +#ifndef TALER_SIGNATURE_SYNC_OBJECT_UPLOAD +#define TALER_SIGNATURE_SYNC_OBJECT_UPLOAD 1454 +#endif + + +GNUNET_NETWORK_STRUCT_BEGIN + +/** + * Data signed by the account public key of a sync client to + * authorize the append/update of a block. + */ +struct SYNC_BlockUploadSignaturePS +{ + struct GNUNET_CRYPTO_SignaturePurpose purpose; + + struct SYNC_BlockNonce prev_nonce; + + struct SYNC_BlockNonce next_nonce; + + struct SYNC_BlockNonce nonce; + + struct GNUNET_HashCode old_hash; + + struct GNUNET_HashCode new_hash; + + struct GNUNET_HashCode refs_hash; +}; + +/** + * Data signed by the account public key of a sync client to + * authorize the deletion of a block. + */ +struct SYNC_BlockDeleteSignaturePS +{ + struct GNUNET_CRYPTO_SignaturePurpose purpose; + + struct SYNC_BlockNonce nonce; + + struct SYNC_BlockNonce prev_nonce; + + struct SYNC_BlockNonce next_nonce; + + struct GNUNET_HashCode hash; + + struct GNUNET_HashCode refs_hash; +}; + +/** + * Data signed by the account public key of a sync client to + * authorize the upload of an object. + */ +struct SYNC_ObjectUploadSignaturePS +{ + struct GNUNET_CRYPTO_SignaturePurpose purpose; + + struct SYNC_ObjectUID uid; + + struct GNUNET_HashCode hash; +}; + +GNUNET_NETWORK_STRUCT_END + + +void +SYNC_block_upload_sign ( + const struct SYNC_AccountPrivateKeyP *account_priv, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *new_hash, + const struct GNUNET_HashCode *refs_hash, + struct SYNC_AccountSignatureP *upload_sig) +{ + struct SYNC_BlockUploadSignaturePS busp = { + .purpose.size = htonl (sizeof (busp)), + .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BLOCK_UPLOAD), + .nonce = *nonce, + .new_hash = *new_hash, + .refs_hash = *refs_hash, + }; + + if (NULL != prev_nonce) + busp.prev_nonce = *prev_nonce; + if (NULL != next_nonce) + busp.next_nonce = *next_nonce; + if (NULL != old_hash) + busp.old_hash = *old_hash; + + GNUNET_CRYPTO_eddsa_sign (&account_priv->eddsa_priv, + &busp, + &upload_sig->eddsa_sig); +} + + +enum GNUNET_GenericReturnValue +SYNC_block_upload_verify ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *old_hash, + const struct GNUNET_HashCode *new_hash, + const struct GNUNET_HashCode *refs_hash, + const struct SYNC_AccountSignatureP *upload_sig) +{ + struct SYNC_BlockUploadSignaturePS busp = { + .purpose.size = htonl (sizeof (busp)), + .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BLOCK_UPLOAD), + .nonce = *nonce, + .new_hash = *new_hash, + .refs_hash = *refs_hash, + }; + + if (NULL != prev_nonce) + busp.prev_nonce = *prev_nonce; + if (NULL != next_nonce) + busp.next_nonce = *next_nonce; + if (NULL != old_hash) + busp.old_hash = *old_hash; + + return GNUNET_CRYPTO_eddsa_verify ( + TALER_SIGNATURE_SYNC_BLOCK_UPLOAD, + &busp, + &upload_sig->eddsa_sig, + &account_pub->eddsa_pub); +} + + +void +SYNC_block_delete_sign ( + const struct SYNC_AccountPrivateKeyP *account_priv, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *hash, + const struct GNUNET_HashCode *refs_hash, + struct SYNC_AccountSignatureP *delete_sig) +{ + struct SYNC_BlockDeleteSignaturePS bdsp = { + .purpose.size = htonl (sizeof (bdsp)), + .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BLOCK_DELETE), + .nonce = *nonce, + .hash = *hash, + .refs_hash = *refs_hash, + }; + + if (NULL != prev_nonce) + bdsp.prev_nonce = *prev_nonce; + if (NULL != next_nonce) + bdsp.next_nonce = *next_nonce; + + GNUNET_CRYPTO_eddsa_sign (&account_priv->eddsa_priv, + &bdsp, + &delete_sig->eddsa_sig); +} + + +enum GNUNET_GenericReturnValue +SYNC_block_delete_verify ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_BlockNonce *nonce, + const struct SYNC_BlockNonce *prev_nonce, + const struct SYNC_BlockNonce *next_nonce, + const struct GNUNET_HashCode *hash, + const struct GNUNET_HashCode *refs_hash, + const struct SYNC_AccountSignatureP *delete_sig) +{ + struct SYNC_BlockDeleteSignaturePS bdsp = { + .purpose.size = htonl (sizeof (bdsp)), + .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BLOCK_DELETE), + .nonce = *nonce, + .hash = *hash, + .refs_hash = *refs_hash, + }; + + if (NULL != prev_nonce) + bdsp.prev_nonce = *prev_nonce; + if (NULL != next_nonce) + bdsp.next_nonce = *next_nonce; + + return GNUNET_CRYPTO_eddsa_verify ( + TALER_SIGNATURE_SYNC_BLOCK_DELETE, + &bdsp, + &delete_sig->eddsa_sig, + &account_pub->eddsa_pub); +} + + +void +SYNC_object_upload_sign ( + const struct SYNC_AccountPrivateKeyP *account_priv, + const struct SYNC_ObjectUID *uid, + const struct GNUNET_HashCode *hash, + struct SYNC_AccountSignatureP *upload_sig) +{ + struct SYNC_ObjectUploadSignaturePS ousp = { + .purpose.size = htonl (sizeof (ousp)), + .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_OBJECT_UPLOAD), + .uid = *uid, + .hash = *hash, + }; + + GNUNET_CRYPTO_eddsa_sign (&account_priv->eddsa_priv, + &ousp, + &upload_sig->eddsa_sig); +} + + +enum GNUNET_GenericReturnValue +SYNC_object_upload_verify ( + const struct SYNC_AccountPublicKeyP *account_pub, + const struct SYNC_ObjectUID *uid, + const struct GNUNET_HashCode *hash, + const struct SYNC_AccountSignatureP *upload_sig) +{ + struct SYNC_ObjectUploadSignaturePS ousp = { + .purpose.size = htonl (sizeof (ousp)), + .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_OBJECT_UPLOAD), + .uid = *uid, + .hash = *hash, + }; + + return GNUNET_CRYPTO_eddsa_verify ( + TALER_SIGNATURE_SYNC_OBJECT_UPLOAD, + &ousp, + &upload_sig->eddsa_sig, + &account_pub->eddsa_pub); +} diff --git a/src/util/test_sync_signatures.c b/src/util/test_sync_signatures.c @@ -0,0 +1,413 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file util/test_sync_signatures.c + * @brief tests for the sync signatures and the object reference hash + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include "sync/sync_service.h" +#include "sync/sync_signatures.h" +#include "sync/sync_util.h" + + +#define FAILIF(cond) \ + do { \ + if (! (cond)) break; \ + GNUNET_break (0); \ + return 1; \ + } while (0) + + +/** + * Check #SYNC_object_refs_hash(): insensitive to the order the + * references arrive in, sensitive to every UID and adjustment. + * + * @return 0 on success + */ +static int +test_refs_hash (void) +{ + struct SYNC_ObjectUID uids[3]; + struct SYNC_ObjectUID shuffled[3]; + const int16_t incs[3] = { 1, -2, 3 }; + int16_t shuffled_incs[3]; + struct GNUNET_HashCode h; + struct GNUNET_HashCode h2; + + for (unsigned int i = 0; i < 3; i++) + GNUNET_CRYPTO_random_block (&uids[i], + sizeof (uids[i])); + + /* An empty reference set hashes deterministically */ + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (0, + NULL, + NULL, + &h)); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (0, + uids, + incs, + &h2)); + FAILIF (0 != GNUNET_memcmp (&h, + &h2)); + + /* ... and differs from any non-empty set */ + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (3, + uids, + incs, + &h)); + FAILIF (0 == GNUNET_memcmp (&h, + &h2)); + + /* JSON object member order is not preserved, so reordering the + references must not change the hash */ + shuffled[0] = uids[2]; + shuffled[1] = uids[0]; + shuffled[2] = uids[1]; + shuffled_incs[0] = incs[2]; + shuffled_incs[1] = incs[0]; + shuffled_incs[2] = incs[1]; + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (3, + shuffled, + shuffled_incs, + &h2)); + FAILIF (0 != GNUNET_memcmp (&h, + &h2)); + + /* Changing an adjustment changes the hash */ + shuffled_incs[0] = (int16_t) (shuffled_incs[0] + 1); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (3, + shuffled, + shuffled_incs, + &h2)); + FAILIF (0 == GNUNET_memcmp (&h, + &h2)); + + /* Changing a UID changes the hash */ + shuffled_incs[0] = incs[2]; + GNUNET_CRYPTO_random_block (&shuffled[0], + sizeof (shuffled[0])); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (3, + shuffled, + shuffled_incs, + &h2)); + FAILIF (0 == GNUNET_memcmp (&h, + &h2)); + + /* Dropping a reference changes the hash */ + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (2, + uids, + incs, + &h2)); + FAILIF (0 == GNUNET_memcmp (&h, + &h2)); + + /* A repeated UID cannot be carried on the wire */ + shuffled[0] = uids[0]; + shuffled[1] = uids[0]; + shuffled[2] = uids[1]; + FAILIF (GNUNET_SYSERR != + SYNC_object_refs_hash (3, + shuffled, + shuffled_incs, + &h2)); + return 0; +} + + +/** + * Check that references survive the wire encoding with their hash + * intact; this is what makes the signature verify server-side. + * + * @return 0 on success + */ +static int +test_refs_json_roundtrip (void) +{ + struct SYNC_ObjectUID uids[3]; + const int16_t incs[3] = { 5, -1, 32767 }; + struct GNUNET_HashCode h; + struct GNUNET_HashCode h2; + json_t *refs; + size_t parsed_len; + struct SYNC_ObjectUID *parsed_uids; + int16_t *parsed_incs; + + for (unsigned int i = 0; i < 3; i++) + GNUNET_CRYPTO_random_block (&uids[i], + sizeof (uids[i])); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (3, + uids, + incs, + &h)); + refs = SYNC_object_refs_serialize (3, + uids, + incs); + FAILIF (NULL == refs); + FAILIF (GNUNET_OK != + SYNC_object_refs_parse (refs, + &parsed_len, + &parsed_uids, + &parsed_incs)); + json_decref (refs); + FAILIF (3 != parsed_len); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (parsed_len, + parsed_uids, + parsed_incs, + &h2)); + GNUNET_free (parsed_uids); + GNUNET_free (parsed_incs); + FAILIF (0 != GNUNET_memcmp (&h, + &h2)); + return 0; +} + + +/** + * Check that the block upload signature covers the object references. + * + * @return 0 on success + */ +static int +test_upload_sig (void) +{ + struct SYNC_AccountPrivateKeyP priv; + struct SYNC_AccountPublicKeyP pub; + struct SYNC_AccountSignatureP sig; + struct SYNC_BlockNonce nonce; + struct SYNC_BlockNonce prev; + struct GNUNET_HashCode old_hash; + struct GNUNET_HashCode new_hash; + struct SYNC_ObjectUID uid; + int16_t inc = 1; + int16_t tampered_inc = 2; + struct GNUNET_HashCode refs_hash; + struct GNUNET_HashCode other_refs_hash; + + GNUNET_CRYPTO_eddsa_key_create (&priv.eddsa_priv); + GNUNET_CRYPTO_eddsa_key_get_public (&priv.eddsa_priv, + &pub.eddsa_pub); + GNUNET_CRYPTO_random_block (&nonce, + sizeof (nonce)); + GNUNET_CRYPTO_random_block (&prev, + sizeof (prev)); + GNUNET_CRYPTO_random_block (&uid, + sizeof (uid)); + GNUNET_CRYPTO_hash ("old", 3, &old_hash); + GNUNET_CRYPTO_hash ("new", 3, &new_hash); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (1, + &uid, + &inc, + &refs_hash)); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (1, + &uid, + &tampered_inc, + &other_refs_hash)); + SYNC_block_upload_sign (&priv, + &nonce, + &prev, + NULL, + &old_hash, + &new_hash, + &refs_hash, + &sig); + FAILIF (GNUNET_OK != + SYNC_block_upload_verify (&pub, + &nonce, + &prev, + NULL, + &old_hash, + &new_hash, + &refs_hash, + &sig)); + /* Altering the adjustment invalidates the signature */ + FAILIF (GNUNET_OK == + SYNC_block_upload_verify (&pub, + &nonce, + &prev, + NULL, + &old_hash, + &new_hash, + &other_refs_hash, + &sig)); + /* So does stripping the references altogether */ + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (0, + NULL, + NULL, + &other_refs_hash)); + FAILIF (GNUNET_OK == + SYNC_block_upload_verify (&pub, + &nonce, + &prev, + NULL, + &old_hash, + &new_hash, + &other_refs_hash, + &sig)); + /* The rest of the signed data is still covered */ + FAILIF (GNUNET_OK == + SYNC_block_upload_verify (&pub, + &nonce, + NULL, + NULL, + &old_hash, + &new_hash, + &refs_hash, + &sig)); + return 0; +} + + +/** + * Check that the block delete signature covers the object references. + * + * @return 0 on success + */ +static int +test_delete_sig (void) +{ + struct SYNC_AccountPrivateKeyP priv; + struct SYNC_AccountPublicKeyP pub; + struct SYNC_AccountSignatureP sig; + struct SYNC_BlockNonce nonce; + struct GNUNET_HashCode block_hash; + struct SYNC_ObjectUID uid; + int16_t dec = -1; + struct GNUNET_HashCode refs_hash; + struct GNUNET_HashCode empty_refs_hash; + + GNUNET_CRYPTO_eddsa_key_create (&priv.eddsa_priv); + GNUNET_CRYPTO_eddsa_key_get_public (&priv.eddsa_priv, + &pub.eddsa_pub); + GNUNET_CRYPTO_random_block (&nonce, + sizeof (nonce)); + GNUNET_CRYPTO_random_block (&uid, + sizeof (uid)); + GNUNET_CRYPTO_hash ("block", 5, &block_hash); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (1, + &uid, + &dec, + &refs_hash)); + FAILIF (GNUNET_OK != + SYNC_object_refs_hash (0, + NULL, + NULL, + &empty_refs_hash)); + SYNC_block_delete_sign (&priv, + &nonce, + NULL, + NULL, + &block_hash, + &refs_hash, + &sig); + FAILIF (GNUNET_OK != + SYNC_block_delete_verify (&pub, + &nonce, + NULL, + NULL, + &block_hash, + &refs_hash, + &sig)); + FAILIF (GNUNET_OK == + SYNC_block_delete_verify (&pub, + &nonce, + NULL, + NULL, + &block_hash, + &empty_refs_hash, + &sig)); + return 0; +} + + +/** + * Check the object upload signature round trip. + * + * @return 0 on success + */ +static int +test_object_sig (void) +{ + struct SYNC_AccountPrivateKeyP priv; + struct SYNC_AccountPublicKeyP pub; + struct SYNC_AccountSignatureP sig; + struct SYNC_ObjectUID uid; + struct SYNC_ObjectUID other_uid; + struct GNUNET_HashCode hash; + + GNUNET_CRYPTO_eddsa_key_create (&priv.eddsa_priv); + GNUNET_CRYPTO_eddsa_key_get_public (&priv.eddsa_priv, + &pub.eddsa_pub); + GNUNET_CRYPTO_random_block (&uid, + sizeof (uid)); + GNUNET_CRYPTO_random_block (&other_uid, + sizeof (other_uid)); + GNUNET_CRYPTO_hash ("object", 6, &hash); + SYNC_object_upload_sign (&priv, + &uid, + &hash, + &sig); + FAILIF (GNUNET_OK != + SYNC_object_upload_verify (&pub, + &uid, + &hash, + &sig)); + FAILIF (GNUNET_OK == + SYNC_object_upload_verify (&pub, + &other_uid, + &hash, + &sig)); + return 0; +} + + +int +main (int argc, + char *const argv[]) +{ + (void) argc; + GNUNET_log_setup (argv[0], + "WARNING", + NULL); + if (0 != test_refs_hash ()) + return 1; + if (0 != test_refs_json_roundtrip ()) + return 1; + if (0 != test_upload_sig ()) + return 1; + if (0 != test_delete_sig ()) + return 1; + if (0 != test_object_sig ()) + return 1; + return 0; +} + + +/* end of test_sync_signatures.c */