From 9b6d845cb1f674285efdacfbd3fb6ebe6eff7c10 Mon Sep 17 00:00:00 2001 From: swrup Date: Thu, 20 Nov 2025 20:03:51 +0100 Subject: [PATCH] JJ: Description from the destination commit: + currency config JJ: Description from source commit: better config: parse file + crunch --- default.config | 43 ------ include/dune | 4 +- include/parse_config.ml | 147 ++++++++------------- src/assets.ml | 12 +- src/assets/default.config | 73 +++++++++++ src/config.ml | 266 +++++++++++++++++++++++++++----------- src/denomination.ml | 2 +- src/devices.ml | 2 +- src/dune | 1 + src/types.ml | 32 ----- 10 files changed, 326 insertions(+), 256 deletions(-) delete mode 100644 default.config create mode 100644 src/assets/default.config diff --git a/default.config b/default.config deleted file mode 100644 index 99fdbdfb..00000000 --- a/default.config +++ /dev/null @@ -1,43 +0,0 @@ -# comment 1 -# foo -# barr -[currency-EUR] -enabled = NO - # comment 2 -code= "EUR" -name =euro -fractional_input_digits = 2 -fractional_normal_digits =2 -fractional_trailing_zero_digits= 2 - % comment 3 -alt_unit_names = "{"0":"€","3":"k€"}" -% comment 4 - -[dummy-section] -dummy = "blah" - -[exchange] -currency = EUR -currency_round_unit = EUR:0.01 -db = postgres -attribute_encryption_key = "deadbeef" -port = 3434 -bind_to = localhost -master_public_key = "8MQF2XPWCKCW4199JFPE08X7Y9XAX21SF2HD8NZKXM3PY3CMCJ90====" -stefan_abs = EUR:0.00 -stefan_log = EUR:0.00 -stefan_lin = 0.00 -aggregator_idle_sleep_interval = "1 hour" -closer_idle_sleep_interval = "1 hour" -transfer_idle_sleep_interval = "1 hour" -wirewatch_idle_sleep_interval = "1 hour" -signkey_legal_duration = "1 year" -max_keys_caching = "4 weeks" -enable_kyc = NO - -[exchangedb] -idle_reserve_expiration_time = "1 year 2 weeks 3 hours 4 minutes 5 seconds" -legal_reserve_expiration_time = "1 year" -aggregator_shift = "1 year" -max_aml_program_runtime = "1 year" -default_purse_limit = 9999 diff --git a/include/dune b/include/dune index 0bb76a72..a68f7d3c 100644 --- a/include/dune +++ b/include/dune @@ -14,10 +14,10 @@ (modules gen_eddsa_key) (libraries fmt mirage-crypto-rng.unix mirage-crypto-ec b32)) -(executable +(library ; todo: should I have a /lib? (name parse_config) (modules parse_config) - (libraries mirage-crypto-ec ptime fmt angstrom amount b32)) + (libraries mirage-crypto-ec ptime fmt angstrom uri amount b32)) ; we prefer to generate taler_signatures.ml once, ; and commit it, for versionning: diff --git a/include/parse_config.ml b/include/parse_config.ml index fb50fcad..115d1af7 100644 --- a/include/parse_config.ml +++ b/include/parse_config.ml @@ -3,11 +3,6 @@ do not support "$"-path expansion*) -(* TODO - - do all config section - - parse alt_unit_names json pair - *) - open Angstrom type item = { @@ -21,14 +16,14 @@ type section = { } let fail_with msg = - Fmt.epr "Failed to parse configuration: %s.@." msg; + Fmt.epr "Configuration failure: %s.@." msg; exit 1 let is_eol = function '\n' | '\r' -> true | _ -> false let is_whitespace = function ' ' | '\t' -> true | _ -> false let whitespace = skip_while is_whitespace -module Parse_file = struct +module Parse_data = struct type line = | Blank | Comment of string @@ -90,7 +85,7 @@ module Parse_file = struct in loop [] [] (List.rev l) - let parse_content s = + let parse s = match parse_string ~consume:All config s with | Error msg -> fail_with (Fmt.str "parse error `%s`" msg) | Ok v -> fold_sections v @@ -105,7 +100,7 @@ module Pp_debug = struct let pp_line ppf line = let open Fmt in - let open Parse_file in + let open Parse_data in match line with | Blank -> Fmt.nop ppf () | Comment s -> pf ppf "#%s" s @@ -205,14 +200,14 @@ let get t ~section ~field = | None -> fail_with (Fmt.str "option `[%s].%s` not found" section field) | Some v -> v -let int v = - match int_of_string_opt v with - | None -> fail_with (Fmt.str "expected int value, got `%s`" v) +let int s = + match int_of_string_opt s with + | None -> fail_with (Fmt.str "expected int value, got `%s`" s) | Some v -> v -let float v = - match float_of_string_opt v with - | None -> fail_with (Fmt.str "expected float value, got `%s`" v) +let float s = + match float_of_string_opt s with + | None -> fail_with (Fmt.str "expected float value, got `%s`" s) | Some v -> v let const_value a b = @@ -225,91 +220,57 @@ let yes_no = function | "YES" -> `YES | s -> fail_with (Fmt.str "expected `YES`/`NO` value, got `%s`" s) -let amount v = v |> Amount.of_string |> unwrap_res -let duration v = Parse_duration.(v |> parse |> to_ptime_span) +let uri s = Uri.of_string s +let amount s = s |> Amount.of_string |> unwrap_res +let duration s = Parse_duration.(s |> parse |> to_ptime_span) -let ed25519 v = - v +let ed25519 s = + s |> B32.decode |> unwrap_res |> Mirage_crypto_ec.Ed25519.pub_of_octets |> Result.map_error (fun e -> Fmt.str "%a" Mirage_crypto_ec.pp_error e) |> unwrap_res -module Config = struct - [@@@ocaml.warning "-32"] +module Parse_alt_unit_names = struct + let rm_brackets s = + let s = String.trim s in + match + String.starts_with ~prefix:"{" s && String.ends_with ~suffix:"}" s + with + | false -> fail_with (Fmt.str "expected json, got `%s`" s) + | true -> + let s = String.sub s 1 (String.length s - 2) in + s - let config = - let read_file file = In_channel.with_open_bin file In_channel.input_all in - let content = read_file "default.config" in - let v = Parse_file.parse_content content in - (*Fmt.pr "config file:@\n%a@." Pp_debug.pp_config v;*) - v + let rm_quotes s = + let s = String.trim s in + match + String.starts_with ~prefix:"\"" s && String.ends_with ~suffix:"\"" s + with + | false -> fail_with (Fmt.str "expected quoted string, got `%s`" s) + | true -> + let s = String.sub s 1 (String.length s - 2) in + s - module Exchange = struct - let get_opt field = get_opt config ~section:"exchange" ~field - let get field = get config ~section:"exchange" ~field - - (* - *) - let currency = (* todo: constraint on currency string *) get "currency" - let currency_round_unit = get "currency_round_unit" |> amount - let db = get "db" |> const_value "postgres" - let attribute_encryption_key = get "attribute_encryption_key" - let port = get "port" |> int - let bind_to = get "bind_to" - let master_public_key = get "master_public_key" |> ed25519 - let stefan_abs = get "stefan_abs" |> amount - let stefan_log = get "stefan_log" |> amount - let stefan_lin = get_opt "stefan_lin" |> Option.map float - - let aggregator_idle_sleep_interval = - get "aggregator_idle_sleep_interval" |> duration - - let closer_idle_sleep_interval = - get "closer_idle_sleep_interval" |> duration - - let transfer_idle_sleep_interval = - get "transfer_idle_sleep_interval" |> duration - - let wirewatch_idle_sleep_interval = - get "wirewatch_idle_sleep_interval" |> duration - - let signkey_legal_duration = get "signkey_legal_duration" |> duration - let max_keys_caching = get "max_keys_caching" |> duration - let enable_kyc = get "enable_kyc" |> yes_no - - (* optional: - tiny_amount - shopping_url - open_banking_gateway_url - aml_spa_dialect - bank_compliance_language - toplevel_redirect_url *) - (* not implemented or not relevant to MTE: - let max_requests = get "max_requests" |> int - base_url - aggregator_shard_size - serve - unixpath - unixpath_mode - terms_dir - terms_etag - privacy_dir - privacy_etag *) - end - - module Exchangedb = struct - let get field = get config ~section:"exchangedb" ~field - - (* - *) - let idle_reserve_expiration_time = - get "idle_reserve_expiration_time" |> duration - - let legal_reserve_expiration_time = - get "legal_reserve_expiration_time" |> duration - - let aggregator_shift = get "aggregator_shift" |> duration - let max_aml_program_runtime = get "max_aml_program_runtime" |> duration - let default_purse_limit = get "default_purse_limit" |> int - end + let parse s = + let s = rm_brackets s in + String.split_on_char ',' s + |> List.map (String.split_on_char ':') + |> List.map (function + | [ k; v ] -> (k, v) + | _ -> fail_with "invalid json key-value map") + |> List.map (fun (k, v) -> + let k = rm_quotes k in + let v = rm_quotes v in + let k = + match int_of_string_opt k with + | None -> + fail_with + (Fmt.str + "invalid json key-value map, expected integer key, got `%s`" + k) + | Some k -> k + in + (k, v)) end diff --git a/src/assets.ml b/src/assets.ml index 846291fa..f27b520b 100644 --- a/src/assets.ml +++ b/src/assets.ml @@ -51,21 +51,23 @@ module Mimetype = struct let pp_mime fmt mime = Fmt.pf fmt "%s/%s" (fst mime) (snd mime) end +(* TODO config *) type t = | Terms | Privacy -let etag = function - | Terms -> Config.Exchange.terms_etag - | Privacy -> Config.Exchange.privacy_etag +let etag k = + Result.get_ok + @@ Headers_lib.Etag.of_crockford32 + @@ match k with Terms -> "0" | Privacy -> "0" let legal_version = function | Terms -> terms_legal_version | Privacy -> privacy_legal_version let base_dir = function - | Terms -> Config.Exchange.terms_dir - | Privacy -> Config.Exchange.privacy_dir + | Terms -> Fpath.v "terms" + | Privacy -> Fpath.v "privacy" (* TODO better use of Fmt to have error prefix or smthing diff --git a/src/assets/default.config b/src/assets/default.config new file mode 100644 index 00000000..0c401640 --- /dev/null +++ b/src/assets/default.config @@ -0,0 +1,73 @@ +[currency-EUR] +enabled= YES +code= EUR +name= euro +fractional_input_digits= 2 +fractional_normal_digits= 2 +fractional_trailing_zero_digits= 2 +alt_unit_names= "{"0":"€","3":"k€"}" + +[exchange] +currency = EUR +currency_round_unit = EUR:0.01 +db = postgres +attribute_encryption_key = "deadbeef" +port = 3434 +bind_to = localhost +master_public_key = "8MQF2XPWCKCW4199JFPE08X7Y9XAX21SF2HD8NZKXM3PY3CMCJ90====" +stefan_abs = EUR:0.00 +stefan_log = EUR:0.00 +stefan_lin = 0.00 +aggregator_idle_sleep_interval = "1 hour" +closer_idle_sleep_interval = "1 hour" +transfer_idle_sleep_interval = "1 hour" +wirewatch_idle_sleep_interval = "1 hour" +signkey_legal_duration = "1 year" +max_keys_caching = "4 weeks" +enable_kyc = NO +terms_etag = "0" +privacy_etag = "0" + +[exchangedb] +idle_reserve_expiration_time = "1 year 2 weeks 3 hours 4 minutes 5 seconds" +legal_reserve_expiration_time = "1 year" +aggregator_shift = "1 year" +max_aml_program_runtime = "1 year" +default_purse_limit = 9999 + +[exchangedb-postgres] +config = "pgx://mte:hunter2@localhost:5432/taler-exchange" + +[taler-exchange-secmod-rsa] +lookahead_sign = "1 year" +overlap_duration = "1 year" + +[taler-exchange-secmod-eddsa] +lookahead_sign = "1 year" +overlap_duration = "1 year" + +[coin_kudo_1] +value= EUR:0.01 +duration_withdraw= "1 year" +duration_spend= "1 year" +duration_legal= "1 year" +fee_withdraw= EUR:0.00 +fee_deposit= EUR:0.00 +fee_refresh= EUR:0.00 +fee_refund= EUR:0.00 +cipher= RSA +rsa_keysize= 2048 +age_restricted= NO + +[coin_kudo_2] +value= EUR:0.02 +duration_withdraw= "1 year" +duration_spend= "1 year" +duration_legal= "1 year" +fee_withdraw= EUR:0.00 +fee_deposit= EUR:0.00 +fee_refresh= EUR:0.00 +fee_refund= EUR:0.00 +cipher= RSA +rsa_keysize= 2048 +age_restricted= NO diff --git a/src/config.ml b/src/config.ml index 64bd756c..992961d6 100644 --- a/src/config.ml +++ b/src/config.ml @@ -1,100 +1,208 @@ -(* hardcoded config for now *) +open Parse_config -let amount s = Amount.of_string s |> Result.get_ok -let zero_eur = amount "EUR:0.00" -let dummy_duration = Ptime.Span.of_int_s 99999999 +let config_data = + let path = Fpath.to_string (Fpath.v "default.config") in + match Assets_crunch.read path with + | None -> Fmt.failwith "static file not found: `%s`" path + | Some data -> + let v = Parse_data.parse data in + (*Fmt.epr "config file:@\n%a@." Pp_debug.pp_config v;*) + v module Exchange = struct - let currency = "EUR" - let currency_round_unit = amount "EUR:0.01" - let db = `Pgx (* plugin to use for the database *) - let attribute_encryption_key = "uhuhg" (* high-entropy nonce. *) - let port = 3434 + let get_opt field = get_opt config_data ~section:"exchange" ~field + let get field = get config_data ~section:"exchange" ~field - let master_public_key = - "8MQF2XPWCKCW4199JFPE08X7Y9XAX21SF2HD8NZKXM3PY3CMCJ90====" - |> B32.decode - |> Result.get_ok - |> Mirage_crypto_ec.Ed25519.pub_of_octets - |> Result.get_ok + (* - *) + let currency = (* todo: constraint on currency string *) get "currency" + let currency_round_unit = get "currency_round_unit" |> amount + let db = get "db" |> const_value "postgres" + let attribute_encryption_key = get "attribute_encryption_key" + let port = get "port" |> int + let bind_to = get "bind_to" + let master_public_key = get "master_public_key" |> ed25519 + let stefan_abs = get "stefan_abs" |> amount + let stefan_log = get "stefan_log" |> amount + let stefan_lin = get_opt "stefan_lin" |> Option.map float - let stefan_abs = zero_eur - let stefan_log = zero_eur - let stefan_lin = 0.0 - let signkey_legal_duration = dummy_duration - let max_keys_caching = 9999999 - let max_requests = 99999999 - let terms_dir = Fpath.(v "terms") - let terms_etag = "0" |> Headers_lib.Etag.of_crockford32 |> Result.get_ok - let privacy_dir = Fpath.(v "privacy") - let privacy_etag = "0" |> Headers_lib.Etag.of_crockford32 |> Result.get_ok - let enable_kyc = `NO -end + let aggregator_idle_sleep_interval = + get "aggregator_idle_sleep_interval" |> duration -let currency_eur = - Types.Config_types.Currency. - { - enabled= `NO; - code= "EUR"; - name= "euro"; - fractional_input_digits= 2; - fractional_normal_digits= 2; - fractional_trailing_zero_digits= 2; - alt_unit_names= [ (0, "E"); (3, "kE") ]; - } + let closer_idle_sleep_interval = get "closer_idle_sleep_interval" |> duration -module Secmod_rsa = struct - let lookahead_sign = dummy_duration - let overlap_duration = dummy_duration - let sm_priv_key = Fpath.(v "rsa_key.priv") - let key_dir = Fpath.(v "rsa") -end + let transfer_idle_sleep_interval = + get "transfer_idle_sleep_interval" |> duration -module Secmod_eddsa = struct - let lookahead_sign = dummy_duration - let overlap_duration = dummy_duration - let sm_priv_key = Fpath.(v "eddsa_key.priv") - let key_dir = Fpath.(v "eddsa") + let wirewatch_idle_sleep_interval = + get "wirewatch_idle_sleep_interval" |> duration + + let signkey_legal_duration = get "signkey_legal_duration" |> duration + let max_keys_caching = get "max_keys_caching" |> duration + let enable_kyc = get "enable_kyc" |> yes_no + let terms_etag = get "terms_etag" + let privacy_etag = get "privacy_etag" + + (* optional: + tiny_amount + shopping_url + open_banking_gateway_url + aml_spa_dialect + bank_compliance_language + toplevel_redirect_url *) + (* not implemented or not relevant to MTE: + let max_requests = get "max_requests" |> int + base_url + aggregator_shard_size + serve + unixpath + unixpath_mode + terms_dir + privacy_dir *) end module Exchangedb = struct - let idle_reserve_expiration_time = dummy_duration - let legal_reserve_expiration_time = dummy_duration - let aggregator_shift = dummy_duration - let default_purse_limit = 9999 - let max_aml_program_runtime = dummy_duration + let get field = get config_data ~section:"exchangedb" ~field + + (* - *) + let idle_reserve_expiration_time = + get "idle_reserve_expiration_time" |> duration + + let legal_reserve_expiration_time = + get "legal_reserve_expiration_time" |> duration + + let aggregator_shift = get "aggregator_shift" |> duration + let max_aml_program_runtime = get "max_aml_program_runtime" |> duration + let default_purse_limit = get "default_purse_limit" |> int end module Exchangedb_postgres = struct - (* pgx://:@:/ *) let config = - let user = "mte" in - let password = "hunter2" in - let host = "localhost" in - let port = 5432 in - let database = "taler-exchange" in - Uri.of_string - @@ Fmt.str "pgx://%s:%s@%s:%d/%s" user password host port database + get config_data ~section:"exchangedb-postgres" ~field:"config" |> uri end -let coin_kudo_1 = - Types.Config_types.Coin. +module Currency = struct + type t = { + enabled: [ `YES | `NO ]; + code: string; + name: string; + fractional_input_digits: int; + fractional_normal_digits: int; + fractional_trailing_zero_digits: int; + alt_unit_names: (int * string) list; + } + + let currency_sections = + List.filter + (fun v -> String.starts_with ~prefix:"currency-" v.header) + config_data + + let parse_currency section = + let get field = get config_data ~section:section.header ~field in { - section_name= "kudo_1"; - value= amount "EUR:0.01"; - duration_withdraw= dummy_duration; - duration_spend= dummy_duration; - duration_legal= dummy_duration; - fee_withdraw= zero_eur; - fee_deposit= zero_eur; - fee_refresh= zero_eur; - fee_refund= zero_eur; - cipher= `RSA; - rsa_keysize= 2048; - age_restricted= `NO; + enabled= get "enabled" |> yes_no; + code= get "code"; + name= get "name"; + fractional_input_digits= get "fractional_input_digits" |> int; + fractional_normal_digits= get "fractional_normal_digits" |> int; + fractional_trailing_zero_digits= + get "fractional_trailing_zero_digits" |> int; + alt_unit_names= get "alt_unit_names" |> Parse_alt_unit_names.parse; } -let coin_kudo_2 = - { coin_kudo_1 with section_name= "kudo_2"; value= amount "EUR:0.02" } + let all_currencies = List.map parse_currency currency_sections -let coins = [ coin_kudo_1; coin_kudo_2 ] + (* I think the exchange only handle one currency *) + let v = + match + List.find_opt (fun v -> v.code = Exchange.currency) all_currencies + with + | None -> + fail_with + (Fmt.str + "section `[currency-%s]` not found, currency `%s` is not defined" + Exchange.currency Exchange.currency) + | Some v -> ( + match v.enabled = `YES with + | false -> + fail_with (Fmt.str "currency `%s` is not enabled" Exchange.currency) + | true -> v) +end + +module Coin = struct + type t = { + section_name: string; + value: Amount.t; + duration_withdraw: Ptime.Span.t; + duration_spend: Ptime.Span.t; + duration_legal: Ptime.Span.t; + fee_withdraw: Amount.t; + fee_deposit: Amount.t; + fee_refresh: Amount.t; + fee_refund: Amount.t; + cipher: [ (* `CS |*) `RSA ]; + rsa_keysize: int; (* : int option (only if `RSA) *) + age_restricted: [ (*`YES|*) `NO ]; + } + + let coin_sections = + List.filter + (fun v -> + (* note: here its a '_' not '-' *) + String.starts_with ~prefix:"coin_" v.header) + config_data + + let parse_coin section = + let get field = get config_data ~section:section.header ~field in + let section_name = + String.sub section.header 5 (String.length section.header - 5) + in + { + section_name; + value= get "value" |> amount; + duration_withdraw= get "duration_withdraw" |> duration; + duration_spend= get "duration_spend" |> duration; + duration_legal= get "duration_legal" |> duration; + fee_withdraw= get "fee_withdraw" |> amount; + fee_deposit= get "fee_deposit" |> amount; + fee_refresh= get "fee_refresh" |> amount; + fee_refund= get "fee_refund" |> amount; + cipher= (get "cipher" |> const_value "RSA" |> fun _s -> `RSA); + rsa_keysize= get "rsa_keysize" |> int; + age_restricted= + ( get "age_restricted" |> yes_no |> function + | `NO -> `NO + | `YES -> fail_with "`age_restricted = YES` is not supported" ); + } + + let all_coins = List.map parse_coin coin_sections + + let get_coin name = + match List.find_opt (fun v -> v.section_name = name) all_coins with + | None -> + fail_with + (Fmt.str "section `[coin_%s]` not found, coin `%s` is not defined" + name name) + | Some v -> v + + let kudo_1 = get_coin "kudo_1" + let kudo_2 = get_coin "kudo_2" +end + +module Exchange_secmod_rsa = struct + let get field = + let section = "taler-exchange-secmod-" ^ "rsa" in + get config_data ~section ~field + + let lookahead_sign = get "lookahead_sign" |> duration + let overlap_duration = get "overlap_duration" |> duration + (* not relevant: sm_priv_key key_dir unixpath *) +end + +module Exchange_secmod_eddsa = struct + let get field = + let section = "taler-exchange-secmod-" ^ "eddsa" in + get config_data ~section ~field + + let lookahead_sign = get "lookahead_sign" |> duration + let overlap_duration = get "overlap_duration" |> duration +end diff --git a/src/denomination.ml b/src/denomination.ml index a1e48c0f..5e0a1eb9 100644 --- a/src/denomination.ml +++ b/src/denomination.ml @@ -36,7 +36,7 @@ let make rsa_keysize; age_restricted= _; } : - Config_types.Coin.t) = + Config.Coin.t) = assert (cipher = `RSA); let stamp_start = Ptime_clock.now () in diff --git a/src/devices.ml b/src/devices.ml index a61b563b..5053eada 100644 --- a/src/devices.ml +++ b/src/devices.ml @@ -29,7 +29,7 @@ module Secmod_denom = struct let finally _key = () in Vif.Device.v ~name:"secmod_denom" ~finally [] @@ fun (_env : env) -> let sm_key = Signkey.generate () in - let keys = List.map Denomination.make Config.coins in + let keys = List.map Denomination.make Config.Coin.all_coins in { sm_key; keys } end diff --git a/src/dune b/src/dune index 18668575..db08080e 100644 --- a/src/dune +++ b/src/dune @@ -13,6 +13,7 @@ b32 ; include + parse_config ; caqti caqti-miou diff --git a/src/types.ml b/src/types.ml index a376a10c..4eab7a54 100644 --- a/src/types.ml +++ b/src/types.ml @@ -1,35 +1,3 @@ -module Config_types = struct - module Currency = struct - type t = { - enabled: [ `YES | `NO ]; - code: string; - name: string; - fractional_input_digits: int; - fractional_normal_digits: int; - fractional_trailing_zero_digits: int; - alt_unit_names: (int * string) list; - } - end - - module Coin = struct - type t = { - (* section_name: Name in the configuration file that defines this denomination *) - section_name: string; - value: Amount.t; - duration_withdraw: Ptime.Span.t; - duration_spend: Ptime.Span.t; - duration_legal: Ptime.Span.t; - fee_withdraw: Amount.t; - fee_deposit: Amount.t; - fee_refresh: Amount.t; - fee_refund: Amount.t; - cipher: [ (* `CS |*) `RSA ]; - rsa_keysize: int; (* : int option (only if `RSA) *) - age_restricted: [ (*`YES|*) `NO ]; - } - end -end - module Amount = struct include Amount