taldir

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

commit eb21817b69582e5774a07e2ae306f76b85aefba7
parent 89a3161bfb0ae1780e47cf3080c29bfad9e1d218
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Tue, 12 Jul 2022 12:43:26 +0200

improve handling of registrations

Diffstat:
Mcmd/taldir-server/main_test.go | 6+++---
Mpkg/rest/taldir.go | 160++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
2 files changed, 113 insertions(+), 53 deletions(-)

diff --git a/cmd/taldir-server/main_test.go b/cmd/taldir-server/main_test.go @@ -47,12 +47,12 @@ var validRegisterRequest = []byte(` } `) -var validRegisterRequestShort = []byte(` +var validRegisterRequestUnmodified = []byte(` { "address": "abc@test", "public_key": "000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946A90", "inbox_url": "myinbox@xyz", - "duration": 23328000000000 + "duration": 0 } `) @@ -194,7 +194,7 @@ func TestReRegisterRequest(s *testing.T) { if http.StatusNoContent != response.Code { s.Errorf("Expected response code %d. Got %d\n", http.StatusNoContent, response.Code) } - req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequestShort)) + req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequestUnmodified)) response = executeRequest(req) if http.StatusOK != response.Code { diff --git a/pkg/rest/taldir.go b/pkg/rest/taldir.go @@ -203,17 +203,28 @@ type Validation struct { // Public key of the user to register PublicKey string `json:"public_key"` - // When does this validation timeframe begin (for retry calculation) - TimeframeStart time.Time - - // How often was this validation re-initiated - InitiationCount int - // How often was a solution for this validation tried SolutionAttemptCount int // The beginning of the last solution timeframe LastSolutionTimeframeStart time.Time + + // The order ID associated with this validation + OrderId string `json:"-"` +} + +type ValidationMetadata struct { + // ORM + gorm.Model `json:"-"` + + // The hash (SHA512) of the address + HAddress string `json:"h_address"` + + // When does this validation timeframe begin (for retry calculation) + TimeframeStart time.Time + + // How often was this validation re-initiated for this address + InitiationCount int } type ErrorDetail struct { @@ -365,6 +376,34 @@ func (t *Taldir) validationRequest(w http.ResponseWriter, r *http.Request){ w.WriteHeader(http.StatusNoContent) } +func (t *Taldir) IsRateLimited(h_address string) (bool, error) { + var validationMetadata ValidationMetadata + err := t.Db.First(&validationMetadata, "h_address = ?", h_address).Error + // NOTE: Check rate limit + if err == nil { + // Limit re-initiation attempts + validationMetadata.InitiationCount++ + if time.Now().Before(validationMetadata.TimeframeStart.Add(t.ValidationTimeframe)) { + if validationMetadata.InitiationCount > t.ValidationInitiationMax { + return true, nil + } + } else { + log.Println("Validation stale, resetting retry counter") + validationMetadata.TimeframeStart = time.Now() + validationMetadata.InitiationCount = 1 + } + err = t.Db.Save(&validationMetadata).Error + } else { + validationMetadata.HAddress = h_address + validationMetadata.InitiationCount = 1 + validationMetadata.TimeframeStart = time.Now() + err = t.Db.Create(&validationMetadata).Error + } + if err != nil { + return false, err + } + return false, nil +} func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ vars := mux.Vars(r) @@ -409,59 +448,54 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ // Round to the nearest multiple of a month reqDuration := time.Duration(req.Duration * 1000) reqDuration = reqDuration.Round(MONTH_DURATION) - validation.Duration = reqDuration.Microseconds() if err == nil { + // Check if this entry is to be modified or extended + entryModified := (req.Inbox != entry.Inbox) || + (req.PublicKey != entry.PublicKey) log.Println("Entry for this address already exists..") regAt := time.UnixMicro(entry.RegisteredAt) entryValidity := regAt.Add(entry.Duration) - requestedValidity := time.Now().Add(reqDuration) - log.Printf("Entry valid until: %s , requested until: %s\n", entryValidity, time.Now().Add(reqDuration)) + requestedValidity := entryValidity.Add(reqDuration) + log.Printf("Entry valid until: %s , requested until: %s\n", entryValidity, requestedValidity) // NOTE: The extension must be at least one month - if requestedValidity.Before(entryValidity.Add(MONTH_DURATION)) { + if (reqDuration.Microseconds() == 0) && !entryModified { + // Nothing changed. Return validity w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") w.Write([]byte("{\"valid_until\": " + entryValidity.String() + "}")) return } else { - validation.Duration = entryValidity.Sub(requestedValidity).Round(MONTH_DURATION).Microseconds() + // Entry modified or duration extension requested } } - err = t.Db.First(&validation, "h_address = ?", h_address).Error - validation.Challenge = util.GenerateChallenge(t.ChallengeBytes) - validation.Inbox = req.Inbox - validation.PublicKey = req.PublicKey - validation.SolutionAttemptCount = 0 - validation.LastSolutionTimeframeStart = time.Now() - if err == nil { - // Limit re-initiation attempts - validation.InitiationCount++ - if time.Now().Before(validation.TimeframeStart.Add(t.ValidationTimeframe)) { - if validation.InitiationCount > t.ValidationInitiationMax { - w.WriteHeader(429) - rlResponse := RateLimitedResponse{ - Code: gana.TALDIR_REGISTER_RATE_LIMITED, - RequestFrequency: t.ValidationTimeframe.Microseconds() / int64(t.ValidationInitiationMax), - Hint: "Registration rate limit reached", - } - jsonResp, _ := json.Marshal(rlResponse) - w.Write(jsonResp) - t.Db.Delete(&validation) - return - } - } else { - log.Println("Validation stale, resetting retry counter") - validation.TimeframeStart = time.Now() - validation.InitiationCount = 1 - } - err = t.Db.Save(&validation).Error - } else { - validation.InitiationCount = 1 - validation.TimeframeStart = time.Now() - err = t.Db.Create(&validation).Error - } - if err != nil { + rateLimited, err := t.IsRateLimited(h_address) + if nil != err { + log.Printf("Error checking rate limit! %w", err) w.WriteHeader(http.StatusInternalServerError) return + } else if rateLimited { + w.WriteHeader(http.StatusTooManyRequests) + rlResponse := RateLimitedResponse{ + Code: gana.TALDIR_REGISTER_RATE_LIMITED, + RequestFrequency: t.ValidationTimeframe.Microseconds() / int64(t.ValidationInitiationMax), + Hint: "Registration rate limit reached", + } + jsonResp, _ := json.Marshal(rlResponse) + w.Write(jsonResp) + t.Db.Delete(&validation) + return + } + err = t.Db.First(&validation, "h_address = ? AND public_key = ? AND inbox = ? AND duration = ?", + h_address, req.PublicKey, req.Inbox, reqDuration).Error + validationExists := (nil == err) + // FIXME: Always set new challenge? + validation.Challenge = util.GenerateChallenge(t.ChallengeBytes) + if !validationExists { + validation.Inbox = req.Inbox + validation.PublicKey = req.PublicKey + validation.SolutionAttemptCount = 0 + validation.LastSolutionTimeframeStart = time.Now() + validation.Duration = reqDuration.Microseconds() } fixedCost := t.Cfg.Section("taldir-" + vars["method"]).Key("challenge_fee").MustString("KUDOS:0") @@ -477,31 +511,49 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ log.Printf("The calculated cost for this registration is: %s:%f for a delta duration of %f", currency, cost, float64(validation.Duration) / float64(MONTH_DURATION.Microseconds())) if cost > 0 { - // FIXME what if provided order ID and validation order ID differ??? - if len(order.Id) == 0 { + if validationExists { + if order.Id != validation.OrderId { + log.Fatalf("Order ID is not validation ID what to do?") + w.WriteHeader(http.StatusInternalServerError) + return + } + } + if len(validation.OrderId) == 0 { // Add new order for new validations orderId, newOrderErr := t.Merchant.AddNewOrder(cost, currency) if newOrderErr != nil { w.WriteHeader(http.StatusInternalServerError) return } - order.Id = orderId + validation.OrderId = orderId } + + // FIXME what if provided order ID and validation order ID differ??? // Check if order paid. FIXME: How to check if this the a correct order?? - payto, paytoErr := t.Merchant.IsOrderPaid(order.Id) + payto, paytoErr := t.Merchant.IsOrderPaid(validation.OrderId) if paytoErr != nil { w.WriteHeader(http.StatusInternalServerError) log.Println(paytoErr) return } if len(payto) != 0 { + t.Db.Save(&validation) w.WriteHeader(http.StatusPaymentRequired) w.Header().Set("Location", payto) // FIXME no idea what to do with this. return } // In this case, this order was paid } - + if validationExists { + err = t.Db.Save(&validation).Error + } else { + err = t.Db.Create(&validation).Error + } + if err != nil { + log.Println(err) + w.WriteHeader(500) + return + } log.Println("Address registration request created:", validation) if !t.Cfg.Section("taldir-" + vars["method"]).HasKey("command") { @@ -615,6 +667,7 @@ func (t *Taldir) DeleteEntry(addr string) error { func (t *Taldir) ClearDatabase() { t.Db.Where("1 = 1").Delete(&Entry{}) t.Db.Where("1 = 1").Delete(&Validation{}) + t.Db.Where("1 = 1").Delete(&ValidationMetadata{}) } func (t *Taldir) termsResponse(w http.ResponseWriter, r *http.Request) { @@ -776,7 +829,14 @@ func (t *Taldir) Initialize(cfgfile string) { if err := t.Db.AutoMigrate(&Validation{}); err != nil { panic(err) } + if err := t.Db.AutoMigrate(&ValidationMetadata{}); err != nil { + panic(err) + } + + // Clean up validations + tx := t.Db.Where("created_at < ?", time.Now().AddDate(0, 0, -1)).Delete(&Validation{}) + log.Printf("Cleaned up %d stale validations.\n", tx.RowsAffected) validationLandingTplFile := t.Cfg.Section("taldir").Key("validation_landing").MustString("templates/validation_landing.html") t.ValidationTpl, err = template.ParseFiles(validationLandingTplFile) if err != nil {