Artifact 514aaf93000a28ad07fb14bbd83816c2144b0f484ad1b5eaf12f173c95b4eed9:
- File
repo_handle_cookie_test.go
— part of check-in
[9d35c01fb3]
at
2026-10-13 00:12:32
on branch main
— fix(sync): log in through the x-f-l-c cookie and accept cleartext passwords (#304) (#362)
fossil's client moves its login card from the body into the x-f-l-c cookie once the server's server-version passes xfer_xflc_check (2.27, 2025-07-27 11:05 or later), and fossil's check_login, when a stored password is not 40 characters, tries the signature again against the shared secret that cleartext password derives (sections 6.4, 6.5). The client always logged in in the body, and the server checked only the stored value.
A request marks itself LoginCookieOK once the gate passes; Message.SplitLoginCookie moves its login card out of the body, and the HTTP transports (sync.HTTPTransport and the public NewHTTPTransport through the adapter) send it as the cookie, on every redirect too. The nonce is unchanged: it already covers what follows the login line, which in cookie form is the whole body. Transports that cannot set a cookie send the login in the body as before. The server side of the cookie landed with the server-version exchange.
HandleOpts gains LoginCookie, so an embedder serving HandleSyncWithOpts over its own HTTP handler can pass the cookie through; XferHandler and ServeHTTP read it themselves. (user: [email protected], size: 2677) [annotate] [blame] [check-ins using]
package libfossil_test import ( "context" "io" "net/http" "net/http/httptest" "path/filepath" "strings" "sync/atomic" "testing" libfossil "github.com/danmestas/go-libfossil" ) // TestHandleSyncWithOptsLoginCookie pins HandleOpts.LoginCookie: an // embedder's own HTTP handler that forwards the x-f-l-c cookie lets a client // that moved its login there after the first round's server-version (ยง6.5) // push as itself, where the round would otherwise arrive as nobody. func TestHandleSyncWithOptsLoginCookie(t *testing.T) { const pc = "77dc1a9065e8b8ff0d35abe4e26498cdf9576d6a" dir := t.TempDir() server, err := libfossil.Create(filepath.Join(dir, "server.fossil"), libfossil.CreateOpts{User: "admin", ProjectCode: pc}) if err != nil { t.Fatalf("Create server: %v", err) } defer server.Close() alice := libfossil.UserOpts{Login: "alice", Password: "pw", Caps: "oi"} if err := server.CreateUser(alice); err != nil { t.Fatalf("CreateUser: %v", err) } if err := server.SetCaps("nobody", ""); err != nil { t.Fatalf("SetCaps: %v", err) } var cookieRounds atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var opts libfossil.HandleOpts if c, err := r.Cookie("x-f-l-c"); err == nil { opts.LoginCookie = c.Value cookieRounds.Add(1) } resp, err := server.HandleSyncWithOpts(r.Context(), body, opts) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-fossil") _, _ = w.Write(resp) })) defer srv.Close() client, err := libfossil.Create(filepath.Join(dir, "client.fossil"), libfossil.CreateOpts{User: "alice", ProjectCode: pc}) if err != nil { t.Fatalf("Create client: %v", err) } defer client.Close() _, uuid, err := client.Commit(libfossil.CommitOpts{ Files: []libfossil.FileToCommit{ {Name: "a.txt", Content: []byte("pushed as alice\n")}, }, Comment: "alice's change", User: "alice", }) if err != nil { t.Fatalf("Commit: %v", err) } res, err := client.Sync(context.Background(), libfossil.NewHTTPTransport(srv.URL), libfossil.SyncOpts{Push: true, User: "alice", Password: "pw"}) if err != nil { t.Fatalf("Sync: %v", err) } if len(res.Errors) != 0 { t.Fatalf("sync errors: %v", strings.Join(res.Errors, "; ")) } if cookieRounds.Load() == 0 { t.Fatal("no round logged in through the cookie") } if _, err := server.ResolveVersion(uuid); err != nil { t.Fatalf("server lacks check-in %s after the push: %v", uuid, err) } }