taldir

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

commit a1a641455a0ded785a7337c183e3ef28dc3da76d
parent f7a7b27a5e8b0ef8ce996a80c0fddb7de527abca
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Mon, 11 Jul 2022 18:29:44 +0200

fix rate limiting logic

Diffstat:
Mcmd/taldir-server/main_test.go | 34++++++++++++++++++++++++++++++++++
Mpkg/taldir/taldir.go | 116++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mtaldir.conf | 4+++-
3 files changed, 108 insertions(+), 46 deletions(-)

diff --git a/cmd/taldir-server/main_test.go b/cmd/taldir-server/main_test.go @@ -181,6 +181,40 @@ func TestReRegisterRequestTooMany(s *testing.T) { } +func TestSolutionRequestTooMany(s *testing.T) { + t.ClearDatabase() + + req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response := executeRequest(req) + + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + h_addr := getHAddress("abc@test") + solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946A90", "wrongSolution") + solutionJSON := "{\"solution\": \"" + solution + "\"}" + req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusForbidden != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) + } + req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusForbidden != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) + } + req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusForbidden != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) + } + req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusTooManyRequests != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusTooManyRequests, response.Code) + } + +} func TestRegisterRequestWrongPubkey(s *testing.T) { t.ClearDatabase() diff --git a/pkg/taldir/taldir.go b/pkg/taldir/taldir.go @@ -78,8 +78,15 @@ type Taldir struct { // Code TTL CodeTtl time.Duration - // Code retries max - CodeRetryMax int + // How often may a challenge be requested + ValidationInitiationMax int + + // How often may a solution be attempted (in the given timeframe) + SolutionAttemptsMax int + + // The timeframe for the above solution attempts + SolutionTimeframe time.Duration + // Code length in bytes before encoding CodeBytes int @@ -161,11 +168,11 @@ type Entry struct { // Public key of the user to register in base32 PublicKey string `json:"public_key"` - // Time of (re)registration. In Unix epoch microseconds) + // Time of (re)registration. RegisteredAt int64 `json:"-"` // How long the registration lasts in microseconds - Duration int64 `json:"-"` + Duration time.Duration `json:"-"` } // A validation is created when a registration for an entry is initiated. @@ -196,7 +203,13 @@ type Validation struct { TimeframeStart time.Time // How often was this validation re-initiated - RetryCount int + InitiationCount int + + // How often was a solution for this validation tried + SolutionAttemptCount int + + // The beginning of the last solution timeframe + LastSolutionTimeframeStart time.Time } type ErrorDetail struct { @@ -241,13 +254,6 @@ type ValidationConfirmation struct { Solution string `json:"solution"` } -// matcher is a language.Matcher configured for all supported languages. -var langMatcher = language.NewMatcher([]language.Tag{ - language.BritishEnglish, - //language.Norwegian, - language.German, -}) - // Primary lookup function. // Allows the caller to query a wallet key using the hash(!) of the // identity, e.g. SHA512(<email address>) @@ -300,21 +306,40 @@ func (t *Taldir) validationRequest(w http.ResponseWriter, r *http.Request){ w.WriteHeader(http.StatusNotFound) return } + validation.SolutionAttemptCount++ + if validation.LastSolutionTimeframeStart.Add(t.SolutionTimeframe).After(time.Now()) { + if validation.SolutionAttemptCount > t.SolutionAttemptsMax { + w.WriteHeader(429) + rlResponse := RateLimitedResponse{ + Code: gana.TALDIR_REGISTER_RATE_LIMITED, + RequestFrequency: t.RequestFrequency, + Hint: "Solution attempt rate limit reached", + } + jsonResp, _ := json.Marshal(rlResponse) + w.Write(jsonResp) + return + } + } else { + log.Println("New solution timeframe set.") + validation.LastSolutionTimeframeStart = time.Now() + validation.SolutionAttemptCount = 1 + } + t.Db.Save(&validation) expectedSolution := util.GenerateSolution(validation.PublicKey, validation.Code) if confirm.Solution != expectedSolution { - // FIXME how TF do we rate limit here?? w.WriteHeader(http.StatusForbidden) return } // FIXME: Expire validations somewhere? err = t.Db.Delete(&validation).Error if err != nil { + log.Fatalf("Error deleting validation") w.WriteHeader(http.StatusInternalServerError) return } entry.HsAddress = saltHAddress(validation.HAddress, t.Salt) entry.Inbox = validation.Inbox - entry.Duration = validation.Duration + entry.Duration = time.Duration(validation.Duration) entry.RegisteredAt = time.Now().UnixMicro() entry.PublicKey = validation.PublicKey err = t.Db.First(&entry, "hs_address = ?", entry.HsAddress).Error @@ -350,6 +375,7 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ w.Write(resp) return } + // Check if this validation method is supported or not. if !t.Validators[vars["method"]] { errDetail.Code = gana.TALDIR_METHOD_NOT_SUPPORTED errDetail.Hint = "Unsupported method" @@ -359,47 +385,39 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ w.Write(resp) return } + + // Setup validation object. Retrieve object from DB if it already + // exists. h := sha512.New() h.Write([]byte(req.Address)) - validation.HAddress = util.EncodeBinaryToString(h.Sum(nil)) - // We first try if there is already an entry for this address which - // is still valid and the duration is not extended. + h_address := util.EncodeBinaryToString(h.Sum(nil)) + validation.HAddress = h_address hs_address := saltHAddress(validation.HAddress, t.Salt) err = t.Db.First(&entry, "hs_address = ?", hs_address).Error if err == nil { log.Println("Entry for this address already exists..") - lastRegValidity := entry.RegisteredAt + entry.Duration - requestedValidity := time.Now().UnixMicro() + req.Duration - earliestReRegistration := entry.RegisteredAt + t.RequestFrequency - // Rate limit re-registrations. - if time.Now().UnixMicro() < earliestReRegistration { - w.WriteHeader(429) - rlResponse := RateLimitedResponse{ - Code: gana.TALDIR_REGISTER_RATE_LIMITED, - RequestFrequency: t.RequestFrequency, - Hint: "Registration rate limit reached", - } - jsonResp, _ := json.Marshal(rlResponse) - w.Write(jsonResp) - return - } - // Do not allow re-registrations with shorter duration. - if requestedValidity <= lastRegValidity { - w.WriteHeader(200) - // FIXME how to return how long it is already paid for?? + regAt := time.UnixMicro(entry.RegisteredAt) + entryValidity := regAt.Add(entry.Duration) + log.Printf("Entry valid until: %s , requested until: %s\n", entryValidity, time.Now().Add(time.Duration(req.Duration))) + if time.Now().Add(time.Duration(req.Duration)).Before(entryValidity) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"valid_until\": " + entryValidity.String() + "}")) return } } - err = t.Db.First(&validation, "h_address = ?", validation.HAddress).Error + err = t.Db.First(&validation, "h_address = ?", h_address).Error validation.Code = util.GenerateCode(t.CodeBytes) validation.Inbox = req.Inbox validation.Duration = req.Duration validation.PublicKey = req.PublicKey + validation.SolutionAttemptCount = 0 + validation.LastSolutionTimeframeStart = time.Now() if err == nil { - // FIXME: Validation already pending for this address - // How should we proceed here? Expire old validations? + // Limit re-initiation attempts + validation.InitiationCount++ if time.Now().Before(validation.TimeframeStart.Add(t.CodeTtl)) { - if validation.RetryCount >= t.CodeRetryMax { + if validation.InitiationCount > t.ValidationInitiationMax { w.WriteHeader(429) rlResponse := RateLimitedResponse{ Code: gana.TALDIR_REGISTER_RATE_LIMITED, @@ -411,14 +429,14 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ t.Db.Delete(&validation) return } - validation.RetryCount++ } else { log.Println("Validation stale, resetting retry counter") validation.TimeframeStart = time.Now() - validation.RetryCount = 0 + validation.InitiationCount = 1 } err = t.Db.Save(&validation).Error } else { + validation.InitiationCount = 1 validation.TimeframeStart = time.Now() err = t.Db.Create(&validation).Error } @@ -426,7 +444,7 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ w.WriteHeader(http.StatusInternalServerError) return } - fmt.Println("Address registration request created:", validation) + log.Println("Address registration request created:", validation) if !t.Cfg.Section("taldir-" + vars["method"]).HasKey("command") { log.Fatal(err) t.Db.Delete(&validation) @@ -449,7 +467,7 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ return } w.WriteHeader(202) - fmt.Printf("Output from method script %s is %s\n", path, out) + log.Printf("Output from method script %s is %s\n", path, out) } func notImplemented(w http.ResponseWriter, r *http.Request) { @@ -655,13 +673,21 @@ func (t *Taldir) Initialize(cfgfile string) { t.Validators[a] = true } t.CodeBytes = t.Cfg.Section("taldir").Key("activation_code_bytes").MustInt(16) - t.CodeRetryMax = t.Cfg.Section("taldir").Key("activation_retry_max").MustInt(2) + t.ValidationInitiationMax = t.Cfg.Section("taldir").Key("validation_initiation_max").MustInt(3) + t.SolutionAttemptsMax = t.Cfg.Section("taldir").Key("solution_attempt_max").MustInt(3) + validationTtlStr := t.Cfg.Section("taldir").Key("activation_code_ttl").MustString("5m") t.CodeTtl, err = time.ParseDuration(validationTtlStr) if err != nil { log.Fatal(err) } + retryTimeframeStr := t.Cfg.Section("taldir").Key("code_attempt_timeframe").MustString("1h") + t.SolutionTimeframe, err = time.ParseDuration(retryTimeframeStr) + if err != nil { + log.Fatal(err) + } + psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", t.Cfg.Section("taldir-pq").Key("host").MustString("localhost"), t.Cfg.Section("taldir-pq").Key("port").MustInt64(5432), diff --git a/taldir.conf b/taldir.conf @@ -11,8 +11,10 @@ default_doc_lang = en-US default_tos_path = terms/ default_pp_path = privacy/ activation_code_bytes = 16 -activation_retry_max = 2 +validation_initiation_max = 3 +solution_attempt_max = 3 activation_code_ttl = 10m +solution_attempt_timeframe = 1h [taldir-email] sender = "taldir@taler.net"