Artifact d08d3a1a47ee5e57b7bcd3e730a60aed1b71adcb09bcd3ccd5bf3102003b24bf:
- File
internal/xfer/login_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: 2591) [annotate] [blame] [check-ins using]
package xfer import ( "testing" "github.com/danmestas/go-libfossil/internal/hash" ) // TestDecodeRequestLoginCookie pins ยง6.5: a login card carried in the // x-f-l-c cookie is put first, with its nonce bound to the whole body, and a // cookie that is not a login card is ignored. func TestDecodeRequestLoginCookie(t *testing.T) { body := []byte("pull sc pc\n# trailing comment\n") nonce := hash.SHA1(body) msg, err := DecodeRequest(body, ContentTypeUncompressed, "login+alice+"+nonce+"+sig") if err != nil { t.Fatalf("DecodeRequest: %v", err) } lc, ok := msg.Cards[0].(*LoginCard) if !ok { t.Fatalf("first card is %T, want the cookie's login card", msg.Cards[0]) } if lc.User != "alice" || lc.Signature != "sig" || !lc.NonceCoversTail() { t.Fatalf("cookie login = %+v, covers body %v", lc, lc.NonceCoversTail()) } notLogins := []string{"", "alice+" + nonce + "+sig", "login+alice+" + nonce, "%zz"} for _, cookie := range notLogins { msg, err := DecodeRequest(body, ContentTypeUncompressed, cookie) if err != nil { t.Fatalf("DecodeRequest(cookie %q): %v", cookie, err) } if _, ok := msg.Cards[0].(*LoginCard); ok { t.Errorf("cookie %q was taken as a login card", cookie) } } } // TestSplitLoginCookieRoundTrip pins that a login moved into the cookie is // read back by DecodeRequest with its nonce covering the body it left, and // that a message not marked LoginCookieOK keeps its login in the body. func TestSplitLoginCookieRoundTrip(t *testing.T) { rest := []Card{&PullCard{ServerCode: "sc", ProjectCode: "pc"}, &CommentCard{Text: "ABC"}} tail, err := (&Message{Cards: rest}).EncodeUncompressed() if err != nil { t.Fatalf("encode: %v", err) } login := &LoginCard{User: "a b", Nonce: hash.SHA1(tail), Signature: "sig"} m := &Message{Cards: append([]Card{login}, rest...), LoginCookieOK: true} sent, cookie := m.SplitLoginCookie() if cookie == "" || len(sent.Cards) != len(rest) { t.Fatalf("split = %d cards, cookie %q; want the login moved out", len(sent.Cards), cookie) } body, err := sent.EncodeUncompressed() if err != nil { t.Fatalf("encode body: %v", err) } got, err := DecodeRequest(body, ContentTypeUncompressed, cookie) if err != nil { t.Fatalf("DecodeRequest: %v", err) } lc, ok := got.Cards[0].(*LoginCard) if !ok || lc.User != "a b" || !lc.NonceCoversTail() { t.Fatalf("decoded login = %+v; want user %q covering the body", got.Cards[0], "a b") } m.LoginCookieOK = false if sent, cookie := m.SplitLoginCookie(); cookie != "" || sent != m { t.Fatalf("unmarked message split: cookie %q", cookie) } }