diff --git a/README.md b/README.md index 00273c4..5e9468c 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,9 @@ Additional arguments can be passed to `./SewerRat` to control its behavior (chec - `-session` specifies the lifetime of a registration sesssion (i.e., the maximum time between starting and finishing the registration, see below). This defaults to 10 minutes. +- `-checktime` specifies the time spent polling for the verification code after a request has been made to `/register/finish` or `/deregister/finish`. + A non-zero value is often necessary on network filesystems where newly written files do not immediately synchronize. + This defaults to 30 seconds. - `-prefix` adds an extra prefix to all endpoints, e.g., to disambiguate between versions. For example, a prefix of `api/v2` would change the list endpoint to `/api/v2/list`. This defaults to an empty string, i.e., no prefix. diff --git a/handlers.go b/handlers.go index 83bf876..b039a94 100644 --- a/handlers.go +++ b/handlers.go @@ -14,6 +14,8 @@ import ( "net/url" "mime" + "time" + "encoding/json" "errors" "strings" @@ -69,7 +71,7 @@ func dumpHttpErrorResponse(w http.ResponseWriter, err error) { dumpErrorResponse(w, status_code, err.Error()) } -func checkVerificationCode(path string, verifier *verificationRegistry) (fs.FileInfo, error) { +func checkVerificationCode(path string, verifier *verificationRegistry, timeout time.Duration) (fs.FileInfo, error) { expected_code, ok := verifier.Pop(path) if !ok { return nil, newHttpError(http.StatusBadRequest, fmt.Errorf("no verification code available for %q", path)) @@ -80,10 +82,29 @@ func checkVerificationCode(path string, verifier *verificationRegistry) (fs.File // that person when registering a directory. expected_path := filepath.Join(path, expected_code) code_info, err := os.Lstat(expected_path) - if errors.Is(err, os.ErrNotExist) { - return nil, newHttpError(http.StatusUnauthorized, fmt.Errorf("verification failed for %q; %w", path, err)) - } else if err != nil { - return nil, fmt.Errorf("failed to inspect verification code for %q; %w", path, err) + + // Exponential back-off up to the time limit. + until := time.Now().Add(timeout) + sleep := time.Duration(1) + for err != nil { + if !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("failed to inspect verification code for %q; %w", path, err) + } + + remaining := until.Sub(time.Now()) + if remaining <= 0 { + return nil, newHttpError(http.StatusUnauthorized, fmt.Errorf("verification failed for %q; %w", path, err)) + } + + if sleep > remaining { + sleep = remaining + } + time.Sleep(sleep) + if sleep < 32 { + sleep *= 2 + } + + code_info, err = os.Lstat(expected_path) } // Similarly, prohibit hard links to avoid spoofing identities. Admittedly, @@ -150,7 +171,7 @@ func newRegisterStartHandler(verifier *verificationRegistry) func(http.ResponseW } } -func newRegisterFinishHandler(db *sql.DB, verifier *verificationRegistry, tokenizer *unicodeTokenizer) func(http.ResponseWriter, *http.Request) { +func newRegisterFinishHandler(db *sql.DB, verifier *verificationRegistry, tokenizer *unicodeTokenizer, timeout time.Duration) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if r.Body == nil { dumpErrorResponse(w, http.StatusBadRequest, "expected a non-empty request body") @@ -196,7 +217,7 @@ func newRegisterFinishHandler(db *sql.DB, verifier *verificationRegistry, tokeni allowed = []string{ "metadata.json" } } - code_info, err := checkVerificationCode(regpath, verifier) + code_info, err := checkVerificationCode(regpath, verifier, timeout) if err != nil { dumpHttpErrorResponse(w, err) return @@ -264,7 +285,7 @@ func newDeregisterStartHandler(db *sql.DB, verifier *verificationRegistry) func( } } -func newDeregisterFinishHandler(db *sql.DB, verifier *verificationRegistry) func(http.ResponseWriter, *http.Request) { +func newDeregisterFinishHandler(db *sql.DB, verifier *verificationRegistry, timeout time.Duration) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if r.Body == nil { dumpErrorResponse(w, http.StatusBadRequest, "expected a non-empty request body") @@ -284,7 +305,7 @@ func newDeregisterFinishHandler(db *sql.DB, verifier *verificationRegistry) func return } - _, err = checkVerificationCode(regpath, verifier) + _, err = checkVerificationCode(regpath, verifier, timeout) if err != nil { dumpHttpErrorResponse(w, err) return diff --git a/handlers_test.go b/handlers_test.go index 35f97ca..579649a 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -85,7 +85,7 @@ func TestCheckVerificationCode(t *testing.T) { t.Fatal(err) } - info, err := checkVerificationCode(target, v) + info, err := checkVerificationCode(target, v, 1) if err != nil { t.Fatal(err) } @@ -101,7 +101,7 @@ func TestCheckVerificationCode(t *testing.T) { t.Fatal(err) } - _, err = checkVerificationCode(target, v) + _, err = checkVerificationCode(target, v, 1) if err == nil || !strings.Contains(err.Error(), "no verification code") { t.Fatal("should have failed") } @@ -118,7 +118,7 @@ func TestCheckVerificationCode(t *testing.T) { t.Fatal(err) } - _, err = checkVerificationCode(target, v) + _, err = checkVerificationCode(target, v, 1) if err == nil || !strings.Contains(err.Error(), "verification failed") { t.Fatal("should have failed") } @@ -145,7 +145,7 @@ func TestCheckVerificationCode(t *testing.T) { t.Fatal(err) } - _, err = checkVerificationCode(target, v) + _, err = checkVerificationCode(target, v, 1) if err == nil || !strings.Contains(err.Error(), "hard link") { t.Fatal("should have failed") } @@ -273,7 +273,7 @@ func TestRegisterHandlers(t *testing.T) { t.Run("register finish without verification", func(t *testing.T) { quickRegisterStart() - handler := http.HandlerFunc(newRegisterFinishHandler(dbconn, verifier, tokr)) + handler := http.HandlerFunc(newRegisterFinishHandler(dbconn, verifier, tokr, 1)) req := createJsonRequest("POST", "/register/finish", map[string]interface{}{ "path": to_add }, t) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) @@ -289,7 +289,7 @@ func TestRegisterHandlers(t *testing.T) { t.Fatal(err) } - handler := http.HandlerFunc(newRegisterFinishHandler(dbconn, verifier, tokr)) + handler := http.HandlerFunc(newRegisterFinishHandler(dbconn, verifier, tokr, 1)) req := createJsonRequest("POST", "/register/finish", map[string]interface{}{ "path": to_add }, t) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) @@ -321,7 +321,7 @@ func TestRegisterHandlers(t *testing.T) { t.Run("register finish with empty names", func(t *testing.T) { quickRegisterStart() - handler := http.HandlerFunc(newRegisterFinishHandler(dbconn, verifier, tokr)) + handler := http.HandlerFunc(newRegisterFinishHandler(dbconn, verifier, tokr, 1)) req := createJsonRequest("POST", "/register/finish", map[string]interface{}{ "path": to_add, "base": []string{ "", "metadata.json" } }, t) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) @@ -337,7 +337,7 @@ func TestRegisterHandlers(t *testing.T) { t.Fatal(err) } - handler := http.HandlerFunc(newRegisterFinishHandler(dbconn, verifier, tokr)) + handler := http.HandlerFunc(newRegisterFinishHandler(dbconn, verifier, tokr, 1)) req := createJsonRequest("POST", "/register/finish", map[string]interface{}{ "path": to_add, "base": []string{ "metadata.json", "other.json" } }, t) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) @@ -450,7 +450,7 @@ func TestDeregisterHandlers(t *testing.T) { t.Run("deregister fail", func(t *testing.T) { quickDeregisterStart() - handler := http.HandlerFunc(newDeregisterFinishHandler(dbconn, verifier)) + handler := http.HandlerFunc(newDeregisterFinishHandler(dbconn, verifier, 1)) // First attempt fails, because we didn't add the registration code. req := createJsonRequest("POST", "/deregister/finish", map[string]interface{}{ "path": to_add }, t) @@ -472,7 +472,7 @@ func TestDeregisterHandlers(t *testing.T) { t.Fatal(err) } - handler := http.HandlerFunc(newDeregisterFinishHandler(dbconn, verifier)) + handler := http.HandlerFunc(newDeregisterFinishHandler(dbconn, verifier, 1)) req := createJsonRequest("POST", "/deregister/finish", map[string]interface{}{ "path": to_add }, t) if err != nil { t.Fatal(err) diff --git a/main.go b/main.go index fc72826..2b264b4 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ func main() { port0 := flag.Int("port", 8080, "Port to listen to for requests") backup0 := flag.Int("backup", 24, "Frequency of back-ups, in hours") update0 := flag.Int("update", 24, "Frequency of updates, in hours") + timeout0 := flag.Int("checktime", 30, "Time spent polling for the verification code, in seconds") prefix0 := flag.String("prefix", "", "Prefix to add to each endpoint, excluding the first and last slashes (default \"\")") lifetime0 := flag.Int("session", 10, "Session lifetime, in minutes") flag.Parse() @@ -43,11 +44,13 @@ func main() { prefix = "/" + prefix } + timeout := time.Duration(*timeout0) + // Setting up the endpoints. http.HandleFunc("POST " + prefix + "/register/start", newRegisterStartHandler(verifier)) - http.HandleFunc("POST " + prefix + "/register/finish", newRegisterFinishHandler(db, verifier, tokenizer)) + http.HandleFunc("POST " + prefix + "/register/finish", newRegisterFinishHandler(db, verifier, tokenizer, timeout)) http.HandleFunc("POST " + prefix + "/deregister/start", newDeregisterStartHandler(db, verifier)) - http.HandleFunc("POST " + prefix + "/deregister/finish", newDeregisterFinishHandler(db, verifier)) + http.HandleFunc("POST " + prefix + "/deregister/finish", newDeregisterFinishHandler(db, verifier, timeout)) http.HandleFunc(prefix + "/query", newQueryHandler(db, tokenizer, wild_tokenizer, "/query")) http.HandleFunc(prefix + "/retrieve/metadata", newRetrieveMetadataHandler(db))