Artifact 6bef3238ab9368e7119a89203d8532ff054936c00ce24a1805e94c49e73b3996:
- File
internal/xfer/login_cookie.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: 2554) [annotate] [blame] [check-ins using]
package xfer import ( "net/url" "strings" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/hash" ) // LoginCookieName is the HTTP cookie in which a client may carry its login // card instead of the body, once the server's release accepts it (§6.5). const LoginCookieName = "x-f-l-c" // DecodeRequest decodes a sync request as Decode does. When loginCookie, the // value of the request's LoginCookieName cookie, holds a login card, that card // is put first, its nonce bound to the whole decompressed body: fossil's // page_xfer handles a cookie login as a login line ahead of the body, so the // "rest of the message" its nonce covers is all of it (§6.5, check_tail_hash). // A cookie that is not a login card is ignored, as page_xfer ignores it. func DecodeRequest(data []byte, contentType, loginCookie string) (*Message, error) { raw := data if len(data) > 0 && contentTypeIsCompressed(contentType) { var err error if raw, err = decompressContainer(data); err != nil { return nil, err } } msg, err := DecodeUncompressed(raw) if err != nil { return nil, err } if lc := parseLoginCookie(loginCookie); lc != nil { lc.tailSHA1 = hash.SHA1(raw) msg.Cards = append([]Card{lc}, msg.Cards...) } return msg, nil } // SplitLoginCookie returns the message to send as the body and the // LoginCookieName cookie value to send beside it. When m is LoginCookieOK and // opens with a login card, that card leaves the body for the cookie. Its // nonce, computed over the cards after it, then covers the whole body, as a // cookie login's must (§6.5). Otherwise m goes as it is, with no cookie. func (m *Message) SplitLoginCookie() (*Message, string) { if !m.LoginCookieOK || len(m.Cards) == 0 { return m, "" } lc, ok := m.Cards[0].(*LoginCard) if !ok { return m, "" } line := "login " + deck.FossilEncode(lc.User) + " " + lc.Nonce + " " + lc.Signature return &Message{Cards: m.Cards[1:]}, url.QueryEscape(line) } // parseLoginCookie reads a login card from a cookie value: URL-encoded, as // fossil's %T writes it, then "login USER NONCE SIGNATURE" split on white // space, as page_xfer tokenizes it. It returns nil for anything else. func parseLoginCookie(v string) *LoginCard { if v == "" { return nil } line, err := url.QueryUnescape(v) if err != nil { return nil } tokens := strings.Fields(line) if len(tokens) != 4 || tokens[0] != "login" { return nil } return &LoginCard{ User: deck.FossilDecode(tokens[1]), Nonce: tokens[2], Signature: tokens[3], } }