This commit is contained in:
swrup 2025-10-13 00:43:46 +02:00
parent aaae59ced0
commit b2217a9873
7 changed files with 253 additions and 191 deletions

View file

@ -7,22 +7,74 @@
(* docs: https://docs.taler.net/manpages/taler-exchange.conf.5.html
https://docs.taler.net/design-documents/003-tos-rendering.html *)
(* todo: maybe move this type to config.ml *)
(* hardcoded config just for static assets *)
module Config = struct
let default_lang = "en"
let default_encoding : [< `Identity | `DEFLATE | `Gzip ] = `Identity
(* TODO Taler documentation markdown mimetype should be the prefered one, and be
supported, according to DD we take text/plain as default instead for now *)
let default_mimetype = ("text", "plain")
let default_extension = ".txt"
let terms_dir = Fpath.(v "terms")
let privacy_dir = Fpath.(v "privacy")
(* ETAG is used as base filename it should be encoded in Crockford base-32 we do
not generate it and we do not verify it *)
let terms_etag = "0" |> Headers_lib.Etag.of_crockford32 |> Result.get_ok
let privacy_etag = "0" |> Headers_lib.Etag.of_crockford32 |> Result.get_ok
let terms_legal_version = "1"
let privacy_legal_version = "1"
end
module Mimetype = struct
let mimetype_extension_assoc =
[
(("text", "plain"), ".txt"); (("text", "markdown"), ".md");
(("text", "html"), ".html"); (("text", "html"), ".htm");
(("application", "pdf"), ".pdf"); (("image", "jpeg"), ".jpg");
(("image", "jpeg"), ".jpeg"); (("image", "png"), ".png");
(("image", "gif"), ".gif");
]
let mimetype_l, _ = List.split mimetype_extension_assoc
let of_cohttp_media = function
| Cohttp.Accept.MediaType (m, m_sub) ->
List.find_opt (( = ) (m, m_sub)) mimetype_l
| AnyMediaSubtype m ->
List.find_opt (fun (m', _) -> String.equal m m') mimetype_l
| AnyMedia -> Some Config.default_mimetype
let to_extension (m, m_sub) =
assert (m <> "*");
assert (m_sub <> "*");
List.assoc_opt (m, m_sub) mimetype_extension_assoc
let of_extension ext =
List.find_map
(fun (mime, ext') ->
match String.equal ext ext' with false -> None | true -> Some mime)
mimetype_extension_assoc
let pp_mime fmt mime = Fmt.pf fmt "%s/%s" (fst mime) (snd mime)
end
type t =
| Terms
| Privacy
let etag = function
| Terms -> Config_mte.terms_etag
| Privacy -> Config_mte.privacy_etag
| Terms -> Config.terms_etag
| Privacy -> Config.privacy_etag
let legal_version = function
| Terms -> Config_mte.terms_legal_version
| Privacy -> Config_mte.privacy_legal_version
| Terms -> Config.terms_legal_version
| Privacy -> Config.privacy_legal_version
let base_dir = function
| Terms -> Config_mte.terms_dir
| Privacy -> Config_mte.privacy_dir
| Terms -> Config.terms_dir
| Privacy -> Config.privacy_dir
(* TODO
better use of Fmt to have error prefix or smthing
@ -55,9 +107,8 @@ let supported_lang_arr, supported_ext_arr =
let () =
if List.is_empty lang_l then Fmt.failwith "no language supported";
if List.is_empty ext_l then Fmt.failwith "no mimetype supported";
if not @@ List.mem Config_mte.default_lang lang_l then
Fmt.failwith "default language `%s` files not found"
Config_mte.default_lang;
if not @@ List.mem Config.default_lang lang_l then
Fmt.failwith "default language `%s` files not found" Config.default_lang;
if not @@ List.mem ".txt" ext_l then
Fmt.failwith "plain text file not found";
if not @@ List.mem ".md" ext_l then Fmt.failwith "markdown file not found";
@ -89,7 +140,7 @@ let supported_lang_arr, supported_ext_arr =
(Array.of_list lang_l, Array.of_list ext_l)
let supported_mimetype_arr =
Array.map Util.Mimetype.of_extension supported_ext_arr |> Array.map Option.get
Array.map Mimetype.of_extension supported_ext_arr |> Array.map Option.get
let is_supported_lang lang = Array.mem lang supported_lang_arr
let is_supported_ext ext = Array.mem ext supported_ext_arr
@ -102,7 +153,7 @@ let is_supported_mimetype mime = Array.mem mime supported_mimetype_arr
let get_content ~lang ~mime t =
let etag = Headers_lib.Etag.to_raw_string (etag t) in
let ext =
match Util.Mimetype.to_extension mime with
match Mimetype.to_extension mime with
| None -> Fmt.failwith "mimetype `%s/%s` unknown" (fst mime) (snd mime)
| Some ext -> ext
in

View file

@ -1,116 +0,0 @@
(* https://docs.taler.net/manpages/taler-exchange.conf.5.html
taler-docs/manpages/taler-exchange.conf.5.rst *)
(* TODO
- generate `config.ml` from config file (virtual module)?
- parse config file format
- no relative path
- default config: use relevant duration, all set to 1 year for now
*)
(* ---- *)
let currency = `Eur
let currency_to_string = function `Eur -> "EUR"
(* Values that represent an amount are in the usual amount syntax: CURRENCY:VALUE.FRACTION,
e.g. EUR:1.50. The FRACTION portion may extend up to 8 places. *)
type value = {
currency: [ `Eur ];
value: int;
fraction: int;
}
let currency_round_unit = { currency= `Eur; value= 0; fraction= 1 }
let value_to_string v =
Fmt.str "%s:%d.%d" (currency_to_string v.currency) v.value v.fraction
(* https://docs.taler.net/manpages/taler-exchange.conf.5.html#exchange-coin-options *)
module Coin = struct
(* How much is the coin worth, the format is CURRENCY:VALUE.FRACTION. For
example, a 10 cent piece is EUR:0.10. *)
let value = { currency= `Eur; value= 0; fraction= 1 }
(*How long can a coin of this type be withdrawn? This limits the losses
incurred by the exchange when a denomination key is compromised.*)
let duration_withdraw = Duration.of_year 1
(*How long is a coin of the given type valid? Smaller values result in lower
storage costs for the exchange.*)
let duration_spend = Duration.of_year 1
(*How long is the coin of the given type legal?*)
let duration_legal = Duration.of_year 1
(*What does it cost to withdraw this coin? Specified using the same format as
value.*)
let fee_withdraw = { currency= `Eur; value= 0; fraction= 0 }
(*What does it cost to deposit this coin? Specified using the same format as
value.*)
let fee_deposit = { currency= `Eur; value= 0; fraction= 0 }
(*What does it cost to refresh this coin? Specified using the same format as
value.*)
let fee_refresh = { currency= `Eur; value= 0; fraction= 0 }
(*What does it cost to refund this coin? Specified using the same format as
value.*)
let fee_refund = { currency= `Eur; value= 0; fraction= 0 }
(*Which cipher to use for this coin? Must be either RSA or CS.*)
let cipher : [ `RSA | `CS ] = `RSA
(*How many bits should the RSA modulus (product of the two primes) have for
this type of coin.*)
let rsa_keysize = -1
(*Set to YES to make this a denomination with support*)
let age_restricted : [ `YES | `NO ] = `NO
end
(* Crockford Base32-encoded master public key, public version of the exchanges long-time offline signing key. *)
let master_public_key = "uhuh"
(* module type for CS/EDDSA/RSA config *)
module Secmod = struct
(* Note that the taler-exchange-secmod-rsa also evaluates the [coin_*] configuration sections described below. *)
(*How long do we generate denomination and signing keys ahead of time?*)
let lookahead_sign = Duration.of_year 1
(*How much should validity periods for coins overlap? Should be long enough to avoid problems with wallets picking one key and then due to network latency another key being valid. The DURATION_WITHDRAW period must be longer than this value.*)
let overlap_duration = Duration.of_year 1
(*
Where should the security module store its long-term private key?
SM_PRIV_KEY
Where should the security module store the private keys it manages?
KEY_DIR
On which path should the security module listen for signing requests?
UNIXPATH
*)
end
module Database = struct
(*After which time period should reserves be closed if they are idle?*)
let idle_reserve_expiration_time = -1
(*After what time do we forget about (drained) reserves during garbage collection?*)
let legal_reserve_expiration_time = -1
(*Delay between a deposit being eligible for aggregation and the aggregator actually triggering.*)
let aggregator_shift = -1
(*Number of concurrent purses that a reserve may have active if it is paid to be opened for a year.*)
let default_purse_limit = -1
(*Maximum time an AML program is allowed to run. (Optional for taler-auditor.)*)
let max_aml_program_runtime = -1
module Postgres = struct
(*How to access the database, e.g. “postgres:///taler-exchange” to use the “taler-exchange” database. Testcases use “talercheck”.*)
let config = "uhuh"
end
end

View file

@ -1,19 +0,0 @@
(* config_mte.ml
config value that are not in official config options *)
let default_lang = "en"
let default_encoding : [< `Identity | `DEFLATE | `Gzip ] = `Identity
(* TODO Taler documentation markdown mimetype should be the prefered one, and be
supported, according to DD we take text/plain as default instead for now *)
let default_mimetype = ("text", "plain")
let default_extension = ".txt"
let terms_dir = Fpath.(v "terms")
let privacy_dir = Fpath.(v "privacy")
(* ETAG is used as base filename it should be encoded in Crockford base-32 we do
not generate it and we do not verify it *)
let terms_etag = "0" |> Headers_lib.Etag.of_crockford32 |> Result.get_ok
let privacy_etag = "0" |> Headers_lib.Etag.of_crockford32 |> Result.get_ok
let terms_legal_version = "1"
let privacy_legal_version = "1"

View file

@ -1,16 +1,22 @@
let pp_array pp_item item = Fmt.array ~sep:(Fmt.any ", ") pp_item item
let accept_header_value =
let s = Fmt.str "%a" Util.(pp_array pp_mime) Assets.supported_mimetype_arr in
let s =
Fmt.str "%a"
(pp_array Assets.Mimetype.pp_mime)
Assets.supported_mimetype_arr
in
s
let avail_languages_header_value =
let s = Fmt.str "%a" (Util.pp_array Fmt.string) Assets.supported_lang_arr in
let s = Fmt.str "%a" (pp_array Fmt.string) Assets.supported_lang_arr in
s
let select_mimetype headers =
let accept = Vif.Headers.get headers "accept" in
Cohttp.Accept.media_ranges accept
|> Cohttp.Accept.qsort
|> List.filter_map (fun (_q, (m, _p)) -> Util.Mimetype.of_cohttp_media m)
|> List.filter_map (fun (_q, (m, _p)) -> Assets.Mimetype.of_cohttp_media m)
|> List.find_opt Assets.is_supported_mimetype
let select_language headers =
@ -19,14 +25,14 @@ let select_language headers =
|> Cohttp.Accept.qsort
|> List.map (fun (_q, lang) -> lang)
|> List.map (function
| Cohttp.Accept.AnyLanguage -> Config_mte.default_lang
| Cohttp.Accept.AnyLanguage -> Assets.Config.default_lang
| Language language_range -> (
(* ignore language subtags (e.g. "en-US" -> "en") *)
match language_range with
| [] -> assert false
| primary_tag :: _ -> primary_tag))
|> List.find_opt Assets.is_supported_lang
|> Option.value ~default:Config_mte.default_lang
|> Option.value ~default:Assets.Config.default_lang
let select_encoding headers =
Vif.Headers.get headers "accept-encoding"
@ -37,7 +43,7 @@ let select_encoding headers =
| Cohttp.Accept.Identity -> Some `Identity
| Deflate -> Some `DEFLATE
| Gzip -> Some `Gzip
| AnyEncoding -> Some Config_mte.default_encoding
| AnyEncoding -> Some Assets.Config.default_encoding
| Encoding _ | Compress -> (* unsupported *) None)
|> function
| [] -> assert false

View file

@ -83,13 +83,13 @@ module Static = struct
in
let* () =
(* todo: is it "taler-privacy-version" for /policy ? *)
add ~field:"taler-terms-version" Config_mte.terms_legal_version
add ~field:"taler-terms-version" Assets.Config.terms_legal_version
in
let* () =
add ~field:"avail-languages" Headers.avail_languages_header_value
in
let* () =
let content_type = Fmt.str "%a" Util.pp_mime mime in
let content_type = Fmt.str "%a" Assets.Mimetype.pp_mime mime in
add ~field:"content-type" content_type
in
respond `OK)

175
src/taler_config.ml Normal file
View file

@ -0,0 +1,175 @@
(* https://docs.taler.net/manpages/taler-exchange.conf.5.html
taler-docs/manpages/taler-exchange.conf.5.rst *)
(* TODO
- generate `config.ml` from config file (virtual module)?
- parse config file format
- no relative path
- default config
- better types
- impl duration
*)
(* TODO unikernel *)
type dir_path
type file_path
(* TODO *)
(* Values that represent a time duration are represented as a series of one or more NUMBER UNIT pairs, e.g. 60 s, 4 weeks 1 day, 5 years 2 minutes. *)
type duration
(* TODO
make it Amount.t *)
type amount = string
type payto_uri = string
(* TODO
still need to parse them and tell that its not supported
some maybe are relevant
idk *)
type not_relevant
type url = string
type seconds = int
(* not relevant for mirage *)
(* this contains path that can be referenced in other with $PATH
(unsupported) *)
module type Global = sig
val taler_home : dir_path
val taler_data_home : dir_path
val taler_config_home : dir_path
val taler_cache_home : dir_path
val taler_runtime_dir : dir_path
end
(* sections "[currency-$NAME]"
see DD51 *)
module type Currency = sig
val enabled : [ `YES | `NO ]
val code : string
val name : string
val fractional_input_digits : int
val fractional_normal_digits : int
val fractional_trailing_zero_digits : int
val alt_unit_names : (int * string) list
end
(* section "[exchange]" *)
module type Exchange = sig
val currency : string
val currency_round_unit : amount
val db : string
val attribute_encryption_key : string
val serve : [ `Unix | `Tcp | `Systemd ]
val unixpath : file_path
val unixpath_mode : int
val port : int
val bind_to : string
val master_public_key : string
val tiny_amount : amount option
val shopping_url : url option
val open_banking_gateway_url : url option
val aml_spa_dialect : string option
val bank_compliance_language : string option
val stefan_abs : amount
val stefan_log : amount
val stefan_lin : float
val base_url : url
val toplevel_redirect_url : string option
val aggregator_idle_sleep_interval : seconds
val closer_idle_sleep_interval : seconds
val transfer_idle_sleep_interval : seconds
val wirewatch_idle_sleep_interval : seconds
val aggregator_shard_size : int option
val signkey_legal_duration : duration
val max_keys_caching : duration
val max_requests : int
val terms_dir : dir_path
val terms_etag : string
val privacy_dir : dir_path
val privacy_etag : string
val enable_kyc : [ `YES | `NO ]
end
(* section "[taler-exchange-secmod-{rsa|cs|eddsa}]". *)
module type Secmod = sig
val lookahead_sign : duration
val overlap_duration : duration
val sm_priv_key : file_path
val key_dir : dir_path
val unixpath : not_relevant
end
module type Secmod_rsa = Secmod
module type Secmod_cs = Secmod
module type Secmod_eddsa = Secmod
(* TODO config
what is the time/duration unit used here? *)
(* section "[exchangedb]". *)
module type Database = sig
val idle_reserve_expiration_time : seconds
val legal_reserve_expiration_time : seconds
val aggregator_shift : seconds
val default_purse_limit : int
val max_aml_program_runtime : int option
module type Postgres_backend = sig
val config : string
end
end
(* sections "[coin_XXX]"
used by secmods *)
module type Coin = sig
val value : amount
val duration_withdraw : duration
val duration_spend : duration
val duration_legal : duration
val fee_withdraw : amount
val fee_deposit : amount
val fee_refresh : amount
val fee_refund : amount
val cipher : [ `CS | `RSA ]
val rsa_keysize : int option (*only if `RSA *)
val age_restricted : [ (*`YES|*) `NO ]
end
(* sections "[exchange-account-XXX]" *)
module type Account = sig
val payto_uri : payto_uri
val enable_debit : [ `YES | `NO ]
val enable_credit : [ `YES | `NO ]
end
(* sections "[exchange-accountcredentials-XXX]"
must exists for each "[exchange-account-XXX]" section
! credentials to access the bank account
should be in a secret configuration file
only redable for `taler-exchange-wirewatch` `taler-exchange-transfer` processes *)
module type Account_secret = sig
val wire_gateway_url : url
val wire_gateway_auth_method : string
val username : string
val password : string
val token : string
end
(* section "[exchange-extension-<extensionname>]" *)
module type Extensions = sig
val enabled : [ (*`YES|*) `NO ]
end
(* section "[exchange-offline]". *)
module type Offline_signing = sig
val master_priv_file : file_path
(* TODO
- we need two different file here
- there is three, not two, crypto helper modules
is it only two, because the eddsa one is not comptabilized as a "crypto helper" here? *)
(* tofu = Trust On First Use *)
val secm_tofu_file : file_path
val secm_denom_pubkey : string option
val secm_esign_pubkey : string option
end

View file

@ -1,43 +1,8 @@
(* TODO *)
module Protocol_version = struct
(* libtool version format *)
(* todo *)
let current = 0
let revision = 0
let age = 0
let v = Fmt.str "%d:%d:%d"
end
module Mimetype = struct
let mimetype_extension_assoc =
[
(("text", "plain"), ".txt"); (("text", "markdown"), ".md");
(("text", "html"), ".html"); (("text", "html"), ".htm");
(("application", "pdf"), ".pdf"); (("image", "jpeg"), ".jpg");
(("image", "jpeg"), ".jpeg"); (("image", "png"), ".png");
(("image", "gif"), ".gif");
]
let mimetype_l, _ = List.split mimetype_extension_assoc
let of_cohttp_media = function
| Cohttp.Accept.MediaType (m, m_sub) ->
List.find_opt (( = ) (m, m_sub)) mimetype_l
| AnyMediaSubtype m ->
List.find_opt (fun (m', _) -> String.equal m m') mimetype_l
| AnyMedia -> Some Config_mte.default_mimetype
let to_extension (m, m_sub) =
assert (m <> "*");
assert (m_sub <> "*");
List.assoc_opt (m, m_sub) mimetype_extension_assoc
let of_extension ext =
List.find_map
(fun (mime, ext') ->
match String.equal ext ext' with false -> None | true -> Some mime)
mimetype_extension_assoc
end
(* -- pretty printers -- *)
let pp_mime fmt mime = Fmt.pf fmt "%s/%s" (fst mime) (snd mime)
let pp_array pp_item item = Fmt.array ~sep:(Fmt.any ", ") pp_item item