cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

config.go (9291B)


      1 // This file is part of taler-cashless2ecash.
      2 // Copyright (C) 2024 Joel Häberli
      3 //
      4 // taler-cashless2ecash is free software: you can redistribute it and/or modify it
      5 // under the terms of the GNU Affero General Public License as published
      6 // by the Free Software Foundation, either version 3 of the License,
      7 // or (at your option) any later version.
      8 //
      9 // taler-cashless2ecash is distributed in the hope that it will be useful, but
     10 // WITHOUT ANY WARRANTY; without even the implied warranty of
     11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     12 // Affero General Public License for more details.
     13 //
     14 // You should have received a copy of the GNU Affero General Public License
     15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 //
     17 // SPDX-License-Identifier: AGPL3.0-or-later
     18 
     19 package config
     20 
     21 import (
     22 	"errors"
     23 	"fmt"
     24 	"math"
     25 	"os"
     26 	"strconv"
     27 	"strings"
     28 
     29 	"gopkg.in/ini.v1"
     30 	"gopkg.in/yaml.v3"
     31 )
     32 
     33 var CONFIG C2ECConfig
     34 
     35 type C2ECConfig struct {
     36 	Server    C2ECServerConfig     `yaml:"c2ec"`
     37 	Database  C2ECDatabseConfig    `yaml:"db"`
     38 	Providers []C2ECProviderConfig `yaml:"providers"`
     39 }
     40 
     41 type C2ECServerConfig struct {
     42 	Source                 string                `yaml:"source"`
     43 	IsProd                 bool                  `yaml:"prod"`
     44 	Host                   string                `yaml:"host"`
     45 	Port                   int                   `yaml:"port"`
     46 	UseUnixDomainSocket    bool                  `yaml:"unix-domain-socket"`
     47 	UnixSocketPath         string                `yaml:"unix-socket-path"`
     48 	UnixPathMode           int                   `yaml:"unix-path-mode"`
     49 	StrictAttestors        bool                  `yaml:"fail-on-missing-attestors"`
     50 	ExchangeBaseUrl        string                `yaml:"exchange-base-url"`
     51 	CreditAccount          string                `yaml:"credit-account"`
     52 	Currency               string                `yaml:"currency"`
     53 	CurrencyFractionDigits int                   `yaml:"currency-fraction-digits"`
     54 	WithdrawalFees         string                `yaml:"withdrawal-fees"`
     55 	MaxRetries             int32                 `yaml:"max-retries"`
     56 	RetryDelayMs           int                   `yaml:"retry-delay-ms"`
     57 	WireGateway            C2ECWireGatewayConfig `yaml:"wire-gateway"`
     58 }
     59 
     60 type C2ECWireGatewayConfig struct {
     61 	Username string `yaml:"username"`
     62 	Password string `yaml:"password"`
     63 }
     64 
     65 type C2ECDatabseConfig struct {
     66 	ConnectionString string `yaml:"connstr"`
     67 	Host             string `yaml:"host"`
     68 	Port             int    `yaml:"port"`
     69 	Username         string `yaml:"username"`
     70 	Password         string `yaml:"password"`
     71 	Database         string `yaml:"database"`
     72 }
     73 
     74 type C2ECProviderConfig struct {
     75 	Name string `yaml:"name"`
     76 	Key  string `yaml:"key"`
     77 }
     78 
     79 func Parse(path string) (*C2ECConfig, error) {
     80 
     81 	f, err := os.Open(path)
     82 	if err != nil {
     83 		return nil, err
     84 	}
     85 	defer f.Close()
     86 
     87 	stat, err := f.Stat()
     88 	if err != nil {
     89 		return nil, err
     90 	}
     91 
     92 	content := make([]byte, stat.Size())
     93 	_, err = f.Read(content)
     94 	if err != nil {
     95 		return nil, err
     96 	}
     97 
     98 	if strings.HasSuffix(path, ".yml") || strings.HasSuffix(path, ".yaml") {
     99 		cfg := new(C2ECConfig)
    100 		err = yaml.Unmarshal(content, cfg)
    101 		if err != nil {
    102 			return nil, err
    103 		}
    104 		return cfg, nil
    105 	}
    106 
    107 	cfg, err := ParseIni(content)
    108 	if err != nil {
    109 		return nil, err
    110 	}
    111 
    112 	a, err := parseAmount(cfg.Server.WithdrawalFees, cfg.Server.CurrencyFractionDigits)
    113 	if err != nil {
    114 		panic("invalid withdrawal fees amount")
    115 	}
    116 	if !strings.EqualFold(a.Currency, cfg.Server.Currency) {
    117 		panic("withdrawal fees currency must be same as the specified currency")
    118 	}
    119 
    120 	return cfg, nil
    121 }
    122 
    123 func ConfigForProvider(name string) (*C2ECProviderConfig, error) {
    124 
    125 	for _, provider := range CONFIG.Providers {
    126 
    127 		if provider.Name == name {
    128 			return &provider, nil
    129 		}
    130 	}
    131 	return nil, errors.New("no such provider")
    132 }
    133 
    134 func ParseIni(content []byte) (*C2ECConfig, error) {
    135 
    136 	ini, err := ini.Load(content)
    137 	if err != nil {
    138 		return nil, err
    139 	}
    140 
    141 	cfg := new(C2ECConfig)
    142 	for _, s := range ini.Sections() {
    143 
    144 		if s.Name() == "c2ec" {
    145 
    146 			value, err := s.GetKey("SOURCE")
    147 			if err != nil {
    148 				return nil, err
    149 			}
    150 			cfg.Server.Source = value.String()
    151 
    152 			value, err = s.GetKey("PROD")
    153 			if err != nil {
    154 				return nil, err
    155 			}
    156 
    157 			cfg.Server.IsProd, err = value.Bool()
    158 			if err != nil {
    159 				return nil, err
    160 			}
    161 
    162 			value, err = s.GetKey("SERVE")
    163 			if err != nil {
    164 				return nil, err
    165 			}
    166 
    167 			str := value.String()
    168 			cfg.Server.UseUnixDomainSocket = str == "unix"
    169 
    170 			value, err = s.GetKey("HOST")
    171 			if err != nil {
    172 				return nil, err
    173 			}
    174 
    175 			cfg.Server.Host = value.String()
    176 
    177 			value, err = s.GetKey("PORT")
    178 			if err != nil {
    179 				return nil, err
    180 			}
    181 
    182 			cfg.Server.Port, err = value.Int()
    183 			if err != nil {
    184 				return nil, err
    185 			}
    186 
    187 			value, err = s.GetKey("UNIXPATH")
    188 			if err != nil {
    189 				return nil, err
    190 			}
    191 
    192 			cfg.Server.UnixSocketPath = value.String()
    193 
    194 			value, err = s.GetKey("UNIXPATH_MODE")
    195 			if err != nil {
    196 				return nil, err
    197 			}
    198 
    199 			cfg.Server.UnixSocketPath = value.String()
    200 
    201 			value, err = s.GetKey("FAIL_ON_MISSING_ATTESTORS")
    202 			if err != nil {
    203 				return nil, err
    204 			}
    205 			cfg.Server.StrictAttestors, err = value.Bool()
    206 			if err != nil {
    207 				return nil, err
    208 			}
    209 
    210 			value, err = s.GetKey("EXCHANGE_BASE_URL")
    211 			if err != nil {
    212 				return nil, err
    213 			}
    214 			cfg.Server.ExchangeBaseUrl = value.String()
    215 
    216 			value, err = s.GetKey("EXCHANGE_ACCOUNT")
    217 			if err != nil {
    218 				return nil, err
    219 			}
    220 			cfg.Server.CreditAccount = value.String()
    221 
    222 			value, err = s.GetKey("CURRENCY")
    223 			if err != nil {
    224 				return nil, err
    225 			}
    226 			cfg.Server.Currency = value.String()
    227 
    228 			value, err = s.GetKey("CURRENCY_FRACTION_DIGITS")
    229 			if err != nil {
    230 				return nil, err
    231 			}
    232 			num, err := value.Int()
    233 			if err != nil {
    234 				return nil, err
    235 			}
    236 			cfg.Server.CurrencyFractionDigits = num
    237 
    238 			value, err = s.GetKey("WITHDRAWAL_FEES")
    239 			if err != nil {
    240 				return nil, err
    241 			}
    242 			cfg.Server.WithdrawalFees = value.String()
    243 
    244 			value, err = s.GetKey("MAX_RETRIES")
    245 			if err != nil {
    246 				return nil, err
    247 			}
    248 
    249 			num, err = value.Int()
    250 			if err != nil {
    251 				return nil, err
    252 			}
    253 			cfg.Server.MaxRetries = int32(num)
    254 
    255 			value, err = s.GetKey("RETRY_DELAY_MS")
    256 			if err != nil {
    257 				return nil, err
    258 			}
    259 
    260 			cfg.Server.RetryDelayMs, err = value.Int()
    261 			if err != nil {
    262 				return nil, err
    263 			}
    264 
    265 		}
    266 
    267 		if s.Name() == "wire-gateway" {
    268 
    269 			value, err := s.GetKey("USERNAME")
    270 			if err != nil {
    271 				return nil, err
    272 			}
    273 			cfg.Server.WireGateway.Username = value.String()
    274 
    275 			value, err = s.GetKey("PASSWORD")
    276 			if err != nil {
    277 				return nil, err
    278 			}
    279 			cfg.Server.WireGateway.Password = value.String()
    280 		}
    281 
    282 		if s.Name() == "database" {
    283 
    284 			value, err := s.GetKey("CONFIG")
    285 			if err != nil {
    286 				return nil, err
    287 			}
    288 
    289 			connstr := value.String()
    290 
    291 			cfg.Database.ConnectionString = connstr
    292 		}
    293 
    294 		if strings.HasPrefix(s.Name(), "provider-") {
    295 
    296 			provider := C2ECProviderConfig{}
    297 
    298 			value, err := s.GetKey("NAME")
    299 			if err != nil {
    300 				return nil, err
    301 			}
    302 			provider.Name = value.String()
    303 
    304 			value, err = s.GetKey("KEY")
    305 			if err != nil {
    306 				return nil, err
    307 			}
    308 			provider.Key = value.String()
    309 
    310 			cfg.Providers = append(cfg.Providers, provider)
    311 		}
    312 	}
    313 	return cfg, nil
    314 }
    315 
    316 /*
    317 	START
    318 
    319 	COPIED FROM internal/utils/amount.go
    320 	due to structuring issues. Must be resolved later.
    321 */
    322 
    323 // The GNU Taler amount object
    324 type amount struct {
    325 
    326 	// The type of currency, e.g. EUR
    327 	Currency string `json:"currency"`
    328 
    329 	// The value (before the ".")
    330 	Value uint64 `json:"value"`
    331 
    332 	// The fraction (after the ".", optional)
    333 	Fraction uint64 `json:"fraction"`
    334 }
    335 
    336 // Create a new amount from value and fraction in a currency
    337 func newAmount(currency string, value uint64, fraction uint64) amount {
    338 	return amount{
    339 		Currency: currency,
    340 		Value:    value,
    341 		Fraction: fraction,
    342 	}
    343 }
    344 
    345 // Parses an amount string in the format <currency>:<value>[.<fraction>]
    346 func parseAmount(s string, fractionDigits int) (*amount, error) {
    347 
    348 	if s == "" {
    349 		return &amount{CONFIG.Server.Currency, 0, 0}, nil
    350 	}
    351 
    352 	if !strings.Contains(s, ":") {
    353 		return nil, fmt.Errorf("invalid amount: %s", s)
    354 	}
    355 
    356 	currencyAndAmount := strings.Split(s, ":")
    357 	if len(currencyAndAmount) != 2 {
    358 		return nil, fmt.Errorf("invalid amount: %s", s)
    359 	}
    360 
    361 	currency := currencyAndAmount[0]
    362 	valueAndFraction := strings.Split(currencyAndAmount[1], ".")
    363 	if len(valueAndFraction) < 1 && len(valueAndFraction) > 2 {
    364 		return nil, fmt.Errorf("invalid value and fraction part in amount %s", s)
    365 	}
    366 	value, err := strconv.Atoi(valueAndFraction[0])
    367 	if err != nil {
    368 		return nil, fmt.Errorf("invalid value in amount %s", s)
    369 	}
    370 
    371 	fraction := 0
    372 	if len(valueAndFraction) == 2 {
    373 		if len(valueAndFraction[1]) > fractionDigits {
    374 			return nil, fmt.Errorf("invalid amount: %s expected at max %d fractional digits", s, fractionDigits)
    375 		}
    376 		k := 0
    377 		if len(valueAndFraction[1]) < fractionDigits {
    378 			k = fractionDigits - len(valueAndFraction[1])
    379 		}
    380 		fractionInt, err := strconv.Atoi(valueAndFraction[1])
    381 		if err != nil {
    382 			return nil, fmt.Errorf("invalid fraction in amount %s", s)
    383 		}
    384 		fraction = fractionInt * int(math.Pow10(k))
    385 	}
    386 
    387 	a := newAmount(currency, uint64(value), uint64(fraction))
    388 	return &a, nil
    389 }
    390 
    391 /*
    392 	END
    393 
    394 	COPIED FROM internal/utils/amount.go
    395 	due to structuring issues. Must be resolved later.
    396 */