anastasis_api_config.c (13729B)
1 /* 2 This file is part of Anastasis 3 Copyright (C) 2020, 2021 Anastasis SARL 4 5 Anastasis is free software; you can redistribute it and/or modify it under the 6 terms of the GNU General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file restclient/anastasis_api_config.c 18 * @brief Implementation of the /config GET 19 * @author Christian Grothoff 20 * @author Dennis Neufeld 21 * @author Dominik Meister 22 */ 23 #include "platform.h" 24 #include <curl/curl.h> 25 #include <microhttpd.h> /* just for HTTP status codes */ 26 #include "anastasis_service.h" 27 #include "anastasis_api_curl_defaults.h" 28 #include <gnunet/gnunet_json_lib.h> 29 #include <taler/taler_json_lib.h> 30 31 32 /** 33 * Which version of the Taler protocol is implemented 34 * by this library? Used to determine compatibility. 35 * 36 * Raised to 1 when the key derivation was fixed to use the full context 37 * string: every key derived by a 0:x:x client differs, so backups written by 38 * one side cannot be recovered by the other. Hence 0 must stay outside of 39 * the compatibility range. 40 * 41 * Raised to 3 for multi-currency pricing, which only *added* the plural 42 * fee fields and "currencies" to /config. The age therefore stays at 1: 43 * a version 2 provider is still usable, we just synthesize the plural 44 * fields from the scalar ones below. 45 */ 46 #define ANASTASIS_PROTOCOL_CURRENT 3 47 48 /** 49 * How many versions are we backwards compatible with? 50 */ 51 #define ANASTASIS_PROTOCOL_AGE 1 52 53 54 struct ANASTASIS_ConfigOperation 55 { 56 /** 57 * The url for this request. 58 */ 59 char *url; 60 61 /** 62 * Handle for the request. 63 */ 64 struct GNUNET_CURL_Job *job; 65 66 /** 67 * Reference to the execution context. 68 */ 69 struct GNUNET_CURL_Context *ctx; 70 71 /** 72 * The callback to pass the backend response to 73 */ 74 ANASTASIS_ConfigCallback cb; 75 76 /** 77 * Closure for @a cb. 78 */ 79 void *cb_cls; 80 81 }; 82 83 84 /** 85 * Parse a successful GET /config response and hand it to @a cb. 86 * 87 * Kept apart from #handle_config_finished() because the price lists are 88 * heap allocated: every exit path from here has to release them, and a 89 * flat sequence of `break's out of a switch cannot express that. 90 * 91 * @param json the response to parse 92 * @param url provider URL, for logging 93 * @param[in,out] acfg configuration to fill in and report 94 * @param cb function to call with the parsed configuration 95 * @param cb_cls closure for @a cb 96 * @return #GNUNET_OK if @a cb was invoked, #GNUNET_SYSERR if @a json was 97 * unusable and the caller must report the failure itself 98 */ 99 static enum GNUNET_GenericReturnValue 100 handle_config_ok (const json_t *json, 101 const char *url, 102 struct ANASTASIS_Config *acfg, 103 ANASTASIS_ConfigCallback cb, 104 void *cb_cls) 105 { 106 const char *name; 107 const json_t *methods; 108 const json_t *currencies = NULL; 109 struct TALER_JSON_ProtocolVersion pv; 110 struct ANASTASIS_AuthorizationMethodConfig *mcfg = NULL; 111 const char **carr = NULL; 112 unsigned int carr_len = 0; 113 enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; 114 struct GNUNET_JSON_Specification spec[] = { 115 GNUNET_JSON_spec_string ("name", 116 &name), 117 GNUNET_JSON_spec_string ("business_name", 118 &acfg->details.ok.business_name), 119 GNUNET_JSON_spec_string ("version", 120 &acfg->details.ok.version), 121 TALER_JSON_spec_version ("version", 122 &pv), 123 GNUNET_JSON_spec_array_const ("methods", 124 &methods), 125 GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes", 126 &acfg->details.ok.storage_limit_in_megabytes), 127 TALER_JSON_spec_amount_any ("annual_fee", 128 &acfg->details.ok.annual_fee), 129 TALER_JSON_spec_amount_any ("truth_upload_fee", 130 &acfg->details.ok.truth_upload_fee), 131 TALER_JSON_spec_amount_any ("liability_limit", 132 &acfg->details.ok.liability_limit), 133 /* Added in protocol version 3. A version 2 provider omits them, and 134 they are synthesized from the scalar fields further down. */ 135 GNUNET_JSON_spec_mark_optional ( 136 GNUNET_JSON_spec_array_const ("currencies", 137 ¤cies), 138 NULL), 139 GNUNET_JSON_spec_mark_optional ( 140 TALER_JSON_spec_amount_list ("annual_fees", 141 &acfg->details.ok.annual_fees), 142 NULL), 143 GNUNET_JSON_spec_mark_optional ( 144 TALER_JSON_spec_amount_list ("truth_upload_fees", 145 &acfg->details.ok.truth_upload_fees), 146 NULL), 147 GNUNET_JSON_spec_mark_optional ( 148 TALER_JSON_spec_amount_list ("liability_limits", 149 &acfg->details.ok.liability_limits), 150 NULL), 151 GNUNET_JSON_spec_fixed_auto ("provider_salt", 152 &acfg->details.ok.provider_salt), 153 GNUNET_JSON_spec_end () 154 }; 155 156 if (GNUNET_OK != 157 GNUNET_JSON_parse (json, 158 spec, 159 NULL, NULL)) 160 { 161 GNUNET_break_op (0); 162 json_dumpf (json, 163 stderr, 164 JSON_INDENT (2)); 165 acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED; 166 return GNUNET_SYSERR; 167 } 168 if (0 != strcmp (name, 169 "anastasis")) 170 { 171 GNUNET_break_op (0); 172 acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED; 173 goto cleanup; 174 } 175 if ( (ANASTASIS_PROTOCOL_CURRENT < pv.current) && 176 (ANASTASIS_PROTOCOL_CURRENT < pv.current - pv.age) ) 177 { 178 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 179 "Provider protocol version too new\n"); 180 acfg->ec = TALER_EC_GENERIC_VERSION_MALFORMED; 181 goto cleanup; 182 } 183 if ( (ANASTASIS_PROTOCOL_CURRENT > pv.current) && 184 (ANASTASIS_PROTOCOL_CURRENT - ANASTASIS_PROTOCOL_AGE > pv.current) ) 185 { 186 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 187 "Provider protocol version too old\n"); 188 GNUNET_break_op (0); 189 acfg->ec = TALER_EC_GENERIC_VERSION_MALFORMED; 190 goto cleanup; 191 } 192 acfg->details.ok.methods_length = (unsigned int) json_array_size (methods); 193 if (((size_t) acfg->details.ok.methods_length) != 194 json_array_size (methods)) 195 { 196 GNUNET_break_op (0); 197 acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED; 198 goto cleanup; 199 } 200 /* Bound the arrays so a malicious provider cannot force an unbounded 201 allocation (a ~40 MB response could otherwise yield ~10^6 elements). */ 202 if (acfg->details.ok.methods_length > 1024) 203 { 204 GNUNET_break_op (0); 205 acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED; 206 goto cleanup; 207 } 208 209 /* Currencies: given outright by a version 3 provider, otherwise the 210 single currency the provider was already quoting everything in. */ 211 if (NULL != currencies) 212 { 213 size_t clen = json_array_size (currencies); 214 size_t idx; 215 json_t *entry; 216 217 if ( (0 == clen) || 218 (clen > 1024) ) 219 { 220 GNUNET_break_op (0); 221 acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED; 222 goto cleanup; 223 } 224 carr = GNUNET_new_array (clen, 225 const char *); 226 json_array_foreach (currencies, idx, entry) 227 { 228 if (! json_is_string (entry)) 229 { 230 GNUNET_break_op (0); 231 acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED; 232 goto cleanup; 233 } 234 carr[idx] = json_string_value (entry); 235 carr_len = idx + 1; 236 } 237 } 238 else 239 { 240 carr = GNUNET_new_array (1, 241 const char *); 242 carr[0] = acfg->details.ok.annual_fee.currency; 243 carr_len = 1; 244 } 245 acfg->details.ok.currencies = carr; 246 acfg->details.ok.currencies_len = carr_len; 247 248 /* Plural fee fields: a version 2 provider has none, so make a 249 one-element list out of what it did send. Everything downstream can 250 then work with lists only. */ 251 { 252 struct 253 { 254 const struct TALER_Amount *scalar; 255 struct TALER_AmountList *list; 256 } fees[] = { 257 { &acfg->details.ok.annual_fee, &acfg->details.ok.annual_fees }, 258 { &acfg->details.ok.truth_upload_fee, 259 &acfg->details.ok.truth_upload_fees }, 260 { &acfg->details.ok.liability_limit, 261 &acfg->details.ok.liability_limits } 262 }; 263 264 for (unsigned int i = 0; i<sizeof (fees) / sizeof (fees[0]); i++) 265 { 266 if (0 != fees[i].list->tal_len) 267 continue; 268 GNUNET_array_append (fees[i].list->tal, 269 fees[i].list->tal_len, 270 *fees[i].scalar); 271 } 272 } 273 274 mcfg = GNUNET_new_array (GNUNET_NZL (acfg->details.ok.methods_length), 275 struct ANASTASIS_AuthorizationMethodConfig); 276 for (unsigned int i = 0; i<acfg->details.ok.methods_length; i++) 277 { 278 struct ANASTASIS_AuthorizationMethodConfig *m = &mcfg[i]; 279 struct GNUNET_JSON_Specification ispec[] = { 280 GNUNET_JSON_spec_string ("type", 281 &m->type), 282 TALER_JSON_spec_amount_any ("cost", 283 &m->usage_fee), 284 GNUNET_JSON_spec_mark_optional ( 285 TALER_JSON_spec_amount_list ("costs", 286 &m->usage_fees), 287 NULL), 288 GNUNET_JSON_spec_end () 289 }; 290 291 if (GNUNET_OK != 292 GNUNET_JSON_parse (json_array_get (methods, 293 i), 294 ispec, 295 NULL, NULL)) 296 { 297 GNUNET_break_op (0); 298 acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED; 299 goto cleanup; 300 } 301 if (0 == m->usage_fees.tal_len) 302 GNUNET_array_append (m->usage_fees.tal, 303 m->usage_fees.tal_len, 304 m->usage_fee); 305 } 306 acfg->details.ok.methods = mcfg; 307 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 308 "Good backend found at `%s'\n", 309 url); 310 cb (cb_cls, 311 acfg); 312 ret = GNUNET_OK; 313 cleanup: 314 if (NULL != mcfg) 315 { 316 for (unsigned int i = 0; i<acfg->details.ok.methods_length; i++) 317 TALER_amount_list_free (&mcfg[i].usage_fees); 318 GNUNET_free (mcfg); 319 } 320 GNUNET_free (carr); 321 GNUNET_JSON_parse_free (spec); 322 if (GNUNET_OK != ret) 323 { 324 /* Everything in there points into memory just released, and the 325 caller is about to report a failure with the same struct. */ 326 memset (&acfg->details.ok, 327 0, 328 sizeof (acfg->details.ok)); 329 } 330 return ret; 331 } 332 333 334 /** 335 * Function called when we're done processing the 336 * HTTP /config request. 337 * 338 * @param cls the `struct ANASTASIS_ConfigOperation` 339 * @param response_code HTTP response code, 0 on error 340 * @param response parsed JSON result, NULL on error 341 */ 342 static void 343 handle_config_finished (void *cls, 344 long response_code, 345 const void *response) 346 { 347 struct ANASTASIS_ConfigOperation *co = cls; 348 const json_t *json = response; 349 struct ANASTASIS_Config acfg = { 350 .http_status = response_code, 351 .response = json 352 }; 353 354 co->job = NULL; 355 switch (response_code) 356 { 357 case 0: 358 /* No reply received */ 359 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 360 "Backend `%s' failed to respond to GET /config\n", 361 co->url); 362 break; 363 case MHD_HTTP_OK: 364 if (GNUNET_OK == 365 handle_config_ok (json, 366 co->url, 367 &acfg, 368 co->cb, 369 co->cb_cls)) 370 { 371 ANASTASIS_config_cancel (co); 372 return; 373 } 374 acfg.http_status = 0; 375 break; 376 case MHD_HTTP_BAD_REQUEST: 377 /* This should never happen, either us or the anastasis server is buggy 378 (or API version conflict); just pass JSON reply to the application */ 379 break; 380 case MHD_HTTP_NOT_FOUND: 381 /* Nothing really to verify */ 382 break; 383 case MHD_HTTP_INTERNAL_SERVER_ERROR: 384 /* Server had an internal issue; we should retry, but this API 385 leaves this to the application */ 386 break; 387 default: 388 /* unexpected response code */ 389 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 390 "Unexpected response code %u\n", 391 (unsigned int) response_code); 392 GNUNET_break_op (0); 393 break; 394 } 395 co->cb (co->cb_cls, 396 &acfg); 397 ANASTASIS_config_cancel (co); 398 } 399 400 401 struct ANASTASIS_ConfigOperation * 402 ANASTASIS_get_config (struct GNUNET_CURL_Context *ctx, 403 const char *base_url, 404 ANASTASIS_ConfigCallback cb, 405 void *cb_cls) 406 { 407 struct ANASTASIS_ConfigOperation *co; 408 409 co = GNUNET_new (struct ANASTASIS_ConfigOperation); 410 co->url = TALER_url_join (base_url, 411 "config", 412 NULL); 413 co->ctx = ctx; 414 co->cb = cb; 415 co->cb_cls = cb_cls; 416 { 417 CURL *eh; 418 419 eh = ANASTASIS_curl_easy_get_ (co->url); 420 co->job = GNUNET_CURL_job_add (ctx, 421 eh, 422 &handle_config_finished, 423 co); 424 } 425 if (NULL == co->job) 426 { 427 GNUNET_free (co->url); 428 GNUNET_free (co); 429 return NULL; 430 } 431 return co; 432 } 433 434 435 void 436 ANASTASIS_config_cancel (struct ANASTASIS_ConfigOperation *co) 437 { 438 if (NULL != co->job) 439 { 440 GNUNET_CURL_job_cancel (co->job); 441 co->job = NULL; 442 } 443 GNUNET_free (co->url); 444 GNUNET_free (co); 445 } 446 447 448 /* end of anastasis_api_config.c */