fix(download): avoid unsafe automatic range resume

This commit is contained in:
zarzet
2026-07-26 01:28:09 +07:00
parent aa0eecb27e
commit 40dd9bb68f
2 changed files with 51 additions and 8 deletions
+13 -7
View File
@@ -129,6 +129,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
var onProgress goja.Callable
var headers map[string]string
var chunkedDownload bool
var resumeDownload bool
trackItemBytes := true
var chunkSize int64
if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) && !goja.IsNull(call.Arguments[2]) {
@@ -170,6 +171,11 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
}
}
}
if resume, ok := opts["resume"]; ok {
if v, ok := resume.(bool); ok {
resumeDownload = v
}
}
}
}
@@ -328,14 +334,14 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
}
}
// Mid-body network failures resume from the current offset instead of
// failing the whole download. Only attempted when the server gave a
// validator (If-Range guards against splicing two versions of the file)
// and the caller did not set its own Range. A server that ignores Range
// answers 200 and the download restarts from zero — same as today.
// Mid-body resume is opt-in because switching networks can route a stable
// URL to a different CDN object even when its validator is unchanged. The
// safe default is to fail and delete the staged partial file. Extensions
// that know their origin supports byte-identical Range resumes can request
// it explicitly with { resume: true }.
validator := resumeValidator(resp.Header)
_, callerSetRange := headers["Range"]
canResume := validator != "" && !callerSetRange
canResume := resumeDownload && validator != "" && !callerSetRange
const maxResumes = 3
resumes := 0
@@ -401,7 +407,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
}
}
validator = resumeValidator(resp.Header)
canResume = validator != ""
canResume = resumeDownload && validator != ""
default:
code := resp.StatusCode
resp.Body.Close()
@@ -267,7 +267,42 @@ func TestFileDownloadFailureLeavesNoFinalFile(t *testing.T) {
}
}
func TestFileDownloadResumesAfterMidBodyCut(t *testing.T) {
func TestFileDownloadDoesNotResumeMidBodyCutByDefault(t *testing.T) {
const full = "hello-world!"
var attempts int
runtime := newFileDownloadTestRuntime(t, func(req *http.Request) (*http.Response, error) {
attempts++
h := make(http.Header)
h.Set("ETag", `"v1"`)
return &http.Response{
StatusCode: 200,
Header: h,
Body: io.NopCloser(&failingBodyReader{data: []byte(full[:6])}),
ContentLength: int64(len(full)),
Request: req,
}, nil
})
result := runtime.fileDownload(goja.FunctionCall{Arguments: []goja.Value{
runtime.vm.ToValue("https://cdn.example.com/track.flac"),
runtime.vm.ToValue("out/track.flac"),
}}).Export().(map[string]any)
if result["success"] != false {
t.Fatalf("expected failed download, got %#v", result)
}
if attempts != 1 {
t.Fatalf("attempts = %d, want no automatic resume", attempts)
}
finalPath := filepath.Join(runtime.dataDir, "out", "track.flac")
if _, err := os.Stat(finalPath); !os.IsNotExist(err) {
t.Fatalf("partial download visible at final path: %v", err)
}
if _, err := os.Stat(stagedDownloadPath(finalPath)); !os.IsNotExist(err) {
t.Fatalf("staged file left behind: %v", err)
}
}
func TestFileDownloadResumesAfterMidBodyCutWhenEnabled(t *testing.T) {
const full = "hello-world!"
var attempts int
var resumeRange, resumeIfRange string
@@ -300,6 +335,7 @@ func TestFileDownloadResumesAfterMidBodyCut(t *testing.T) {
result := runtime.fileDownload(goja.FunctionCall{Arguments: []goja.Value{
runtime.vm.ToValue("https://cdn.example.com/track.flac"),
runtime.vm.ToValue("out/track.flac"),
runtime.vm.ToValue(map[string]any{"resume": true}),
}}).Export().(map[string]any)
if result["success"] != true {
t.Fatalf("download result = %#v", result)
@@ -342,6 +378,7 @@ func TestFileDownloadResumeRestartsWhenRangeIgnored(t *testing.T) {
result := runtime.fileDownload(goja.FunctionCall{Arguments: []goja.Value{
runtime.vm.ToValue("https://cdn.example.com/track.flac"),
runtime.vm.ToValue("out/track.flac"),
runtime.vm.ToValue(map[string]any{"resume": true}),
}}).Export().(map[string]any)
if result["success"] != true {
t.Fatalf("download result = %#v", result)