/*
This file is part of TALER
(C) 2014-2024 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
*/
/**
* @file taler-merchant-httpd.c
* @brief HTTP serving layer intended to perform crypto-work and
* communication with the exchange
* @author Marcello Stanisci
* @author Christian Grothoff
* @author Florian Dold
* @author Priscilla HUANG
*/
#include "platform.h"
#include
#include
#include
#include
#include
#include "taler-merchant-httpd_config.h"
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_get-orders-ID.h"
#include "taler-merchant-httpd_get-templates-ID.h"
#include "taler-merchant-httpd_mhd.h"
#include "taler-merchant-httpd_private-delete-account-ID.h"
#include "taler-merchant-httpd_private-delete-categories-ID.h"
#include "taler-merchant-httpd_private-delete-instances-ID.h"
#include "taler-merchant-httpd_private-delete-instances-ID-token.h"
#include "taler-merchant-httpd_private-delete-products-ID.h"
#include "taler-merchant-httpd_private-delete-orders-ID.h"
#include "taler-merchant-httpd_private-delete-otp-devices-ID.h"
#include "taler-merchant-httpd_private-delete-templates-ID.h"
#include "taler-merchant-httpd_private-delete-token-families-SLUG.h"
#include "taler-merchant-httpd_private-delete-transfers-ID.h"
#include "taler-merchant-httpd_private-delete-webhooks-ID.h"
#include "taler-merchant-httpd_private-get-accounts.h"
#include "taler-merchant-httpd_private-get-accounts-ID.h"
#include "taler-merchant-httpd_private-get-categories.h"
#include "taler-merchant-httpd_private-get-categories-ID.h"
#include "taler-merchant-httpd_private-get-instances.h"
#include "taler-merchant-httpd_private-get-instances-ID.h"
#include "taler-merchant-httpd_private-get-instances-ID-kyc.h"
#include "taler-merchant-httpd_private-get-pos.h"
#include "taler-merchant-httpd_private-get-products.h"
#include "taler-merchant-httpd_private-get-products-ID.h"
#include "taler-merchant-httpd_private-get-orders.h"
#include "taler-merchant-httpd_private-get-orders-ID.h"
#include "taler-merchant-httpd_private-get-otp-devices.h"
#include "taler-merchant-httpd_private-get-otp-devices-ID.h"
#include "taler-merchant-httpd_private-get-templates.h"
#include "taler-merchant-httpd_private-get-templates-ID.h"
#include "taler-merchant-httpd_private-get-token-families.h"
#include "taler-merchant-httpd_private-get-token-families-SLUG.h"
#include "taler-merchant-httpd_private-get-transfers.h"
#include "taler-merchant-httpd_private-get-webhooks.h"
#include "taler-merchant-httpd_private-get-webhooks-ID.h"
#include "taler-merchant-httpd_private-patch-accounts-ID.h"
#include "taler-merchant-httpd_private-patch-categories-ID.h"
#include "taler-merchant-httpd_private-patch-instances-ID.h"
#include "taler-merchant-httpd_private-patch-orders-ID-forget.h"
#include "taler-merchant-httpd_private-patch-otp-devices-ID.h"
#include "taler-merchant-httpd_private-patch-products-ID.h"
#include "taler-merchant-httpd_private-patch-templates-ID.h"
#include "taler-merchant-httpd_private-patch-token-families-SLUG.h"
#include "taler-merchant-httpd_private-patch-webhooks-ID.h"
#include "taler-merchant-httpd_private-post-account.h"
#include "taler-merchant-httpd_private-post-categories.h"
#include "taler-merchant-httpd_private-post-instances.h"
#include "taler-merchant-httpd_private-post-instances-ID-auth.h"
#include "taler-merchant-httpd_private-post-instances-ID-token.h"
#include "taler-merchant-httpd_private-post-otp-devices.h"
#include "taler-merchant-httpd_private-post-orders.h"
#include "taler-merchant-httpd_private-post-orders-ID-refund.h"
#include "taler-merchant-httpd_private-post-products.h"
#include "taler-merchant-httpd_private-post-products-ID-lock.h"
#include "taler-merchant-httpd_private-post-templates.h"
#include "taler-merchant-httpd_private-post-token-families.h"
#include "taler-merchant-httpd_private-post-transfers.h"
#include "taler-merchant-httpd_private-post-webhooks.h"
#include "taler-merchant-httpd_post-orders-ID-abort.h"
#include "taler-merchant-httpd_post-orders-ID-claim.h"
#include "taler-merchant-httpd_post-orders-ID-paid.h"
#include "taler-merchant-httpd_post-orders-ID-pay.h"
#include "taler-merchant-httpd_post-using-templates.h"
#include "taler-merchant-httpd_post-orders-ID-refund.h"
#include "taler-merchant-httpd_spa.h"
#include "taler-merchant-httpd_statics.h"
/**
* Fixme: document.
*/
#define INSTANCE_STALENESS GNUNET_TIME_UNIT_MINUTES
/**
* Backlog for listen operation on unix-domain sockets.
*/
#define UNIX_BACKLOG 500
/**
* Default maximum upload size permitted. Can be overridden
* per handler.
*/
#define DEFAULT_MAX_UPLOAD_SIZE (16 * 1024)
/**
* Which currency do we use?
*/
char *TMH_currency;
/**
* What is the base URL for this merchant backend? NULL if it is not
* configured and is to be determined from HTTP headers (X-Forwarded-Host and
* X-Forwarded-Port and X-Forwarded-Prefix) of the reverse proxy.
*/
char *TMH_base_url;
/**
* Inform the auditor for all deposit confirmations (global option)
*/
int TMH_force_audit;
/**
* Connection handle to the our database
*/
struct TALER_MERCHANTDB_Plugin *TMH_db;
/**
* Event handler for instance settings changes.
*/
static struct GNUNET_DB_EventHandler *instance_eh;
/**
* Hashmap pointing at merchant instances by 'id'. An 'id' is
* just a string that identifies a merchant instance. When a frontend
* needs to specify an instance to the backend, it does so by 'id'
*/
struct GNUNET_CONTAINER_MultiHashMap *TMH_by_id_map;
/**
* How long do we need to keep information on paid contracts on file for tax
* or other legal reasons? Used to block deletions for younger transaction
* data.
*/
struct GNUNET_TIME_Relative TMH_legal_expiration;
/**
* Length of the TMH_cspecs array.
*/
unsigned int TMH_num_cspecs;
/**
* Rendering specs for currencies.
*/
struct TALER_CurrencySpecification *TMH_cspecs;
/**
* The port we are running on
*/
static uint16_t port;
/**
* Should a "Connection: close" header be added to each HTTP response?
*/
static int merchant_connection_close;
/**
* Context for all exchange operations (useful to the event loop).
*/
struct GNUNET_CURL_Context *TMH_curl_ctx;
/**
* Context for integrating #TMH_curl_ctx with the
* GNUnet event loop.
*/
static struct GNUNET_CURL_RescheduleContext *merchant_curl_rc;
/**
* Global return code
*/
static int global_ret;
/**
* Our configuration.
*/
static const struct GNUNET_CONFIGURATION_Handle *cfg;
/**
* Initial authorization token.
*/
char *TMH_default_auth;
/**
* Check validity of login @a token for the given @a instance_id.
*
* @param token the login token given in the request
* @param instance_id the instance the login is to be checked against
* @param[out] as set to scope of the token if it is valid
* @return TALER_EC_NONE on success
*/
static enum TALER_ErrorCode
TMH_check_token (const char *token,
const char *instance_id,
enum TMH_AuthScope *as)
{
enum TMH_AuthScope scope;
struct GNUNET_TIME_Timestamp expiration;
enum GNUNET_DB_QueryStatus qs;
struct TALER_MERCHANTDB_LoginTokenP btoken;
if (NULL == token)
{
*as = TMH_AS_NONE;
return TALER_EC_NONE;
}
/* This was presumably checked before... */
GNUNET_assert (0 == strncasecmp (token,
RFC_8959_PREFIX,
strlen (RFC_8959_PREFIX)));
token += strlen (RFC_8959_PREFIX);
if (GNUNET_OK !=
GNUNET_STRINGS_string_to_data (token,
strlen (token),
&btoken,
sizeof (btoken)))
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Given authorization token `%s' is malformed\n",
token);
GNUNET_break_op (0);
return TALER_EC_GENERIC_TOKEN_MALFORMED;
}
qs = TMH_db->select_login_token (TMH_db->cls,
instance_id,
&btoken,
&expiration,
&scope);
if (qs < 0)
{
GNUNET_break (0);
return TALER_EC_GENERIC_DB_FETCH_FAILED;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Authorization token `%s' unknown\n",
token);
return TALER_EC_GENERIC_TOKEN_UNKNOWN;
}
if (GNUNET_TIME_absolute_is_past (expiration.abs_time))
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Authorization token `%s' expired\n",
token);
return TALER_EC_GENERIC_TOKEN_EXPIRED;
}
*as = scope;
return TALER_EC_NONE;
}
enum GNUNET_GenericReturnValue
TMH_check_auth (const char *token,
struct TALER_MerchantAuthenticationSaltP *salt,
struct TALER_MerchantAuthenticationHashP *hash)
{
struct GNUNET_HashCode val;
char *dec;
size_t dec_len;
if (GNUNET_is_zero (hash))
return GNUNET_OK;
if (NULL == token)
return GNUNET_SYSERR;
dec_len = GNUNET_STRINGS_urldecode (token,
strlen (token),
&dec);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Checking against token with salt %s\n",
TALER_B2S (salt));
GNUNET_assert (GNUNET_YES ==
GNUNET_CRYPTO_kdf (&val,
sizeof (val),
salt,
sizeof (*salt),
dec,
dec_len,
"merchant-instance-auth",
strlen ("merchant-instance-auth"),
NULL,
0));
GNUNET_free (dec);
return (0 == GNUNET_memcmp (&val,
&hash->hash))
? GNUNET_OK
: GNUNET_SYSERR;
}
void
TMH_compute_auth (const char *token,
struct TALER_MerchantAuthenticationSaltP *salt,
struct TALER_MerchantAuthenticationHashP *hash)
{
GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
salt,
sizeof (*salt));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Computing initial auth using token with salt %s\n",
TALER_B2S (salt));
GNUNET_assert (GNUNET_YES ==
GNUNET_CRYPTO_kdf (hash,
sizeof (*hash),
salt,
sizeof (*salt),
token,
strlen (token),
"merchant-instance-auth",
strlen ("merchant-instance-auth"),
NULL,
0));
}
void
TMH_wire_method_free (struct TMH_WireMethod *wm)
{
GNUNET_free (wm->payto_uri.full_payto);
GNUNET_free (wm->wire_method);
GNUNET_free (wm->credit_facade_url);
json_decref (wm->credit_facade_credentials);
GNUNET_free (wm);
}
void
TMH_instance_decref (struct TMH_MerchantInstance *mi)
{
struct TMH_WireMethod *wm;
mi->rc--;
if (0 != mi->rc)
return;
TMH_force_get_orders_resume (mi);
while (NULL != (wm = (mi->wm_head)))
{
GNUNET_CONTAINER_DLL_remove (mi->wm_head,
mi->wm_tail,
wm);
TMH_wire_method_free (wm);
}
GNUNET_free (mi->settings.id);
GNUNET_free (mi->settings.name);
GNUNET_free (mi->settings.email);
GNUNET_free (mi->settings.website);
GNUNET_free (mi->settings.logo);
json_decref (mi->settings.address);
json_decref (mi->settings.jurisdiction);
GNUNET_free (mi);
}
enum GNUNET_GenericReturnValue
TMH_instance_free_cb (void *cls,
const struct GNUNET_HashCode *key,
void *value)
{
struct TMH_MerchantInstance *mi = value;
(void) cls;
(void) key;
GNUNET_assert (GNUNET_OK ==
GNUNET_CONTAINER_multihashmap_remove (TMH_by_id_map,
&mi->h_instance,
mi));
TMH_instance_decref (mi);
return GNUNET_YES;
}
/**
* Shutdown task (magically invoked when the application is being
* quit)
*
* @param cls NULL
*/
static void
do_shutdown (void *cls)
{
(void) cls;
TMH_force_orders_resume ();
TMH_force_ac_resume ();
TMH_force_pc_resume ();
TMH_force_kyc_resume ();
TMH_force_gorc_resume ();
TMH_force_wallet_get_order_resume ();
TMH_force_wallet_refund_order_resume ();
{
struct MHD_Daemon *mhd;
mhd = TALER_MHD_daemon_stop ();
if (NULL != mhd)
MHD_stop_daemon (mhd);
}
if (NULL != instance_eh)
{
TMH_db->event_listen_cancel (instance_eh);
instance_eh = NULL;
}
TMH_EXCHANGES_done ();
if (NULL != TMH_db)
{
TALER_MERCHANTDB_plugin_unload (TMH_db);
TMH_db = NULL;
}
if (NULL != TMH_by_id_map)
{
GNUNET_CONTAINER_multihashmap_iterate (TMH_by_id_map,
&TMH_instance_free_cb,
NULL);
GNUNET_CONTAINER_multihashmap_destroy (TMH_by_id_map);
TMH_by_id_map = NULL;
}
TALER_TEMPLATING_done ();
if (NULL != TMH_curl_ctx)
{
GNUNET_CURL_fini (TMH_curl_ctx);
TMH_curl_ctx = NULL;
}
if (NULL != merchant_curl_rc)
{
GNUNET_CURL_gnunet_rc_destroy (merchant_curl_rc);
merchant_curl_rc = NULL;
}
}
/**
* 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 TMH_HandlerContext *hc = *con_cls;
(void) cls;
if (NULL == hc)
return;
GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id);
{
#if MHD_VERSION >= 0x00097304
const union MHD_ConnectionInfo *ci;
unsigned int http_status = 0;
ci = MHD_get_connection_info (connection,
MHD_CONNECTION_INFO_HTTP_STATUS);
if (NULL != ci)
http_status = ci->http_status;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Request for `%s' completed with HTTP status %u (%d)\n",
hc->url,
http_status,
toe);
#else
(void) connection;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Finished handling request for `%s' with MHD termination code %d\n",
hc->url,
(int) toe);
#endif
}
if (NULL != hc->cc)
hc->cc (hc->ctx);
TALER_MHD_parse_post_cleanup_callback (hc->json_parse_context);
GNUNET_free (hc->infix);
if (NULL != hc->request_body)
json_decref (hc->request_body);
if (NULL != hc->instance)
TMH_instance_decref (hc->instance);
GNUNET_free (hc->full_url);
GNUNET_free (hc);
*con_cls = NULL;
}
struct TMH_MerchantInstance *
TMH_lookup_instance (const char *instance_id)
{
struct GNUNET_HashCode h_instance;
if (NULL == instance_id)
instance_id = "default";
GNUNET_CRYPTO_hash (instance_id,
strlen (instance_id),
&h_instance);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Looking for by-id key %s of '%s' in hashmap\n",
GNUNET_h2s (&h_instance),
instance_id);
/* We're fine if that returns NULL, the calling routine knows how
to handle that */
return GNUNET_CONTAINER_multihashmap_get (TMH_by_id_map,
&h_instance);
}
/**
* Add instance definition to our active set of instances.
*
* @param[in,out] mi merchant instance details to define
* @return #GNUNET_OK on success, #GNUNET_NO if the same ID is in use already
*/
enum GNUNET_GenericReturnValue
TMH_add_instance (struct TMH_MerchantInstance *mi)
{
const char *id;
int ret;
id = mi->settings.id;
if (NULL == id)
id = "default";
GNUNET_CRYPTO_hash (id,
strlen (id),
&mi->h_instance);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Looking for by-id key %s of `%s' in hashmap\n",
GNUNET_h2s (&mi->h_instance),
id);
ret = GNUNET_CONTAINER_multihashmap_put (TMH_by_id_map,
&mi->h_instance,
mi,
GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY);
if (GNUNET_OK == ret)
{
GNUNET_assert (mi->rc < UINT_MAX);
mi->rc++;
}
return ret;
}
/**
* Handle a OPTIONS "*" request.
*
* @param rh context of the handler
* @param connection the MHD connection to handle
* @param[in,out] hc context with further information about the request
* @return MHD result code
*/
static MHD_RESULT
handle_server_options (const struct TMH_RequestHandler *rh,
struct MHD_Connection *connection,
struct TMH_HandlerContext *hc)
{
(void) rh;
(void) hc;
return TALER_MHD_reply_cors_preflight (connection);
}
/**
* Generates the response for "/", redirecting the
* client to the "/webui/" from where we serve the SPA.
*
* @param rh request handler
* @param connection MHD connection
* @param hc handler context
* @return MHD result code
*/
static MHD_RESULT
spa_redirect (const struct TMH_RequestHandler *rh,
struct MHD_Connection *connection,
struct TMH_HandlerContext *hc)
{
const char *text = "Redirecting to /webui/";
struct MHD_Response *response;
char *dst;
response = MHD_create_response_from_buffer (strlen (text),
(void *) text,
MHD_RESPMEM_PERSISTENT);
if (NULL == response)
{
GNUNET_break (0);
return MHD_NO;
}
TALER_MHD_add_global_headers (response);
GNUNET_break (MHD_YES ==
MHD_add_response_header (response,
MHD_HTTP_HEADER_CONTENT_TYPE,
"text/plain"));
if ( (NULL == hc->instance) ||
(0 == strcmp ("default",
hc->instance->settings.id)) )
dst = GNUNET_strdup ("/webui/");
else
GNUNET_asprintf (&dst,
"/instances/%s/webui/",
hc->instance->settings.id);
if (MHD_NO ==
MHD_add_response_header (response,
MHD_HTTP_HEADER_LOCATION,
dst))
{
GNUNET_break (0);
MHD_destroy_response (response);
GNUNET_free (dst);
return MHD_NO;
}
GNUNET_free (dst);
{
MHD_RESULT ret;
ret = MHD_queue_response (connection,
MHD_HTTP_FOUND,
response);
MHD_destroy_response (response);
return ret;
}
}
/**
* Extract the token from authorization header value @a auth.
*
* @param auth pointer to authorization header value,
* will be updated to point to the start of the token
* or set to NULL if header value is invalid
*/
static void
extract_token (const char **auth)
{
const char *bearer = "Bearer ";
const char *tok = *auth;
if (0 != strncmp (tok,
bearer,
strlen (bearer)))
{
*auth = NULL;
return;
}
tok += strlen (bearer);
while (' ' == *tok)
tok++;
if (0 != strncasecmp (tok,
RFC_8959_PREFIX,
strlen (RFC_8959_PREFIX)))
{
*auth = NULL;
return;
}
*auth = tok;
}
/**
* Checks if the @a rh matches the given (parsed) URL.
*
* @param rh handler to compare against
* @param url the main URL (without "/private/" prefix, if any)
* @param prefix_strlen length of the prefix, i.e. 8 for '/orders/' or 7 for '/config'
* @param infix_url infix text, i.e. "$ORDER_ID".
* @param infix_strlen length of the string in @a infix_url
* @param suffix_url suffix, i.e. "/refund", including the "/"
* @param suffix_strlen number of characters in @a suffix_url
* @return true if @a rh matches this request
*/
static bool
prefix_match (const struct TMH_RequestHandler *rh,
const char *url,
size_t prefix_strlen,
const char *infix_url,
size_t infix_strlen,
const char *suffix_url,
size_t suffix_strlen)
{
if ( (prefix_strlen != strlen (rh->url_prefix)) ||
(0 != memcmp (url,
rh->url_prefix,
prefix_strlen)) )
return false;
if (! rh->have_id_segment)
{
/* Require /$PREFIX/$SUFFIX or /$PREFIX */
if (NULL != suffix_url)
return false; /* too many segments to match */
if ( (NULL == infix_url) /* either or */
^ (NULL == rh->url_suffix) )
return false; /* suffix existence mismatch */
/* If /$PREFIX/$SUFFIX, check $SUFFIX matches */
if ( (NULL != infix_url) &&
( (infix_strlen != strlen (rh->url_suffix)) ||
(0 != memcmp (infix_url,
rh->url_suffix,
infix_strlen)) ) )
return false; /* cannot use infix as suffix: content mismatch */
}
else
{
/* Require /$PREFIX/$ID or /$PREFIX/$ID/$SUFFIX */
if (NULL == infix_url)
return false; /* infix existence mismatch */
if ( ( (NULL == suffix_url)
^ (NULL == rh->url_suffix) ) )
return false; /* suffix existence mismatch */
if ( (NULL != suffix_url) &&
( (suffix_strlen != strlen (rh->url_suffix)) ||
(0 != memcmp (suffix_url,
rh->url_suffix,
suffix_strlen)) ) )
return false; /* suffix content mismatch */
}
return true;
}
/**
* Function called first by MHD with the full URL.
*
* @param cls NULL
* @param full_url the full URL
* @param con MHD connection object
* @return our handler context
*/
static void *
full_url_track_callback (void *cls,
const char *full_url,
struct MHD_Connection *con)
{
struct TMH_HandlerContext *hc;
hc = GNUNET_new (struct TMH_HandlerContext);
GNUNET_async_scope_fresh (&hc->async_scope_id);
GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id);
hc->full_url = GNUNET_strdup (full_url);
return hc;
}
/**
* 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 the MHD connection to 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 set up by the
* full_url_track_callback().
* @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 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 TMH_RequestHandler management_handlers[] = {
/* GET /instances */
{
.url_prefix = "/instances",
.method = MHD_HTTP_METHOD_GET,
.skip_instance = true,
.default_only = true,
.handler = &TMH_private_get_instances
},
/* POST /instances */
{
.url_prefix = "/instances",
.method = MHD_HTTP_METHOD_POST,
.skip_instance = true,
.default_only = true,
.handler = &TMH_private_post_instances,
/* allow instance data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* GET /instances/$ID/ */
{
.url_prefix = "/instances/",
.method = MHD_HTTP_METHOD_GET,
.skip_instance = true,
.default_only = true,
.have_id_segment = true,
.handler = &TMH_private_get_instances_default_ID
},
/* DELETE /instances/$ID */
{
.url_prefix = "/instances/",
.method = MHD_HTTP_METHOD_DELETE,
.skip_instance = true,
.default_only = true,
.have_id_segment = true,
.handler = &TMH_private_delete_instances_default_ID
},
/* PATCH /instances/$ID */
{
.url_prefix = "/instances/",
.method = MHD_HTTP_METHOD_PATCH,
.skip_instance = true,
.default_only = true,
.have_id_segment = true,
.handler = &TMH_private_patch_instances_default_ID,
/* allow instance data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* POST /auth: */
{
.url_prefix = "/instances/",
.url_suffix = "auth",
.method = MHD_HTTP_METHOD_POST,
.skip_instance = true,
.default_only = true,
.have_id_segment = true,
.handler = &TMH_private_post_instances_default_ID_auth,
/* Body should be pretty small. */
.max_upload = 1024 * 1024
},
/* GET /kyc: */
{
.url_prefix = "/instances/",
.url_suffix = "kyc",
.method = MHD_HTTP_METHOD_GET,
.skip_instance = true,
.default_only = true,
.have_id_segment = true,
.handler = &TMH_private_get_instances_default_ID_kyc,
},
{
.url_prefix = NULL
}
};
static struct TMH_RequestHandler private_handlers[] = {
/* GET /instances/$ID/: */
{
.url_prefix = "/",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_instances_ID
},
/* DELETE /instances/$ID/: */
{
.url_prefix = "/",
.method = MHD_HTTP_METHOD_DELETE,
.allow_deleted_instance = true,
.handler = &TMH_private_delete_instances_ID
},
/* PATCH /instances/$ID/: */
{
.url_prefix = "/",
.method = MHD_HTTP_METHOD_PATCH,
.handler = &TMH_private_patch_instances_ID,
.allow_deleted_instance = true,
/* allow instance data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* POST /auth: */
{
.url_prefix = "/auth",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_instances_ID_auth,
/* Body should be pretty small. */
.max_upload = 1024 * 1024,
},
/* GET /kyc: */
{
.url_prefix = "/kyc",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_instances_ID_kyc,
},
/* GET /pos: */
{
.url_prefix = "/pos",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_pos
},
/* GET /categories: */
{
.url_prefix = "/categories",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_categories
},
/* POST /categories: */
{
.url_prefix = "/categories",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_categories,
/* allow category data of up to 8 kb, that should be plenty */
.max_upload = 1024 * 8
},
/* GET /categories/$ID: */
{
.url_prefix = "/categories/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_get_categories_ID
},
/* DELETE /categories/$ID: */
{
.url_prefix = "/categories/",
.method = MHD_HTTP_METHOD_DELETE,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_delete_categories_ID
},
/* PATCH /categories/$ID/: */
{
.url_prefix = "/categories/",
.method = MHD_HTTP_METHOD_PATCH,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_patch_categories_ID,
/* allow category data of up to 8 kb, that should be plenty */
.max_upload = 1024 * 8
},
/* GET /products: */
{
.url_prefix = "/products",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_products
},
/* POST /products: */
{
.url_prefix = "/products",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_products,
/* allow product data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* GET /products/$ID: */
{
.url_prefix = "/products/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_get_products_ID
},
/* DELETE /products/$ID/: */
{
.url_prefix = "/products/",
.method = MHD_HTTP_METHOD_DELETE,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_delete_products_ID
},
/* PATCH /products/$ID/: */
{
.url_prefix = "/products/",
.method = MHD_HTTP_METHOD_PATCH,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_patch_products_ID,
/* allow product data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* POST /products/$ID/lock: */
{
.url_prefix = "/products/",
.url_suffix = "lock",
.method = MHD_HTTP_METHOD_POST,
.have_id_segment = true,
.handler = &TMH_private_post_products_ID_lock,
/* the body should be pretty small, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* POST /orders: */
{
.url_prefix = "/orders",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_orders,
/* allow contracts of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* GET /orders/$ID: */
{
.url_prefix = "/orders/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_get_orders_ID
},
/* GET /orders: */
{
.url_prefix = "/orders",
.method = MHD_HTTP_METHOD_GET,
.allow_deleted_instance = true,
.handler = &TMH_private_get_orders
},
/* POST /orders/$ID/refund: */
{
.url_prefix = "/orders/",
.url_suffix = "refund",
.method = MHD_HTTP_METHOD_POST,
.have_id_segment = true,
.handler = &TMH_private_post_orders_ID_refund,
/* the body should be pretty small, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* PATCH /orders/$ID/forget: */
{
.url_prefix = "/orders/",
.url_suffix = "forget",
.method = MHD_HTTP_METHOD_PATCH,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_patch_orders_ID_forget,
/* the body should be pretty small, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* DELETE /orders/$ID: */
{
.url_prefix = "/orders/",
.method = MHD_HTTP_METHOD_DELETE,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_delete_orders_ID
},
/* POST /transfers: */
{
.url_prefix = "/transfers",
.method = MHD_HTTP_METHOD_POST,
.allow_deleted_instance = true,
.handler = &TMH_private_post_transfers,
/* the body should be pretty small, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* DELETE /transfers/$ID: */
{
.url_prefix = "/transfers/",
.method = MHD_HTTP_METHOD_DELETE,
.allow_deleted_instance = true,
.handler = &TMH_private_delete_transfers_ID,
.have_id_segment = true,
/* the body should be pretty small, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* GET /transfers: */
{
.url_prefix = "/transfers",
.method = MHD_HTTP_METHOD_GET,
.allow_deleted_instance = true,
.handler = &TMH_private_get_transfers
},
/* POST /otp-devices: */
{
.url_prefix = "/otp-devices",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_otp_devices
},
/* GET /otp-devices: */
{
.url_prefix = "/otp-devices",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_otp_devices
},
/* GET /otp-devices/$ID/: */
{
.url_prefix = "/otp-devices/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.handler = &TMH_private_get_otp_devices_ID
},
/* DELETE /otp-devices/$ID/: */
{
.url_prefix = "/otp-devices/",
.method = MHD_HTTP_METHOD_DELETE,
.have_id_segment = true,
.handler = &TMH_private_delete_otp_devices_ID
},
/* PATCH /otp-devices/$ID/: */
{
.url_prefix = "/otp-devices/",
.method = MHD_HTTP_METHOD_PATCH,
.have_id_segment = true,
.handler = &TMH_private_patch_otp_devices_ID
},
/* POST /templates: */
{
.url_prefix = "/templates",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_templates,
/* allow template data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* GET /templates: */
{
.url_prefix = "/templates",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_templates
},
/* GET /templates/$ID/: */
{
.url_prefix = "/templates/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_get_templates_ID
},
/* DELETE /templates/$ID/: */
{
.url_prefix = "/templates/",
.method = MHD_HTTP_METHOD_DELETE,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_delete_templates_ID
},
/* PATCH /templates/$ID/: */
{
.url_prefix = "/templates/",
.method = MHD_HTTP_METHOD_PATCH,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_patch_templates_ID,
/* allow template data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* GET /webhooks: */
{
.url_prefix = "/webhooks",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_webhooks
},
/* POST /webhooks: */
{
.url_prefix = "/webhooks",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_webhooks,
/* allow webhook data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* GET /webhooks/$ID/: */
{
.url_prefix = "/webhooks/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_get_webhooks_ID
},
/* DELETE /webhooks/$ID/: */
{
.url_prefix = "/webhooks/",
.method = MHD_HTTP_METHOD_DELETE,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_delete_webhooks_ID
},
/* PATCH /webhooks/$ID/: */
{
.url_prefix = "/webhooks/",
.method = MHD_HTTP_METHOD_PATCH,
.have_id_segment = true,
.allow_deleted_instance = true,
.handler = &TMH_private_patch_webhooks_ID,
/* allow webhook data of up to 8 MB, that should be plenty;
note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB)
would require further changes to the allocation logic
in the code... */
.max_upload = 1024 * 1024 * 8
},
/* POST /accounts: */
{
.url_prefix = "/accounts",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_account,
/* allow account details of up to 8 kb, that should be plenty */
.max_upload = 1024 * 8
},
/* PATCH /accounts/$H_WIRE: */
{
.url_prefix = "/accounts/",
.method = MHD_HTTP_METHOD_PATCH,
.handler = &TMH_private_patch_accounts_ID,
.have_id_segment = true,
/* allow account details of up to 8 kb, that should be plenty */
.max_upload = 1024 * 8
},
/* GET /accounts: */
{
.url_prefix = "/accounts",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_accounts
},
/* GET /accounts/$H_WIRE: */
{
.url_prefix = "/accounts/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.handler = &TMH_private_get_accounts_ID
},
/* DELETE /accounts/$H_WIRE: */
{
.url_prefix = "/accounts/",
.method = MHD_HTTP_METHOD_DELETE,
.handler = &TMH_private_delete_account_ID,
.have_id_segment = true
},
/* POST /token: */
{
.url_prefix = "/token",
.auth_scope = TMH_AS_REFRESHABLE,
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_instances_ID_token,
/* Body should be tiny. */
.max_upload = 1024
},
/* DELETE /token: */
{
.url_prefix = "/token",
.auth_scope = TMH_AS_READ_ONLY,
.method = MHD_HTTP_METHOD_DELETE,
.handler = &TMH_private_delete_instances_ID_token,
},
/* GET /tokenfamilies: */
{
.url_prefix = "/tokenfamilies",
.method = MHD_HTTP_METHOD_GET,
.handler = &TMH_private_get_tokenfamilies
},
/* POST /tokenfamilies: */
{
.url_prefix = "/tokenfamilies",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_private_post_token_families
},
/* GET /tokenfamilies/$SLUG/: */
{
.url_prefix = "/tokenfamilies/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.handler = &TMH_private_get_tokenfamilies_SLUG
},
/* DELETE /tokenfamilies/$SLUG/: */
{
.url_prefix = "/tokenfamilies/",
.method = MHD_HTTP_METHOD_DELETE,
.have_id_segment = true,
.handler = &TMH_private_delete_token_families_SLUG
},
/* PATCH /tokenfamilies/$SLUG/: */
{
.url_prefix = "/tokenfamilies/",
.method = MHD_HTTP_METHOD_PATCH,
.have_id_segment = true,
.handler = &TMH_private_patch_token_family_SLUG,
},
{
.url_prefix = NULL
}
};
static struct TMH_RequestHandler public_handlers[] = {
{
/* for "default" instance, it does not even
have to exist before we give the WebUI */
.url_prefix = "/",
.method = MHD_HTTP_METHOD_GET,
.mime_type = "text/html",
.skip_instance = true,
.default_only = true,
.handler = &spa_redirect,
.response_code = MHD_HTTP_FOUND
},
{
/* for "normal" instance,s they must exist
before we give the WebUI */
.url_prefix = "/",
.method = MHD_HTTP_METHOD_GET,
.mime_type = "text/html",
.handler = &spa_redirect,
.response_code = MHD_HTTP_FOUND
},
{
.url_prefix = "/webui/",
.method = MHD_HTTP_METHOD_GET,
.mime_type = "text/html",
.skip_instance = true,
.have_id_segment = true,
.handler = &TMH_return_spa,
.response_code = MHD_HTTP_OK
},
{
.url_prefix = "/agpl",
.method = MHD_HTTP_METHOD_GET,
.skip_instance = true,
.handler = &TMH_MHD_handler_agpl_redirect
},
{
.url_prefix = "/config",
.method = MHD_HTTP_METHOD_GET,
.skip_instance = true,
.default_only = true,
.handler = &MH_handler_config
},
/* Also serve the same /config per instance */
{
.url_prefix = "/config",
.method = MHD_HTTP_METHOD_GET,
.skip_instance = false,
.allow_deleted_instance = true,
.handler = &MH_handler_config
},
/* POST /orders/$ID/abort: */
{
.url_prefix = "/orders/",
.have_id_segment = true,
.url_suffix = "abort",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_post_orders_ID_abort,
/* wallet may give us many coins to sign, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* POST /orders/$ID/claim: */
{
.url_prefix = "/orders/",
.have_id_segment = true,
.url_suffix = "claim",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_post_orders_ID_claim,
/* the body should be pretty small, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* POST /orders/$ID/pay: */
{
.url_prefix = "/orders/",
.have_id_segment = true,
.url_suffix = "pay",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_post_orders_ID_pay,
/* wallet may give us many coins to sign, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* POST /orders/$ID/paid: */
{
.url_prefix = "/orders/",
.have_id_segment = true,
.allow_deleted_instance = true,
.url_suffix = "paid",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_post_orders_ID_paid,
/* the body should be pretty small, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* POST /orders/$ID/refund: */
{
.url_prefix = "/orders/",
.have_id_segment = true,
.allow_deleted_instance = true,
.url_suffix = "refund",
.method = MHD_HTTP_METHOD_POST,
.handler = &TMH_post_orders_ID_refund,
/* the body should be pretty small, allow 1 MB of upload
to set a conservative bound for sane wallets */
.max_upload = 1024 * 1024
},
/* GET /orders/$ID: */
{
.url_prefix = "/orders/",
.method = MHD_HTTP_METHOD_GET,
.allow_deleted_instance = true,
.have_id_segment = true,
.handler = &TMH_get_orders_ID
},
/* GET /static/ *: */
{
.url_prefix = "/static/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.handler = &TMH_return_static
},
/* GET /templates/$ID/: */
{
.url_prefix = "/templates/",
.method = MHD_HTTP_METHOD_GET,
.have_id_segment = true,
.handler = &TMH_get_templates_ID
},
/* POST /templates/$ID: */
{
.url_prefix = "/templates/",
.method = MHD_HTTP_METHOD_POST,
.have_id_segment = true,
.handler = &TMH_post_using_templates_ID,
.max_upload = 1024 * 1024
},
{
.url_prefix = "*",
.method = MHD_HTTP_METHOD_OPTIONS,
.handler = &handle_server_options
},
{
.url_prefix = NULL
}
};
struct TMH_HandlerContext *hc = *con_cls;
struct TMH_RequestHandler *handlers;
bool use_default = false;
(void) cls;
(void) version;
if (NULL != hc->url)
{
/* MHD calls us again for a request, for first call
see 'else' case below */
GNUNET_assert (NULL != hc->rh);
GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id);
if ( (hc->has_body) &&
(NULL == hc->request_body) )
{
size_t mul = hc->rh->max_upload;
enum GNUNET_GenericReturnValue res;
if (0 == mul)
mul = DEFAULT_MAX_UPLOAD_SIZE;
if ( (hc->total_upload + *upload_data_size < hc->total_upload) ||
(hc->total_upload + *upload_data_size > mul) )
{
/* Client exceeds upload limit. Should _usually_ be checked earlier
when we look at the MHD_HTTP_HEADER_CONTENT_LENGTH, alas with
chunked encoding an uploader MAY have omitted this, and thus
not permitted us to check on time. In this case, we just close
the connection once it exceeds our limit (instead of waiting
for the upload to complete and then fail). This could theoretically
cause some clients to retry, alas broken or malicious clients
are likely to retry anyway, so little we can do about it, and
failing earlier seems the best option here. */
GNUNET_break_op (0);
return MHD_NO;
}
hc->total_upload += *upload_data_size;
res = TALER_MHD_parse_post_json (connection,
&hc->json_parse_context,
upload_data,
upload_data_size,
&hc->request_body);
if (GNUNET_SYSERR == res)
return MHD_NO;
/* A error response was already generated */
if ( (GNUNET_NO == res) ||
/* or, need more data to accomplish parsing */
(NULL == hc->request_body) )
return MHD_YES; /* let MHD call us *again* */
}
/* Upload complete (if any), call handler to generate reply */
return hc->rh->handler (hc->rh,
connection,
hc);
}
hc->url = url;
{
const char *correlation_id;
correlation_id = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
"Taler-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;
}
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 == strcasecmp (method,
MHD_HTTP_METHOD_HEAD))
method = MHD_HTTP_METHOD_GET; /* MHD will deal with the rest */
/* Find out the merchant backend instance for the request.
* If there is an instance, remove the instance specification
* from the beginning of the request URL. */
{
const char *instance_prefix = "/instances/";
if (0 == strncmp (url,
instance_prefix,
strlen (instance_prefix)))
{
/* url starts with "/instances/" */
const char *istart = url + strlen (instance_prefix);
const char *slash = strchr (istart, '/');
char *instance_id;
if (NULL == slash)
instance_id = GNUNET_strdup (istart);
else
instance_id = GNUNET_strndup (istart,
slash - istart);
if (0 == strcmp (instance_id,
"default"))
{
MHD_RESULT ret;
struct MHD_Response *response;
const char *rstart = hc->full_url + strlen (instance_prefix);
const char *rslash = strchr (rstart, '/');
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Client used deprecated '/instances/default/' path. Redirecting to modern path\n");
response
= MHD_create_response_from_buffer (0,
NULL,
MHD_RESPMEM_PERSISTENT);
TALER_MHD_add_global_headers (response);
if (MHD_NO ==
MHD_add_response_header (response,
MHD_HTTP_HEADER_LOCATION,
rslash))
{
GNUNET_break (0);
MHD_destroy_response (response);
return MHD_NO;
}
ret = MHD_queue_response (connection,
MHD_HTTP_PERMANENT_REDIRECT,
response);
MHD_destroy_response (response);
return ret;
}
hc->instance = TMH_lookup_instance (instance_id);
if ( (NULL == hc->instance) &&
(0 == strcmp ("default",
instance_id)) )
hc->instance = TMH_lookup_instance (NULL);
if ( (0 == strcmp ("default",
instance_id)) &&
(NULL != TMH_default_auth) &&
(NULL != hc->instance) )
{
/* Override default instance access control */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Command-line override of access control\n");
TMH_compute_auth (TMH_default_auth,
&hc->instance->auth.auth_salt,
&hc->instance->auth.auth_hash);
hc->instance->auth_override = true;
GNUNET_free (TMH_default_auth);
}
GNUNET_free (instance_id);
if (NULL == slash)
url = "";
else
url = slash;
}
else
{
/* use 'default' */
use_default = true;
hc->instance = TMH_lookup_instance (NULL);
if ( (NULL != TMH_default_auth) &&
(NULL != hc->instance) )
{
/* Override default instance access control */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Command-line override of access control\n");
TMH_compute_auth (TMH_default_auth,
&hc->instance->auth.auth_salt,
&hc->instance->auth.auth_hash);
hc->instance->auth_override = true;
GNUNET_free (TMH_default_auth);
}
}
if (NULL != hc->instance)
{
GNUNET_assert (hc->instance->rc < UINT_MAX);
hc->instance->rc++;
}
}
{
const char *management_prefix = "/management/";
const char *private_prefix = "/private/";
if ( (0 == strncmp (url,
management_prefix,
strlen (management_prefix))) )
{
handlers = management_handlers;
url += strlen (management_prefix) - 1;
}
else if ( (0 == strncmp (url,
private_prefix,
strlen (private_prefix))) ||
(0 == strcmp (url,
"/private")) )
{
handlers = private_handlers;
if (0 == strcmp (url,
"/private"))
url = "/";
else
url += strlen (private_prefix) - 1;
}
else
{
handlers = public_handlers;
}
}
if (0 == strcmp (url,
""))
url = "/"; /* code below does not like empty string */
{
/* Matching URL found, but maybe method doesn't match */
size_t prefix_strlen; /* i.e. 8 for "/orders/", or 7 for "/config" */
const char *infix_url = NULL; /* i.e. "$ORDER_ID", no '/'-es */
size_t infix_strlen = 0; /* number of characters in infix_url */
const char *suffix_url = NULL; /* i.e. "refund", excludes '/' at the beginning */
size_t suffix_strlen = 0; /* number of characters in suffix_url */
/* parse the URL into the three different components */
{
const char *slash;
slash = strchr (&url[1], '/');
if (NULL == slash)
{
/* the prefix was everything */
prefix_strlen = strlen (url);
}
else
{
prefix_strlen = slash - url + 1; /* includes both '/'-es if present! */
infix_url = slash + 1;
slash = strchr (infix_url, '/');
if (NULL == slash)
{
/* the infix was the rest */
infix_strlen = strlen (infix_url);
}
else
{
infix_strlen = slash - infix_url; /* excludes both '/'-es */
suffix_url = slash + 1; /* skip the '/' */
suffix_strlen = strlen (suffix_url);
}
hc->infix = GNUNET_strndup (infix_url,
infix_strlen);
}
}
/* find matching handler */
{
bool url_found = false;
for (unsigned int i = 0; NULL != handlers[i].url_prefix; i++)
{
struct TMH_RequestHandler *rh = &handlers[i];
if (rh->default_only && (! use_default))
continue;
if (! prefix_match (rh,
url,
prefix_strlen,
infix_url,
infix_strlen,
suffix_url,
suffix_strlen))
continue;
url_found = true;
if (0 == strcasecmp (method,
MHD_HTTP_METHOD_OPTIONS))
{
return TALER_MHD_reply_cors_preflight (connection);
}
if ( (rh->method != NULL) &&
(0 != strcasecmp (method,
rh->method)) )
continue;
hc->rh = rh;
break;
}
/* Handle HTTP 405: METHOD NOT ALLOWED case */
if ( (NULL == hc->rh) &&
(url_found) )
{
struct MHD_Response *reply;
MHD_RESULT ret;
char *allowed = NULL;
GNUNET_break_op (0);
/* compute 'Allowed:' header (required by HTTP spec for 405 replies) */
for (unsigned int i = 0; NULL != handlers[i].url_prefix; i++)
{
struct TMH_RequestHandler *rh = &handlers[i];
if (rh->default_only && (! use_default))
continue;
if (! prefix_match (rh,
url,
prefix_strlen,
infix_url,
infix_strlen,
suffix_url,
suffix_strlen))
continue;
if (NULL == allowed)
{
allowed = GNUNET_strdup (rh->method);
}
else
{
char *tmp;
GNUNET_asprintf (&tmp,
"%s, %s",
allowed,
rh->method);
GNUNET_free (allowed);
allowed = tmp;
}
if (0 == strcasecmp (rh->method,
MHD_HTTP_METHOD_GET))
{
char *tmp;
GNUNET_asprintf (&tmp,
"%s, %s",
allowed,
MHD_HTTP_METHOD_HEAD);
GNUNET_free (allowed);
allowed = tmp;
}
}
reply = TALER_MHD_make_error (TALER_EC_GENERIC_METHOD_INVALID,
method);
GNUNET_break (MHD_YES ==
MHD_add_response_header (reply,
MHD_HTTP_HEADER_ALLOW,
allowed));
GNUNET_free (allowed);
ret = MHD_queue_response (connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
reply);
MHD_destroy_response (reply);
return ret;
}
if (NULL == hc->rh)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Endpoint `%s' not known\n",
hc->url);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
hc->url);
}
}
}
/* At this point, we must have found a handler */
GNUNET_assert (NULL != hc->rh);
/* If an instance should be there, check one exists */
if ( (NULL == hc->instance) &&
(! hc->rh->skip_instance) )
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Instance for `%s' not known\n",
hc->url);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
hc->infix);
}
/* Access control for non-public handlers */
if (public_handlers != handlers)
{
const char *auth;
bool auth_ok;
bool auth_malformed = false;
/* PATCHing an instance can alternatively be checked against
the default instance */
auth = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_AUTHORIZATION);
if (NULL != auth)
{
/* We _only_ complain about malformed auth headers if
authorization was truly required (#6737). This helps
in case authorization was disabled in the backend
because some reverse proxy is already doing it, and
then that reverse proxy may forward malformed auth
headers to the backend. */
extract_token (&auth);
if (NULL == auth)
auth_malformed = true;
hc->auth_token = auth;
}
/* If we have zero configured instances (not even ones that have been
purged) AND no override credentials, THEN we accept anything (no access
control), as we then also have no data to protect. */
auth_ok = ( (0 ==
GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) &&
(NULL == TMH_default_auth) );
/* Check against selected instance, if we have one */
if (NULL != hc->instance)
auth_ok |= (GNUNET_OK ==
TMH_check_auth (auth,
&hc->instance->auth.auth_salt,
&hc->instance->auth.auth_hash));
else /* Are the credentials provided OK for CLI override? */
auth_ok |= (use_default &&
(NULL != TMH_default_auth) &&
(NULL != auth) &&
(! auth_malformed) &&
(0 == strcmp (auth,
TMH_default_auth)) );
if (auth_ok)
{
hc->auth_scope = TMH_AS_ALL;
}
else
{
if (NULL != hc->instance)
{
enum TALER_ErrorCode ec;
ec = TMH_check_token (auth,
hc->instance->settings.id,
&hc->auth_scope);
if (TALER_EC_NONE != ec)
return TALER_MHD_reply_with_ec (connection,
ec,
NULL);
}
else
hc->auth_scope = TMH_AS_NONE;
}
/* We grant access if:
- scope is 'all'
- rh has an explicit non-NONE scope that matches
- scope is 'read only' and we have a GET request */
if (! ( (TMH_AS_ALL == hc->auth_scope) ||
( (TMH_AS_NONE != hc->rh->auth_scope) &&
(hc->rh->auth_scope == (hc->rh->auth_scope & hc->auth_scope)) ) ||
( (TMH_AS_READ_ONLY == (hc->auth_scope & TMH_AS_READ_ONLY)) &&
(0 == strcmp (MHD_HTTP_METHOD_GET,
method)) ) ) )
{
if (auth_malformed &&
(TMH_AS_NONE == hc->auth_scope) )
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_UNAUTHORIZED,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"'" RFC_8959_PREFIX
"' prefix or 'Bearer' missing in 'Authorization' header");
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_UNAUTHORIZED,
TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
"Check 'Authorization' header");
}
} /* if (use_private) */
if ( (NULL == hc->instance) &&
(! hc->rh->skip_instance) )
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Instance for URL `%s' not known\n",
url);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
url);
}
if ( (NULL != hc->instance) && /* make static analysis happy */
(! hc->rh->skip_instance) &&
(hc->instance->deleted) &&
(! hc->rh->allow_deleted_instance) )
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Instance `%s' was deleted\n",
hc->instance->settings.id);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_MERCHANT_GENERIC_INSTANCE_DELETED,
hc->instance->settings.id);
}
/* parse request body */
hc->has_body = ( (0 == strcasecmp (method,
MHD_HTTP_METHOD_POST)) ||
/* PUT is not yet used */
(0 == strcasecmp (method,
MHD_HTTP_METHOD_PATCH)) );
if (hc->has_body)
{
TALER_MHD_check_content_length (connection,
0 == hc->rh->max_upload
? DEFAULT_MAX_UPLOAD_SIZE
: hc->rh->max_upload);
GNUNET_break (NULL == hc->request_body); /* can't have it already */
}
return MHD_YES; /* wait for MHD to call us again */
}
/**
* Callback invoked with information about a bank account.
*
* @param cls closure with a `struct TMH_MerchantInstance *`
* @param merchant_priv private key of the merchant instance
* @param acc details about the account
*/
static void
add_account_cb (void *cls,
const struct TALER_MerchantPrivateKeyP *merchant_priv,
const struct TALER_MERCHANTDB_AccountDetails *acc)
{
struct TMH_MerchantInstance *mi = cls;
struct TMH_WireMethod *wm;
(void) merchant_priv;
wm = GNUNET_new (struct TMH_WireMethod);
wm->h_wire = acc->h_wire;
wm->payto_uri.full_payto
= GNUNET_strdup (acc->payto_uri.full_payto);
wm->wire_salt = acc->salt;
wm->wire_method
= TALER_payto_get_method (acc->payto_uri.full_payto);
wm->active = acc->active;
GNUNET_CONTAINER_DLL_insert (mi->wm_head,
mi->wm_tail,
wm);
}
/**
* Function called during startup to add all known instances to our
* hash map in memory for faster lookups when we receive requests.
*
* @param cls closure, NULL, unused
* @param merchant_pub public key of the instance
* @param merchant_priv private key of the instance, NULL if not available
* @param is detailed configuration settings for the instance
* @param ias authentication settings for the instance
*/
static void
add_instance_cb (void *cls,
const struct TALER_MerchantPublicKeyP *merchant_pub,
const struct TALER_MerchantPrivateKeyP *merchant_priv,
const struct TALER_MERCHANTDB_InstanceSettings *is,
const struct TALER_MERCHANTDB_InstanceAuthSettings *ias)
{
struct TMH_MerchantInstance *mi;
enum GNUNET_DB_QueryStatus qs;
(void) cls;
mi = TMH_lookup_instance (is->id);
if (NULL != mi)
{
/* (outdated) entry exists, remove old entry */
(void) TMH_instance_free_cb (NULL,
&mi->h_instance,
mi);
}
mi = GNUNET_new (struct TMH_MerchantInstance);
mi->settings = *is;
mi->auth = *ias;
mi->settings.id = GNUNET_strdup (mi->settings.id);
mi->settings.name = GNUNET_strdup (mi->settings.name);
if (NULL != mi->settings.email)
mi->settings.email = GNUNET_strdup (mi->settings.email);
if (NULL != mi->settings.website)
mi->settings.website = GNUNET_strdup (mi->settings.website);
if (NULL != mi->settings.logo)
mi->settings.logo = GNUNET_strdup (mi->settings.logo);
mi->settings.address = json_incref (mi->settings.address);
mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction);
if (NULL != merchant_priv)
mi->merchant_priv = *merchant_priv;
else
mi->deleted = true;
mi->merchant_pub = *merchant_pub;
qs = TMH_db->select_accounts (TMH_db->cls,
mi->settings.id,
&add_account_cb,
mi);
if (0 > qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Error loading accounts of `%s' from database\n",
mi->settings.id);
}
GNUNET_assert (GNUNET_OK ==
TMH_add_instance (mi));
}
/**
* Trigger (re)loading of instance settings from DB.
*
* @param cls NULL
* @param extra ID of the instance that changed, NULL
* to load all instances (will not handle purges!)
* @param extra_len number of bytes in @a extra
*/
static void
load_instances (void *cls,
const void *extra,
size_t extra_len)
{
enum GNUNET_DB_QueryStatus qs;
const char *id = extra;
(void) cls;
if ( (NULL != extra) &&
( (0 == extra_len) ||
('\0' != id[extra_len - 1]) ) )
{
GNUNET_break (0 == extra_len);
extra = NULL;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Received instance settings notification: reload `%s'\n",
id);
if (NULL == extra)
{
qs = TMH_db->lookup_instances (TMH_db->cls,
false,
&add_instance_cb,
NULL);
}
else
{
struct TMH_MerchantInstance *mi;
/* This must be done here to handle instance
purging, as for purged instances, the DB
lookup below will otherwise do nothing */
mi = TMH_lookup_instance (id);
if (NULL != mi)
{
(void) TMH_instance_free_cb (NULL,
&mi->h_instance,
mi);
}
qs = TMH_db->lookup_instance (TMH_db->cls,
id,
false,
&add_instance_cb,
NULL);
}
if (0 > qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed initialization. Check database setup.\n");
global_ret = EXIT_NOPERMISSION;
GNUNET_SCHEDULER_shutdown ();
return;
}
}
/**
* A transaction modified an instance setting (or created/deleted/purged
* one). Notify all backends about the change.
*
* @param id ID of the instance that changed
*/
void
TMH_reload_instances (const char *id)
{
struct GNUNET_DB_EventHeaderP es = {
.size = ntohs (sizeof (es)),
.type = ntohs (TALER_DBEVENT_MERCHANT_INSTANCE_SETTINGS)
};
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Generating instance settings notification: reload `%s'\n",
id);
TMH_db->event_notify (TMH_db->cls,
&es,
id,
(NULL == id)
? 0
: strlen (id) + 1);
}
/**
* 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)
{
int fh;
enum TALER_MHD_GlobalOptions go;
int elen;
const char *tok;
(void) cls;
(void) args;
(void) cfgfile;
tok = getenv ("TALER_MERCHANT_TOKEN");
if ( (NULL != tok) &&
(NULL == TMH_default_auth) )
TMH_default_auth = GNUNET_strdup (tok);
if ( (NULL != TMH_default_auth) &&
(0 != strncmp (TMH_default_auth,
RFC_8959_PREFIX,
strlen (RFC_8959_PREFIX))) )
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Authentication token does not start with `%s' prefix\n",
RFC_8959_PREFIX);
global_ret = EXIT_NOTCONFIGURED;
GNUNET_SCHEDULER_shutdown ();
return;
}
cfg = config;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Starting taler-merchant-httpd\n");
go = TALER_MHD_GO_NONE;
if (merchant_connection_close)
go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE;
TALER_MHD_setup (go);
global_ret = EXIT_SUCCESS;
GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
NULL);
TMH_curl_ctx
= GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
&merchant_curl_rc);
if (NULL == TMH_curl_ctx)
{
GNUNET_break (0);
global_ret = EXIT_NO_RESTART;
GNUNET_SCHEDULER_shutdown ();
return;
}
merchant_curl_rc = GNUNET_CURL_gnunet_rc_create (TMH_curl_ctx);
/* Disable 100 continue processing */
GNUNET_break (GNUNET_OK ==
GNUNET_CURL_append_header (TMH_curl_ctx,
MHD_HTTP_HEADER_EXPECT ":"));
GNUNET_CURL_enable_async_scope_header (TMH_curl_ctx,
"Taler-Correlation-Id");
if (GNUNET_SYSERR ==
TALER_config_get_currency (cfg,
&TMH_currency))
{
GNUNET_SCHEDULER_shutdown ();
return;
}
/* TODO: Load config variables for merchant token family
cipher type "rsa" or "cs" and key size.
Defaults to "rsa" and 2048 bits. */
if (GNUNET_OK !=
TALER_CONFIG_parse_currencies (cfg,
TMH_currency,
&TMH_num_cspecs,
&TMH_cspecs))
{
GNUNET_SCHEDULER_shutdown ();
return;
}
if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_time (cfg,
"merchant",
"LEGAL_PRESERVATION",
&TMH_legal_expiration))
{
GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
"merchant",
"LEGAL_PRESERVATION");
GNUNET_SCHEDULER_shutdown ();
return;
}
if (GNUNET_OK ==
GNUNET_CONFIGURATION_get_value_string (cfg,
"merchant",
"BASE_URL",
&TMH_base_url))
{
if (! TALER_is_web_url (TMH_base_url))
{
GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
"merchant",
"BASE_URL",
"Needs to start with 'http://' or 'https://'");
GNUNET_SCHEDULER_shutdown ();
return;
}
}
if (GNUNET_YES ==
GNUNET_CONFIGURATION_get_value_yesno (cfg,
"merchant",
"FORCE_AUDIT"))
TMH_force_audit = GNUNET_YES;
TALER_TEMPLATING_init ("merchant");
if (GNUNET_OK !=
TMH_spa_init ())
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to load single page app\n");
GNUNET_SCHEDULER_shutdown ();
return;
}
/* /static/ is currently not used */
/* (void) TMH_statics_init (); */
if (NULL ==
(TMH_by_id_map = GNUNET_CONTAINER_multihashmap_create (4,
GNUNET_YES)))
{
GNUNET_SCHEDULER_shutdown ();
return;
}
if (NULL ==
(TMH_db = TALER_MERCHANTDB_plugin_load (cfg)))
{
GNUNET_SCHEDULER_shutdown ();
return;
}
if (GNUNET_OK !=
TMH_db->connect (TMH_db->cls))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to connect to database. Consider running taler-merchant-dbinit!\n");
GNUNET_SCHEDULER_shutdown ();
return;
}
elen = TMH_EXCHANGES_init (config);
if (GNUNET_SYSERR == elen)
{
GNUNET_SCHEDULER_shutdown ();
return;
}
if (0 == elen)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Fatal: no trusted exchanges configured. Exiting.\n");
GNUNET_SCHEDULER_shutdown ();
return;
}
{
struct GNUNET_DB_EventHeaderP es = {
.size = ntohs (sizeof (es)),
.type = ntohs (TALER_DBEVENT_MERCHANT_INSTANCE_SETTINGS)
};
instance_eh = TMH_db->event_listen (TMH_db->cls,
&es,
GNUNET_TIME_UNIT_FOREVER_REL,
&load_instances,
NULL);
}
load_instances (NULL,
NULL,
0);
fh = TALER_MHD_bind (cfg,
"merchant",
&port);
if ( (0 == port) &&
(-1 == fh) )
{
GNUNET_SCHEDULER_shutdown ();
return;
}
{
struct MHD_Daemon *mhd;
mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK
| MHD_USE_AUTO,
port,
NULL, NULL,
&url_handler, NULL,
MHD_OPTION_LISTEN_SOCKET, fh,
MHD_OPTION_URI_LOG_CALLBACK,
&full_url_track_callback, NULL,
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. Is the port in use?\n");
GNUNET_SCHEDULER_shutdown ();
return;
}
global_ret = EXIT_SUCCESS;
TALER_MHD_daemon_start (mhd);
}
}
/**
* The main function of the serve tool
*
* @param argc number of arguments from the command line
* @param argv command line arguments
* @return 0 ok, non-zero on error
*/
int
main (int argc,
char *const *argv)
{
enum GNUNET_GenericReturnValue res;
struct GNUNET_GETOPT_CommandLineOption options[] = {
GNUNET_GETOPT_option_flag ('C',
"connection-close",
"force HTTP connections to be closed after each request",
&merchant_connection_close),
GNUNET_GETOPT_option_timetravel ('T',
"timetravel"),
GNUNET_GETOPT_option_string ('a',
"auth",
"TOKEN",
"use TOKEN to initially authenticate access to the default instance (you can also set the TALER_MERCHANT_TOKEN environment variable instead)",
&TMH_default_auth),
GNUNET_GETOPT_option_version (PACKAGE_VERSION "-" VCS_VERSION),
GNUNET_GETOPT_OPTION_END
};
TALER_OS_init ();
res = GNUNET_PROGRAM_run (argc, argv,
"taler-merchant-httpd",
"Taler merchant's HTTP backend interface",
options,
&run, NULL);
if (GNUNET_SYSERR == res)
return EXIT_INVALIDARGUMENT;
if (GNUNET_NO == res)
return EXIT_SUCCESS;
return global_ret;
}