Skip to content

Commit

Permalink
Poll for the verification code with exponential backoff + timeout.
Browse files Browse the repository at this point in the history
This is necessary for some networked filesystems where the client's written
file may not be immediately visible to the SewerRat backend.
  • Loading branch information
LTLA committed Aug 7, 2024
1 parent 7111be4 commit ba6ce00
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 21 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 30 additions & 9 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"net/url"
"mime"

"time"

"encoding/json"
"errors"
"strings"
Expand Down Expand Up @@ -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))
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down
20 changes: 10 additions & 10 deletions handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit ba6ce00

Please sign in to comment.