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 */