taldir.go (40858B)
1 // This file is part of tdir, the Taler Directory implementation. 2 // Copyright (C) 2022 Martin Schanzenbach 3 // 4 // Taldir 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 // Taldir 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 taldir implements the taler directory service. 20 package taldir 21 22 /* TODO 23 - ToS compression 24 - ToS etag 25 */ 26 27 import ( 28 "crypto/sha512" 29 "encoding/base64" 30 "encoding/binary" 31 "encoding/json" 32 "errors" 33 "fmt" 34 "html/template" 35 "log" 36 "net/http" 37 "net/url" 38 "os" 39 "regexp" 40 "strings" 41 "time" 42 43 "github.com/gertd/go-pluralize" 44 "github.com/gorilla/mux" 45 "github.com/kataras/i18n" 46 _ "github.com/lib/pq" 47 "github.com/schanzen/taler-go/pkg/merchant" 48 tos "github.com/schanzen/taler-go/pkg/rest" 49 talerutil "github.com/schanzen/taler-go/pkg/util" 50 "github.com/skip2/go-qrcode" 51 "taler.net/taldir/internal/gana" 52 "taler.net/taldir/internal/util" 53 ) 54 55 // Taldir is the primary object of the Taldir service 56 type Taldir struct { 57 58 // The main router 59 Router *mux.Router 60 61 // The main DB handle 62 DB *TaldirDatabase 63 64 // Our configuration from the config.json 65 Cfg TaldirConfig 66 67 // Map of supported validators as defined in the configuration 68 Validators map[string]Validator 69 70 // Map of supported disseminators as defined in the configuration 71 Disseminators map[string]Disseminator 72 73 // imprint page 74 ImprintTpl *template.Template 75 76 // landing page 77 ValidationTpl *template.Template 78 79 // lookup result/registration page 80 LookupResultPageTpl *template.Template 81 82 // landing page 83 LandingPageTpl *template.Template 84 85 // about page 86 AboutPageTpl *template.Template 87 88 // The alias salt 89 Salt string 90 91 // The host base url 92 Host string 93 94 // Valid Payment System Address 95 ValidPMSRegex string 96 97 // The timeframe for the validation requests 98 ValidationTimeframe time.Duration 99 100 // How often may a challenge be requested 101 ValidationInitiationMax int64 102 103 // How often may a solution be attempted (in the given timeframe) 104 SolutionAttemptsMax int 105 106 // The timeframe for the above solution attempts 107 SolutionTimeframe time.Duration 108 109 // Challenge length in bytes before encoding 110 ChallengeBytes int 111 112 // Merchant object 113 Merchant merchant.Merchant 114 115 // Monthly fee amount 116 MonthlyFee *talerutil.Amount 117 118 // Registrar base URL 119 BaseURL string 120 121 // Currency Spec 122 CurrencySpec talerutil.CurrencySpecification 123 124 // I18n 125 I18n *i18n.I18n 126 127 // Logger 128 Logger TaldirLogger 129 } 130 131 // VersionResponse is the JSON response of the /config endpoint 132 type VersionResponse struct { 133 // libtool-style representation of the Merchant protocol version, see 134 // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning 135 // The format is "current:revision:age". 136 Version string `json:"version"` 137 138 // Name of the protocol. 139 Name string `json:"name"` // "taler-directory" 140 141 // Supported alias types 142 AliasType []AliasType `json:"alias_types"` 143 144 // fee for one month of registration 145 MonthlyFee string `json:"monthly_fee"` 146 } 147 148 // AliasType is part of the VersionResponse and contains a supported validator 149 type AliasType struct { 150 151 // Name of the alias type, e.g. "email" or "sms". 152 Name string `json:"name"` 153 154 // per challenge fee 155 ChallengeFee string `json:"challenge_fee"` 156 } 157 158 // RateLimitedResponse is the JSON response when a rate limit is hit 159 type RateLimitedResponse struct { 160 161 // Taler error code, TALER_EC_TALDIR_REGISTER_RATE_LIMITED. 162 Code int `json:"code"` 163 164 // At what frequency are new registrations allowed. FIXME: In what? Currently: In microseconds 165 RequestFrequency int64 `json:"request_frequency"` 166 167 // The human readable error message. 168 Hint string `json:"hint"` 169 } 170 171 // RegisterMessage is the JSON paylaod when a registration is requested 172 type RegisterMessage struct { 173 174 // Alias, in type-specific format 175 Alias string `json:"alias"` 176 177 // Target URI to associate with this alias 178 TargetURI string `json:"target_uri"` 179 180 // For how long should the registration last 181 Duration int64 `json:"duration"` 182 } 183 184 // ErrorDetail is the detailed error payload returned from Taldir endpoints 185 type ErrorDetail struct { 186 187 // Numeric error code unique to the condition. 188 // The other arguments are specific to the error value reported here. 189 Code int `json:"code"` 190 191 // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ... 192 // Should give a human-readable hint about the error's nature. Optional, may change without notice! 193 Hint string `json:"hint,omitempty"` 194 195 // Optional detail about the specific input value that failed. May change without notice! 196 Detail string `json:"detail,omitempty"` 197 198 // Name of the parameter that was bogus (if applicable). 199 Parameter string `json:"parameter,omitempty"` 200 201 // Path to the argument that was bogus (if applicable). 202 Path string `json:"path,omitempty"` 203 204 // Offset of the argument that was bogus (if applicable). 205 Offset string `json:"offset,omitempty"` 206 207 // Index of the argument that was bogus (if applicable). 208 Index string `json:"index,omitempty"` 209 210 // Name of the object that was bogus (if applicable). 211 Object string `json:"object,omitempty"` 212 213 // Name of the currency than was problematic (if applicable). 214 Currency string `json:"currency,omitempty"` 215 216 // Expected type (if applicable). 217 TypeExpected string `json:"type_expected,omitempty"` 218 219 // Type that was provided instead (if applicable). 220 TypeActual string `json:"type_actual,omitempty"` 221 } 222 223 // ValidationConfirmation is the payload sent by the client t complete a 224 // registration. 225 type ValidationConfirmation struct { 226 // The solution is the SHA-512 hash of the challenge value 227 // chosen by TalDir (encoded as string just as given in the URL, but 228 // excluding the 0-termination) concatenated with the binary 32-byte 229 // value representing the wallet's EdDSA public key. 230 // The hash is provided as string in Crockford base32 encoding. 231 Solution string `json:"solution"` 232 } 233 234 // NOTE: Go stores durations as nanoseconds. TalDir usually operates on microseconds 235 const monthDurationUs = 2592000000000 236 237 // 1 Month as Go duration 238 const monthDuration = time.Duration(monthDurationUs * 1000) 239 240 func (t *Taldir) isPMSValid(pms string) (err error) { 241 if t.ValidPMSRegex != "" { 242 matched, _ := regexp.MatchString(t.ValidPMSRegex, pms) 243 if !matched { 244 return fmt.Errorf("payment System Address `%s' invalid", pms) // TODO i18n 245 } 246 } 247 return 248 } 249 250 // Primary lookup function. 251 // Allows the caller to query a wallet key using the hash(!) of the 252 // alias 253 // 254 // @Summary Look up an alias entry 255 // @Description Returns the target URI associated with the given hashed alias. 256 // @Tags entries 257 // @Produce json 258 // @Param h_alias path string true "Crockford base32-encoded SHA-512 hash of the alias" 259 // @Success 200 {object} Entry 260 // @Failure 404 261 // @Router /{h_alias} [get] 262 func (t *Taldir) getSingleEntry(w http.ResponseWriter, r *http.Request) { 263 vars := mux.Vars(r) 264 var entry Entry 265 hsAlias := saltHAlias(vars["h_alias"], t.Salt) 266 var err = t.DB.GetEntryByHsAlias(&entry, hsAlias) 267 if err == nil { 268 w.Header().Set("Content-Type", "application/json") 269 resp, _ := json.Marshal(entry) 270 w.Write(resp) 271 return 272 } 273 w.WriteHeader(http.StatusNotFound) 274 } 275 276 // Disseminate entry 277 func (t *Taldir) disseminateStop(e Entry) error { 278 for _, d := range t.Disseminators { 279 err := d.DisseminateStop(&e) 280 if err != nil { 281 t.Logger.Logf(LogWarning, "Dissemination stop failed for disseminator `%s' and entry `%s'", d.Name(), e.HsAlias) 282 } 283 } 284 return nil 285 } 286 287 // Disseminate entry 288 func (t *Taldir) disseminateStart(e Entry) { 289 for _, d := range t.Disseminators { 290 err := d.DisseminateStart(&e) 291 if err != nil { 292 t.Logger.Logf(LogWarning, "Dissemination start failed for disseminator `%s' and entry `%s': %v", d.Name(), e.HsAlias, err) 293 } 294 } 295 } 296 297 // Disseminate all entries 298 func (t *Taldir) disseminateEntries() error { 299 entries, err := t.DB.GetAllEntries() 300 if nil != err { 301 return err 302 } 303 for _, e := range entries { 304 t.disseminateStart(e) 305 } 306 return nil 307 } 308 309 // HashAlias hashes the alias with its type in a prefix-free fashion 310 // SHA512(len(atype||alias)||atype||alias) 311 func HashAlias(atype string, alias string) []byte { 312 h := sha512.New() 313 b := make([]byte, 4) 314 binary.BigEndian.PutUint32(b, uint32(len(atype)+len(alias))) 315 h.Write(b) 316 h.Write([]byte(atype)) 317 h.Write([]byte(alias)) 318 return h.Sum(nil) 319 } 320 321 // Hashes an identity key (see hashAlias) with a salt for 322 // Lookup and storage. 323 func saltHAlias(hAlias string, salt string) string { 324 h := sha512.New() 325 h.Write([]byte(hAlias)) 326 h.Write([]byte(salt)) 327 return util.Base32CrockfordEncode(h.Sum(nil)) 328 } 329 330 // Called by the registrant to validate the registration request. The reference ID was 331 // provided "out of band" using a validation method such as email or SMS 332 // 333 // @Summary Complete alias registration 334 // @Description Submits the solution to the out-of-band challenge to confirm the registration. 335 // @Tags registration 336 // @Accept json 337 // @Param h_alias path string true "Crockford base32-encoded SHA-512 hash of the alias" 338 // @Param body body ValidationConfirmation true "Challenge solution" 339 // @Success 204 "Registration confirmed" 340 // @Failure 400 {object} ErrorDetail "Invalid JSON" 341 // @Failure 403 "Wrong solution" 342 // @Failure 404 "Validation not found" 343 // @Failure 429 "Too many solution attempts" 344 // @Failure 500 345 // @Router /{h_alias} [post] 346 func (t *Taldir) validationRequest(w http.ResponseWriter, r *http.Request) { 347 vars := mux.Vars(r) 348 var entry Entry 349 var validation Validation 350 var confirm ValidationConfirmation 351 var errDetail ErrorDetail 352 if r.Body == nil { 353 http.Error(w, "No request body", http.StatusBadRequest) 354 return 355 } 356 err := json.NewDecoder(r.Body).Decode(&confirm) 357 if err != nil { 358 errDetail.Code = 1006 //TALER_EC_JSON_INVALID 359 errDetail.Hint = "Unable to parse JSON" 360 resp, _ := json.Marshal(errDetail) 361 w.WriteHeader(http.StatusBadRequest) 362 w.Write(resp) 363 return 364 } 365 err = t.DB.GetFirstValidationByHAlias(&validation, vars["h_alias"]) 366 t.Logger.Logf(LogDebug, "Got validation %v", validation) 367 if err != nil { 368 w.WriteHeader(http.StatusNotFound) 369 return 370 } 371 validation.SolutionAttemptCount++ 372 if time.UnixMicro(validation.LastSolutionTimeframeStart + t.SolutionTimeframe.Microseconds()).After(time.Now()) { 373 if validation.SolutionAttemptCount > t.SolutionAttemptsMax { 374 w.WriteHeader(http.StatusTooManyRequests) 375 return 376 } 377 } else { 378 t.Logger.Logf(LogDebug, "New solution timeframe set.") 379 validation.LastSolutionTimeframeStart = time.Now().UnixMicro() 380 validation.SolutionAttemptCount = 1 381 } 382 t.DB.UpdateValidation(&validation) 383 t.Logger.Logf(LogDebug, "Generating solution from %s and %s", validation.TargetURI, validation.Challenge) 384 expectedSolution := util.GenerateSolution(validation.TargetURI, validation.Challenge) 385 386 t.Logger.Logf(LogDebug, "Expected solution: `%s', given: `%s'\n", expectedSolution, confirm.Solution) 387 if confirm.Solution != expectedSolution { 388 w.WriteHeader(http.StatusForbidden) 389 return 390 } 391 _, err = t.DB.DeleteValidation(&validation) 392 if err != nil { 393 t.Logger.Logf(LogError, "Error deleting validation: %v", err) 394 w.WriteHeader(http.StatusInternalServerError) 395 return 396 } 397 entry.HsAlias = saltHAlias(validation.HAlias, t.Salt) 398 tmpDuration := (entry.Duration + validation.Duration) * 1000 399 err = t.DB.GetEntryByHsAlias(&entry, entry.HsAlias) 400 if err == nil { 401 if validation.TargetURI == "" { 402 _, err = t.DB.DeleteEntry(&entry) 403 if err != nil { 404 t.Logger.Logf(LogError, "Error deleting entry: %v", err) 405 w.WriteHeader(http.StatusInternalServerError) 406 return 407 } 408 t.Logger.Logf(LogDebug, "Deleted entry for '%s´\n", entry.HsAlias) 409 t.disseminateStop(entry) 410 } else { 411 entry.TargetURI = validation.TargetURI 412 entry.Duration = tmpDuration 413 err = t.DB.UpdateEntry(&entry) 414 t.Logger.Logf(LogDebug, "Updated entry in database to: %v", entry) 415 if err != nil { 416 t.Logger.Logf(LogError, "Error updating entry: %v", err) 417 w.WriteHeader(http.StatusInternalServerError) 418 return 419 } 420 t.disseminateStart(entry) 421 } 422 } else { 423 t.Logger.Logf(LogError, "Entry does not yet exist: %v", err) 424 if validation.TargetURI == "" { 425 t.Logger.Logf(LogWarning, "Validated a deletion request but no entry found for `%s'\n", entry.HsAlias) 426 } else { 427 entry.TargetURI = validation.TargetURI 428 err = t.DB.InsertEntry(&entry) 429 if err != nil { 430 t.Logger.Logf(LogError, "Error inserting entry: %v", err) 431 w.WriteHeader(http.StatusInternalServerError) 432 return 433 } 434 t.Logger.Logf(LogError, "Inserted entry: %v", entry) 435 } 436 } 437 w.WriteHeader(http.StatusNoContent) 438 } 439 440 func (t *Taldir) isRateLimited(hAlias string) (bool, error) { 441 validations, err := t.DB.GetAllValidationsByHAlias(hAlias) 442 // NOTE: Check rate limit 443 if err == nil { 444 // Limit re-initiation attempts to ValidationInitiationMax times 445 // within the expiration timeframe of a validation. 446 t.Logger.Logf(LogDebug, "Pending validations are %d", len(validations)) 447 return len(validations) >= int(t.ValidationInitiationMax), nil 448 } 449 return false, nil 450 } 451 452 // registerRequest initiates the registration or update of an alias. 453 // 454 // @Summary Initiate alias registration 455 // @Description Starts the registration process for an alias. Sends an out-of-band challenge 456 // @Description via the specified validator (e.g. email or SMS). If the entry already exists 457 // @Description with no changes, returns the remaining validity instead. 458 // @Tags registration 459 // @Accept json 460 // @Produce json 461 // @Param alias_type path string true "Alias type (e.g. \"email\", \"sms\")" 462 // @Param body body RegisterMessage true "Registration request" 463 // @Success 200 {object} object{valid_for=integer} "Existing entry unchanged; returns remaining validity in microseconds" 464 // @Success 202 "Challenge sent" 465 // @Failure 400 {object} ErrorDetail "Invalid request body or target URI" 466 // @Failure 402 "Payment required" 467 // @Failure 404 {object} ErrorDetail "Alias type not supported" 468 // @Failure 429 {object} RateLimitedResponse "Registration rate limit reached" 469 // @Failure 500 470 // @Router /register/{alias_type} [post] 471 func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request) { 472 vars := mux.Vars(r) 473 var req RegisterMessage 474 var errDetail ErrorDetail 475 var validation Validation 476 var entry Entry 477 478 // Check if this validation method is supported or not. 479 validator, ok := t.Validators[vars["alias_type"]] 480 if !ok { 481 // FIXME rename GANA entry to alias type 482 errDetail.Code = gana.TALDIR_METHOD_NOT_SUPPORTED 483 errDetail.Hint = "Unsupported alias_type" 484 errDetail.Detail = "Given alias_type: " + vars["alias_type"] 485 resp, _ := json.Marshal(errDetail) 486 w.WriteHeader(http.StatusNotFound) 487 w.Write(resp) 488 return 489 } 490 if r.Body == nil { 491 http.Error(w, "No request body", http.StatusBadRequest) 492 return 493 } 494 err := json.NewDecoder(r.Body).Decode(&req) 495 if err != nil { 496 errDetail.Code = gana.GENERIC_JSON_INVALID 497 errDetail.Hint = "Unable to parse JSON" 498 resp, _ := json.Marshal(errDetail) 499 w.WriteHeader(http.StatusBadRequest) 500 w.Write(resp) 501 return 502 } 503 t.Logger.Logf(LogDebug, "Received registerRequest %v", req) 504 505 if req.TargetURI != "" { 506 err = t.isPMSValid(req.TargetURI) 507 if nil != err { 508 errDetail.Code = gana.GENERIC_JSON_INVALID 509 errDetail.Hint = err.Error() 510 w.Header().Set("Content-Type", "application/json") 511 resp, _ := json.Marshal(errDetail) 512 w.WriteHeader(http.StatusBadRequest) 513 w.Write(resp) 514 return 515 } 516 } 517 518 // Setup validation object. Retrieve object from DB if it already 519 // exists. 520 hAliasBin := HashAlias(validator.Name(), req.Alias) 521 hAlias := util.Base32CrockfordEncode(hAliasBin) 522 validation.HAlias = hAlias 523 validation.ValidatorName = validator.Name() 524 hsAlias := saltHAlias(validation.HAlias, t.Salt) 525 err = t.DB.GetEntryByHsAlias(&entry, hsAlias) 526 // Round to the nearest multiple of a month 527 reqDuration := time.Duration(req.Duration * 1000) 528 reqDuration = reqDuration.Round(monthDuration) 529 if err == nil { 530 t.Logger.Logf(LogDebug, "Found entry in database %v matching %v", entry, hsAlias) 531 // Check if this entry is to be modified or extended 532 entryModified := (req.TargetURI != entry.TargetURI) 533 entryValidity := time.UnixMicro(entry.CreatedAt + entry.Duration) 534 // NOTE: The extension must be at least one month 535 t.Logger.Logf(LogDebug, "Entry to be modified: %t, requested (rounded) duration: %d", entryModified, reqDuration.Microseconds()) 536 if (reqDuration.Microseconds() == 0) && !entryModified { 537 // Nothing changed. Return validity 538 t.Logger.Logf(LogDebug, "Returning validity of entry") 539 w.WriteHeader(http.StatusOK) 540 w.Header().Set("Content-Type", "application/json") 541 w.Write(fmt.Appendf(make([]byte, 0), "{\"valid_for\": %d}", time.Until(entryValidity).Microseconds())) 542 return 543 } 544 } 545 rateLimited, err := t.isRateLimited(hAlias) 546 if nil != err { 547 t.Logger.Logf(LogError, "Error checking rate limit! %v", err) 548 w.WriteHeader(http.StatusInternalServerError) 549 return 550 } else if rateLimited { 551 w.WriteHeader(http.StatusTooManyRequests) 552 rlResponse := RateLimitedResponse{ 553 Code: gana.TALDIR_REGISTER_RATE_LIMITED, 554 RequestFrequency: t.ValidationTimeframe.Microseconds() / t.ValidationInitiationMax, 555 Hint: "Registration rate limit reached", 556 } 557 jsonResp, _ := json.Marshal(rlResponse) 558 w.Write(jsonResp) 559 t.DB.DeleteValidation(&validation) 560 return 561 } 562 t.Logger.Logf(LogDebug, "Looking for validation with %v %v %v\n", hAlias, req.TargetURI, reqDuration.Microseconds()) 563 err = t.DB.GetValidation(&validation, hAlias, req.TargetURI, reqDuration) 564 validationExists := (nil == err) 565 t.Logger.Logf(LogDebug, "Validation exists %v\n", validationExists) 566 // FIXME: Always set new challenge? 567 validation.Challenge = util.GenerateChallenge(t.ChallengeBytes) 568 validation.TargetURI = req.TargetURI 569 validation.SolutionAttemptCount = 0 570 validation.LastSolutionTimeframeStart = time.Now().UnixMicro() 571 validation.Duration = reqDuration.Microseconds() 572 validation.CreatedAt = validation.LastSolutionTimeframeStart 573 t.Logger.Logf(LogDebug, "Storing new validation %v\n", validation) 574 err = t.DB.InsertValidation(&validation) 575 if nil != err { 576 t.Logger.Logf(LogError, "Error inserting validation! %v", err) 577 w.WriteHeader(http.StatusInternalServerError) 578 return 579 } 580 581 sliceDuration := time.Duration(validation.Duration * 1000) 582 cost, err := util.CalculateCost(t.MonthlyFee.String(), 583 validator.ChallengeFee(), 584 sliceDuration, 585 monthDuration) 586 if err != nil { 587 fmt.Println(err) 588 w.WriteHeader(http.StatusInternalServerError) 589 return 590 } 591 if !cost.IsZero() { 592 validation.RequiresPayment = true 593 if len(validation.OrderID) == 0 { 594 // Add new order for new validations 595 // FIXME: What is the URL we want to provide here? 596 orderID, newOrderErr := t.Merchant.AddNewOrder(*cost, "Taldir registration", t.BaseURL) 597 if newOrderErr != nil { 598 fmt.Println(newOrderErr) 599 w.WriteHeader(http.StatusInternalServerError) 600 return 601 } 602 validation.OrderID = orderID 603 } 604 605 // Check if order paid. 606 // FIXME: Remember that it was activated and paid 607 // FIXME: We probably need to handle the return code here (see gns registrar for how) 608 _, _, payto, paytoErr := t.Merchant.IsOrderPaid(validation.OrderID) 609 if paytoErr != nil { 610 w.WriteHeader(http.StatusInternalServerError) 611 t.Logger.Logf(LogError, "%s\n", paytoErr) 612 return 613 } 614 if len(payto) != 0 { 615 err = t.DB.UpdateValidation(&validation) 616 if nil != err { 617 t.Logger.Logf(LogError, "Error inserting validation! %v", err) 618 w.WriteHeader(http.StatusInternalServerError) 619 return 620 } 621 w.WriteHeader(http.StatusPaymentRequired) 622 w.Header().Set("Taler", payto) // FIXME no idea what to do with this. 623 return 624 } 625 // In this case, this order was paid 626 } 627 err = t.DB.UpdateValidation(&validation) 628 if err != nil { 629 t.Logger.Logf(LogError, "%s\n", err.Error()) 630 w.WriteHeader(http.StatusInternalServerError) 631 return 632 } 633 topic := t.I18n.GetLocale(r).GetMessage("taldirRegTopic") 634 link := t.Host + "/register/" + url.QueryEscape(validation.HAlias) + "/" + url.QueryEscape(validation.Challenge) + "?alias=" + url.QueryEscape(req.Alias) 635 message := t.I18n.GetLocale(r).GetMessage("taldirRegMessage", link) 636 redirectionLink, err := validator.RegistrationStart(topic, link, message, req.Alias, validation.Challenge) 637 if err != nil { 638 t.Logger.Logf(LogError, "%s\n", err.Error()) 639 t.DB.DeleteValidation(&validation) 640 w.WriteHeader(http.StatusInternalServerError) 641 return 642 } 643 // FIXME does this persist this boolean or do we need to call Db.Save again? 644 validation.ChallengeSent = true 645 if len(redirectionLink) > 0 { 646 // This is dangerous, of course, but our validators are trusted, right? 647 w.Header().Set("Location", redirectionLink) 648 } 649 w.WriteHeader(http.StatusAccepted) 650 } 651 652 func (t *Taldir) oidcValidatorResponse(w http.ResponseWriter, r *http.Request) { 653 vars := mux.Vars(r) 654 for name, validator := range t.Validators { 655 if validator.Type() != ValidatorTypeOIDC { 656 continue 657 } 658 if name != vars["validator"] { 659 continue 660 } 661 oidcValidator := validator.(OidcValidator) 662 alias, challenge, err := oidcValidator.ProcessOidcCallback(r) 663 if err != nil { 664 t.Logger.Logf(LogError, "%s\n", err.Error()) 665 w.WriteHeader(http.StatusInternalServerError) 666 return 667 } 668 ha := HashAlias(validator.Name(), alias) 669 hAlias := util.Base32CrockfordEncode(ha) 670 http.Redirect(w, r, fmt.Sprintf("/register/%s/%s?alias=%s", hAlias, challenge, alias), http.StatusSeeOther) 671 return 672 } 673 w.WriteHeader(http.StatusNotFound) 674 } 675 676 // configResponse returns the service configuration. 677 // 678 // @Summary Get service configuration 679 // @Description Returns service metadata including the supported alias types and monthly fee. 680 // @Tags config 681 // @Produce json 682 // @Success 200 {object} VersionResponse 683 // @Router /config [get] 684 func (t *Taldir) configResponse(w http.ResponseWriter, r *http.Request) { 685 meths := []AliasType{} 686 i := 0 687 for key := range t.Validators { 688 var meth AliasType 689 meth.Name = key 690 meth.ChallengeFee = t.Validators[key].ChallengeFee() 691 i++ 692 meths = append(meths, meth) 693 } 694 cfg := VersionResponse{ 695 Version: "0:0:0", 696 Name: "taler-directory", 697 MonthlyFee: t.Cfg.Ini.GetString("directory", "monthly_fee", "KUDOS:1"), 698 AliasType: meths, 699 } 700 w.Header().Set("Content-Type", "application/json") 701 response, _ := json.Marshal(cfg) 702 w.Write(response) 703 } 704 705 func (t *Taldir) validationPage(w http.ResponseWriter, r *http.Request) { 706 vars := mux.Vars(r) 707 var walletLink string 708 var alias string 709 var png []byte 710 var validation Validation 711 712 err := t.DB.GetFirstValidationByHAlias(&validation, vars["h_alias"]) 713 w.Header().Set("Content-Type", "text/html; charset=utf-8") 714 if err != nil { 715 // This validation does not exist. 716 w.WriteHeader(http.StatusNotFound) 717 return 718 } 719 if vars["challenge"] != validation.Challenge { 720 t.Logger.Logf(LogWarning, "Solution does not match challenge!\n") 721 w.WriteHeader(http.StatusBadRequest) 722 return 723 } 724 725 alias = r.URL.Query().Get("alias") 726 727 if alias == "" { 728 w.WriteHeader(http.StatusNotFound) 729 return 730 } 731 732 // FIXME requires a prefix-free encoding 733 hAliasBin := HashAlias(validation.ValidatorName, alias) 734 expectedHAlias := util.Base32CrockfordEncode(hAliasBin) 735 736 if expectedHAlias != validation.HAlias { 737 t.Logger.Logf(LogWarning, "Alias does not match challenge!\n") 738 w.WriteHeader(http.StatusBadRequest) 739 return 740 } 741 742 // FIXME: This is kind of broken and probably requires wallet support/integration first 743 if validation.RequiresPayment { 744 t.Logger.Logf(LogWarning, "Validation requires payment\n") 745 walletLink = "taler://taldir/" + vars["h_alias"] + "/" + vars["challenge"] + "-wallet" 746 png, err = qrcode.Encode(walletLink, qrcode.Medium, 256) 747 if err != nil { 748 w.WriteHeader(http.StatusInternalServerError) 749 return 750 } 751 encodedPng := base64.StdEncoding.EncodeToString(png) 752 753 fullData := map[string]any{ 754 "version": t.Cfg.Version, 755 "QRCode": template.URL("data:image/png;base64," + encodedPng), 756 "WalletLink": template.URL(walletLink), 757 "productDisclaimerShort": template.HTML(t.I18n.GetLocale(r).GetMessage("productDisclaimerShort")), 758 } 759 t.ValidationTpl.Execute(w, fullData) 760 } else { 761 expectedSolution := util.GenerateSolution(validation.TargetURI, validation.Challenge) 762 confirmDeletionOrRegistration := "" 763 if validation.TargetURI == "" { 764 confirmDeletionOrRegistration = t.I18n.GetLocale(r).GetMessage("confirmDelete", alias) 765 } else { 766 confirmDeletionOrRegistration = t.I18n.GetLocale(r).GetMessage("confirmReg", alias, validation.TargetURI) 767 } 768 fullData := map[string]any{ 769 "version": t.Cfg.Version, 770 "error": r.URL.Query().Get("error"), 771 "target_uri": template.URL(validation.TargetURI), 772 "alias": template.URL(alias), 773 "halias": template.URL(validation.HAlias), 774 "solution": template.URL(expectedSolution), 775 "confirmDeletionOrRegistration": template.HTML(confirmDeletionOrRegistration), 776 "productDisclaimerShort": template.HTML(t.I18n.GetLocale(r).GetMessage("productDisclaimerShort")), 777 "tr": t.I18n.GetLocale(r).GetMessage, 778 } 779 t.ValidationTpl.Execute(w, fullData) 780 } 781 } 782 783 // ClearDatabase nukes the database (for tests) 784 func (t *Taldir) ClearDatabase() { 785 err := t.DB.ClearDatabase() 786 if err != nil { 787 t.Logger.Logf(LogWarning, "Error clearing database: %v", err) 788 } 789 } 790 791 func (t *Taldir) termsResponse(w http.ResponseWriter, r *http.Request) { 792 termspath := t.Cfg.Ini.GetFilename("directory", "default_terms_path", "terms/", t.Cfg.Datahome) 793 tos.ServiceTermsResponse(w, r, termspath, tos.TalerTosConfig{ 794 DefaultFileType: t.Cfg.Ini.GetString("directory", "default_doc_filetype", "text/html"), 795 DefaultLanguage: t.Cfg.Ini.GetString("directory", "default_doc_lang", "en"), 796 SupportedFileTypes: strings.Split(t.Cfg.Ini.GetString("directory", "supported_doc_filetypes", ""), " "), 797 }) 798 } 799 800 func (t *Taldir) privacyResponse(w http.ResponseWriter, r *http.Request) { 801 pppath := t.Cfg.Ini.GetFilename("directory", "default_pp_path", "privacy/", t.Cfg.Datahome) 802 tos.PrivacyPolicyResponse(w, r, pppath, tos.TalerTosConfig{ 803 DefaultFileType: t.Cfg.Ini.GetString("directory", "default_doc_filetype", "text/html"), 804 DefaultLanguage: t.Cfg.Ini.GetString("directory", "default_doc_lang", "en"), 805 SupportedFileTypes: strings.Split(t.Cfg.Ini.GetString("directory", "supported_doc_filetypes", ""), " "), 806 }) 807 } 808 809 func (t *Taldir) landingPage(w http.ResponseWriter, r *http.Request) { 810 w.Header().Set("Content-Type", "text/html; charset=utf-8") 811 translateFunc := t.I18n.GetLocale(r).GetMessage 812 fullData := map[string]any{ 813 "validators": t.Validators, 814 "version": t.Cfg.Version, 815 "lookupOrRegisterCardTitle": template.HTML(translateFunc("lookup")), 816 "selectAliasToLookupCardText": template.HTML(translateFunc("selectAliasToLookup")), 817 "registerCardText": template.HTML(translateFunc("howtoRegisterOrModify")), 818 "productDisclaimerShort": template.HTML(translateFunc("productDisclaimerShort")), 819 "error": translateFunc(r.URL.Query().Get("error")), 820 "tr": translateFunc, 821 } 822 err := t.LandingPageTpl.Execute(w, fullData) 823 if err != nil { 824 fmt.Println(err) 825 } 826 } 827 828 func (t *Taldir) imprintPage(w http.ResponseWriter, r *http.Request) { 829 w.Header().Set("Content-Type", "text/html; charset=utf-8") 830 translateFunc := t.I18n.GetLocale(r).GetMessage 831 fullData := map[string]any{ 832 "validators": t.Validators, 833 "version": t.Cfg.Version, 834 "productDisclaimerShort": template.HTML(translateFunc("productDisclaimerShort")), 835 "error": translateFunc(r.URL.Query().Get("error")), 836 "tr": translateFunc, 837 } 838 err := t.ImprintTpl.Execute(w, fullData) 839 if err != nil { 840 fmt.Println(err) 841 } 842 } 843 844 func (t *Taldir) aboutPage(w http.ResponseWriter, r *http.Request) { 845 w.Header().Set("Content-Type", "text/html; charset=utf-8") 846 translateFunc := t.I18n.GetLocale(r).GetMessage 847 fullData := map[string]any{ 848 "validators": t.Validators, 849 "version": t.Cfg.Version, 850 "productDisclaimerShort": template.HTML(translateFunc("productDisclaimerShort")), 851 "productDisclaimer": template.HTML(translateFunc("productDisclaimer")), 852 "error": translateFunc(r.URL.Query().Get("error")), 853 "tr": translateFunc, 854 } 855 err := t.AboutPageTpl.Execute(w, fullData) 856 if err != nil { 857 fmt.Println(err) 858 } 859 } 860 861 func (t *Taldir) typeLookupResultPage(w http.ResponseWriter, r *http.Request) { 862 var entry Entry 863 vars := mux.Vars(r) 864 w.Header().Set("Content-Type", "text/html; charset=utf-8") 865 866 // Check if this alias type is supported or not. 867 val, ok := t.Validators[vars["alias_type"]] 868 if !ok { 869 w.WriteHeader(http.StatusNotFound) 870 return 871 } 872 873 // Check if alias is valid 874 alias := r.URL.Query().Get("alias") 875 err := val.IsAliasValid(alias) 876 emsg := "" 877 found := false 878 if nil != err { 879 t.Logger.Logf(LogWarning, "Not a valid alias\n") 880 emsg = t.I18n.GetLocale(r).GetMessage("aliasInvalid", alias) 881 http.Redirect(w, r, fmt.Sprintf("/landing/"+val.Name()+"?error=%s", emsg), http.StatusSeeOther) 882 return 883 } else { 884 hAliasBin := HashAlias(val.Name(), r.URL.Query().Get("alias")) 885 hAlias := util.Base32CrockfordEncode(hAliasBin[:]) 886 hsAlias := saltHAlias(hAlias, t.Salt) 887 err := t.DB.GetEntryByHsAlias(&entry, hsAlias) 888 if err != nil { 889 t.Logger.Logf(LogError, "`%s` not found.\n", hAlias) 890 } else { 891 found = true 892 } 893 } 894 encodedPng := "" 895 talerAddContactURI := "" 896 if found && strings.HasPrefix(entry.TargetURI, "https://") { 897 // This could be a mailbox URI and we can create a helper QR code for import 898 hostDomain := strings.TrimPrefix(entry.TargetURI, "https://") 899 talerAddContactURI, err = url.JoinPath("taler://add-contact", val.Name(), r.URL.Query().Get("alias"), hostDomain) 900 if nil == err { 901 talerAddContactURI += "?sourceBaseUrl=" + url.QueryEscape(t.BaseURL) 902 qrPng, qrErr := qrcode.Encode(talerAddContactURI, qrcode.Medium, 256) 903 if qrErr != nil { 904 t.Logger.Logf(LogError, "Failed to create QR code") 905 w.WriteHeader(http.StatusInternalServerError) 906 return 907 } 908 encodedPng = base64.StdEncoding.EncodeToString(qrPng) 909 } 910 } 911 912 fullData := map[string]any{ 913 "version": t.Cfg.Version, 914 "qrCode": template.URL("data:image/png;base64," + encodedPng), 915 "talerAddContactURI": template.URL(talerAddContactURI), 916 "available": !found, 917 "alias_type": val.Name(), 918 "alias": r.URL.Query().Get("alias"), 919 "result": entry.TargetURI, 920 "error": emsg, 921 "productDisclaimerShort": template.HTML(t.I18n.GetLocale(r).GetMessage("productDisclaimerShort")), 922 "tr": t.I18n.GetLocale(r).GetMessage, 923 } 924 err = t.LookupResultPageTpl.Execute(w, fullData) 925 if err != nil { 926 fmt.Println(err) 927 } 928 } 929 930 func (t *Taldir) typeLandingPage(w http.ResponseWriter, r *http.Request) { 931 vars := mux.Vars(r) 932 w.Header().Set("Content-Type", "text/html; charset=utf-8") 933 934 // Check if this alias type is supported or not. 935 val, ok := t.Validators[vars["alias_type"]] 936 if !ok { 937 w.WriteHeader(http.StatusNotFound) 938 return 939 } 940 fullData := map[string]any{ 941 "version": t.Cfg.Version, 942 "error": r.URL.Query().Get("error"), 943 "productDisclaimerShort": template.HTML(t.I18n.GetLocale(r).GetMessage("productDisclaimerShort")), 944 "tr": t.I18n.GetLocale(r).GetMessage, 945 } 946 err := val.LandingPageTpl().Execute(w, fullData) 947 if err != nil { 948 fmt.Println(err) 949 } 950 } 951 952 func (t *Taldir) setupHandlers() { 953 t.Router = mux.NewRouter().StrictSlash(true) 954 955 /* ToS API */ 956 t.Router.HandleFunc("/terms", t.termsResponse).Methods("GET") 957 t.Router.HandleFunc("/privacy", t.privacyResponse).Methods("GET") 958 t.Router.HandleFunc("/imprint", t.imprintPage).Methods("GET") 959 960 /* About page */ 961 t.Router.HandleFunc("/about", t.aboutPage).Methods("GET") 962 963 /* Config API */ 964 t.Router.HandleFunc("/config", t.configResponse).Methods("GET") 965 966 /* Assets HTML */ 967 t.Router.PathPrefix("/css").Handler(http.StripPrefix("/css", http.FileServer(http.Dir(t.getFileName("static/css"))))) 968 t.Router.PathPrefix("/images").Handler(http.StripPrefix("/images", http.FileServer(http.Dir(t.getFileName("static/images"))))) 969 t.Router.PathPrefix("/fontawesome").Handler(http.StripPrefix("/fontawesome", http.FileServer(http.Dir(t.getFileName("static/fontawesome"))))) 970 971 /* Registration API */ 972 t.Router.HandleFunc("/", t.landingPage).Methods("GET") 973 t.Router.HandleFunc("/{h_alias}", t.getSingleEntry).Methods("GET") 974 t.Router.HandleFunc("/lookup/{alias_type}", t.typeLookupResultPage).Methods("GET") 975 t.Router.HandleFunc("/landing/{alias_type}", t.typeLandingPage).Methods("GET") 976 t.Router.HandleFunc("/register/{alias_type}", t.registerRequest).Methods("POST") 977 t.Router.HandleFunc("/register/{h_alias}/{challenge}", t.validationPage).Methods("GET") 978 t.Router.HandleFunc("/{h_alias}", t.validationRequest).Methods("POST") 979 980 // OIDC validator callback URI(s) 981 t.Router.HandleFunc("/oidc_validator/{validator}", t.oidcValidatorResponse).Methods("GET") 982 983 } 984 985 var pluralizeClient = pluralize.NewClient() 986 987 func getFuncs(current *i18n.Locale) template.FuncMap { 988 return template.FuncMap{ 989 "plural": func(word string, count int) string { 990 return pluralizeClient.Pluralize(word, count, true) 991 }, 992 } 993 } 994 995 func (t *Taldir) getFileName(relativeFileName string) string { 996 _, err := os.Stat(relativeFileName) 997 if errors.Is(err, os.ErrNotExist) { 998 _, err := os.Stat(t.Cfg.Datahome + "/" + relativeFileName) 999 if errors.Is(err, os.ErrNotExist) { 1000 t.Logger.Logf(LogError, "Tried fallback not found %s\n", t.Cfg.Datahome+"/"+relativeFileName) 1001 return "" 1002 } 1003 return t.Cfg.Datahome + "/" + relativeFileName 1004 } 1005 return relativeFileName 1006 } 1007 1008 // Initialize the Taldir instance with cfgfile 1009 func (t *Taldir) Initialize(cfg TaldirConfig) { 1010 t.Cfg = cfg 1011 t.Logger = TaldirLogger{ 1012 InternalLogger: log.New(os.Stdout, "taler-directory:", log.LstdFlags), 1013 logLevel: cfg.Loglevel, 1014 } 1015 // FIXME localedir 1016 i18n, err := i18n.New(i18n.Glob("./locales/*/*", i18n.LoaderConfig{ 1017 // Set custom functions per locale! 1018 Funcs: getFuncs, 1019 }), "en-US", "de-DE") 1020 if err != nil { 1021 panic(err) 1022 } 1023 t.I18n = i18n 1024 1025 navTplFile := cfg.Ini.GetFilename("directory", "navigation", "web/templates/nav.html", t.Cfg.Datahome) 1026 footerTplFile := cfg.Ini.GetFilename("directory", "footer", "web/templates/footer.html", t.Cfg.Datahome) 1027 t.BaseURL = cfg.Ini.GetString("directory", "base_url", "http://localhost:11000") 1028 t.Validators = make(map[string]Validator) 1029 for _, sec := range cfg.Ini.IterateSections("directory-validator-") { 1030 if !strings.HasPrefix(sec, "directory-validator-") { 1031 continue 1032 } 1033 vname := strings.TrimPrefix(sec, "directory-validator-") 1034 if !cfg.Ini.GetBool(sec, "enabled", false) { 1035 t.Logger.Logf(LogWarning, "`Validator `%s' disabled.\n", vname) 1036 continue 1037 } 1038 vlandingPageTplFile := cfg.Ini.GetFilename(sec, "registration_page", "web/templates/landing_"+vname+".html", t.Cfg.Datahome) 1039 vlandingPageTpl, err := template.ParseFiles(vlandingPageTplFile, navTplFile, footerTplFile) 1040 if err != nil { 1041 t.Logger.Logf(LogWarning, "`%s` template not found, disabling validator `%s`: `%v`\n", vlandingPageTplFile, vname, err) 1042 continue 1043 } 1044 var v Validator 1045 vtype := cfg.Ini.GetString(sec, "type", "") 1046 if len(vtype) == 0 { 1047 t.Logger.Logf(LogWarning, "`type` key in section `[%s]` not found, disabling validator.\n", sec) 1048 continue 1049 } 1050 switch vtype { 1051 case string(ValidatorTypeCommand): 1052 v = Validator(makeCommandValidator(&cfg, vname, vlandingPageTpl)) 1053 case string(ValidatorTypeOIDC): 1054 v = makeOidcValidator(&cfg, vname, vlandingPageTpl) 1055 default: 1056 t.Logger.Logf(LogWarning, "`%s` type unknown, disabling validator `%s`.\n", vtype, vname) 1057 continue 1058 } 1059 t.Validators[vname] = v 1060 t.Logger.Logf(LogDebug, "`%s` validator enabled.\n", vname) 1061 } 1062 t.Logger.Logf(LogDebug, "Found %d validators.\n", len(t.Validators)) 1063 t.Disseminators = make(map[string]Disseminator) 1064 gnsdisseminator := makeGnsDisseminator(&cfg) 1065 if gnsdisseminator.IsEnabled() { 1066 t.Disseminators[gnsdisseminator.Name()] = &gnsdisseminator 1067 t.Logger.Logf(LogInfo, "Disseminator `%s' enabled.\n", gnsdisseminator.Name()) 1068 } 1069 t.ChallengeBytes = cfg.Ini.GetInt("directory", "challenge_bytes", 16) 1070 t.ValidationInitiationMax = cfg.Ini.GetInt64("directory", "validation_initiation_max", 3) 1071 t.SolutionAttemptsMax = cfg.Ini.GetInt("directory", "solution_attempt_max", 3) 1072 1073 t.ValidPMSRegex = cfg.Ini.GetString("directory", "valid_payment_system_address_regex", ".*") 1074 t.ValidationTimeframe, err = cfg.Ini.GetDuration("directory", "validation_timeframe", time.Minute*5) 1075 if err != nil { 1076 t.Logger.InternalLogger.Fatal(err) 1077 os.Exit(1) 1078 } 1079 1080 t.SolutionTimeframe, err = cfg.Ini.GetDuration("directory", "solution_attempt_timeframe", time.Hour) 1081 if err != nil { 1082 t.Logger.InternalLogger.Fatal(err) 1083 os.Exit(1) 1084 } 1085 t.MonthlyFee, err = cfg.Ini.GetAmount("directory", "monthly_fee", &talerutil.Amount{}) 1086 if err != nil { 1087 t.Logger.InternalLogger.Fatal(err) 1088 os.Exit(1) 1089 } 1090 1091 t.DB = cfg.Db 1092 if cfg.Ini.GetBool("directory", "purge_mappings_on_startup_dangerous", false) { 1093 t.Logger.Logf(LogWarning, "DANGER Purging mappings!") 1094 num, err := t.DB.DeleteAllEntries() 1095 if err != nil { 1096 t.Logger.Logf(LogDebug, "Error purging entries: `%v'.\n", err) 1097 } 1098 t.Logger.Logf(LogDebug, "Deleted %d entries.\n", num) 1099 } 1100 // Clean up validations 1101 validationExp, err := cfg.Ini.GetDuration("directory", "validation_expiration", time.Hour*24) 1102 if err != nil { 1103 t.Logger.InternalLogger.Fatal(err) 1104 os.Exit(1) 1105 } 1106 go func() { 1107 for { 1108 num, err := t.DB.DeleteStaleValidations(validationExp) 1109 if err != nil { 1110 t.Logger.Logf(LogDebug, "Error purging stale validations: `%v'.\n", err) 1111 } 1112 t.Logger.Logf(LogInfo, "Cleaned up %d stale validations.\n", num) 1113 time.Sleep(validationExp) 1114 } 1115 }() 1116 imprintTplFile := cfg.Ini.GetFilename("directory", "imprint_page", "web/templates/imprint.html", t.Cfg.Datahome) 1117 t.ImprintTpl, err = template.ParseFiles(imprintTplFile, navTplFile, footerTplFile) 1118 if err != nil { 1119 t.Logger.InternalLogger.Fatal(err) 1120 os.Exit(1) 1121 } 1122 validationLandingTplFile := cfg.Ini.GetFilename("directory", "validation_landing", "web/templates/validation_landing.html", t.Cfg.Datahome) 1123 t.ValidationTpl, err = template.ParseFiles(validationLandingTplFile, navTplFile, footerTplFile) 1124 if err != nil { 1125 t.Logger.InternalLogger.Fatal(err) 1126 os.Exit(1) 1127 } 1128 landingTplFile := cfg.Ini.GetFilename("directory", "landing_page", "web/templates/landing.html", t.Cfg.Datahome) 1129 t.LandingPageTpl, err = template.ParseFiles(landingTplFile, navTplFile, footerTplFile) 1130 if err != nil { 1131 t.Logger.InternalLogger.Fatal(err) 1132 os.Exit(1) 1133 } 1134 lookupResultTplFile := cfg.Ini.GetFilename("directory", "lookup_result_page", "web/templates/lookup_result.html", t.Cfg.Datahome) 1135 t.LookupResultPageTpl, err = template.ParseFiles(lookupResultTplFile, navTplFile, footerTplFile) 1136 if err != nil { 1137 t.Logger.InternalLogger.Fatal(err) 1138 os.Exit(1) 1139 } 1140 aboutTplFile := cfg.Ini.GetFilename("directory", "about_page", "web/templates/about.html", t.Cfg.Datahome) 1141 t.AboutPageTpl, err = template.ParseFiles(aboutTplFile, navTplFile, footerTplFile) 1142 if err != nil { 1143 t.Logger.InternalLogger.Fatal(err) 1144 os.Exit(1) 1145 } 1146 t.Salt = os.Getenv("TALDIR_SALT") 1147 if t.Salt == "" { 1148 t.Salt = cfg.Ini.GetString("directory", "salt", "ChangeMe") 1149 } 1150 t.Host = cfg.Ini.GetString("directory", "base_url", "http://localhost") 1151 t.Merchant = cfg.Merchant 1152 merchConfig, err := t.Merchant.GetConfig() 1153 if err != nil { 1154 t.Logger.InternalLogger.Fatal(err) 1155 os.Exit(1) 1156 } 1157 currencySpec, currencySupported := merchConfig.Currencies[t.MonthlyFee.Currency] 1158 for !currencySupported { 1159 t.Logger.InternalLogger.Fatalf("Currency `%s' not supported by merchant!\n", t.MonthlyFee.Currency) 1160 os.Exit(1) 1161 } 1162 t.CurrencySpec = currencySpec 1163 t.setupHandlers() 1164 t.disseminateEntries() 1165 }