Artifact be183cc0cb4af35235e37ca42fdda2095e595435e697c1ccb5844c31ed6f0858:
- File
internal/sync/redirect.go
— part of check-in
[0650171d55]
at
2026-10-23 21:25:20
on branch main
— feat(wasm): build the whole public API with TinyGo
go-libfossil compiled for js/wasm only with the standard toolchain, whose output is about 16 MB, too large to ship with a page or an edge function. Three standard-library features TinyGo lacks kept it from building there: http.ErrUseLastResponse and net.DNSError in the sync client, and os.SameFile in checkout-marker recovery.
Each now sits behind a tinygo build tag. Under TinyGo a sync redirect ends the request with an error instead of being followed, DNS failures are not classified as transient, and two names of one checkout marker are reported as ambiguous rather than paired; the standard build is unchanged.
examples/tinygo exercises the whole public API, sync and signing included. Built with TinyGo 0.43 and wasm-opt -Oz it is 3.83 MB raw and 1.40 MB gzipped, against 16.1 MB and 4.06 MB from the standard toolchain. A new CI job builds it with TinyGo on every change. (user: [email protected], size: 3708) [annotate] [blame] [check-ins using] [more...]
package sync import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/url" "strings" "github.com/danmestas/go-libfossil/internal/xfer" ) // maxRedirects is fossil's MAX_REDIRECTS (src/xfer.c). http_exchange // decrements it on each redirect and fails when it reaches zero, so 19 // redirects are followed and the twentieth is fatal (draft §4). const maxRedirects = 20 // ErrRedirectDowngrade reports a redirect from HTTPS to plain HTTP. Following // it would resend the request, login card included, in cleartext. var ErrRedirectDowngrade = errors.New("sync: cannot redirect from HTTPS to HTTP") // ErrRedirectLimit reports a twentieth redirect. var ErrRedirectLimit = errors.New("sync: redirect limit exceeded") // PostXfer POSTs body to target and follows redirects as fossil's // http_exchange does (src/http.c), not as net/http does. net/http turns a 301 // or 302 into a GET without the body, follows a downgrade from HTTPS to HTTP, // and stops after 10. Fossil re-POSTs the same body for 301, 302, 307 and // 308, strips a trailing /xfer from the new location, refuses a downgrade, // and fails on the twentieth redirect. // // The client's own redirect policy is replaced for these requests only, so // a caller's client is used as given but cannot loosen these rules. A redirect // without a Location is returned as is, for the caller to report its status. // // loginCookie, when not empty, goes in the x-f-l-c cookie of every request, // as fossil rebuilds a cookie login for each redirect target (§4, §6.5). func PostXfer(ctx context.Context, client *http.Client, target, contentType, loginCookie string, body []byte) (*http.Response, error) { if client == nil { panic("sync.PostXfer: client must not be nil") } c := *client c.CheckRedirect = func(*http.Request, []*http.Request) error { return errStopRedirect } for left := maxRedirects; ; { req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", contentType) if loginCookie != "" { req.Header.Set("Cookie", xfer.LoginCookieName+"="+loginCookie) } resp, err := c.Do(req) if err != nil { return nil, err } loc := resp.Header.Get("Location") if !isFollowedRedirect(resp.StatusCode) || loc == "" { return resp, nil } // Drain a little so the connection can be reused, then let it go. io.Copy(io.Discard, io.LimitReader(resp.Body, 4096)) resp.Body.Close() left-- if left == 0 { return nil, ErrRedirectLimit } if target, err = redirectTarget(target, loc); err != nil { return nil, err } } } func isFollowedRedirect(code int) bool { switch code { case http.StatusMovedPermanently, http.StatusFound, http.StatusTemporaryRedirect, http.StatusPermanentRedirect: return true } return false } // redirectTarget resolves loc against from and applies fossil's rules to it. // Fossil strips one trailing /xfer, though draft §4 says "repeatedly": its // loop truncates the string at the match, after which the substring it // compares is four characters long and can never equal "/xfer" again. func redirectTarget(from, loc string) (string, error) { base, err := url.Parse(from) if err != nil { return "", fmt.Errorf("sync: redirect from %q: %w", from, err) } next, err := base.Parse(loc) if err != nil { return "", fmt.Errorf("sync: malformed redirect %q: %w", loc, err) } if next.Scheme != "http" && next.Scheme != "https" { return "", fmt.Errorf("sync: cannot redirect to %q", loc) } if base.Scheme == "https" && next.Scheme == "http" { return "", ErrRedirectDowngrade } return strings.TrimSuffix(next.String(), "/xfer"), nil }