taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

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 }