mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-29 15:28:48 +02:00
fix(extensions): surface swallowed signed-session verification in provider fallback
Two holes let a provider's verification requirement vanish so the fallback chain failed outright instead of opening the challenge: - classifyDownloadErrorType did not recognize 'signed session expired' (the exact error ensureSignedSession returns), so an expired session failed generically. - Extensions that catch the needsVerification response internally (e.g. qobuz-web's availability search) report a plain 'not available', which the fallback loop skipped past silently. The runtime now remembers when a signed-session call inside the current script invocation required verification; the provider wrapper consumes that after checkAvailability/download and tags the failure as verification_required, regardless of how the script handled it. The source-extension path also classifies thrown errors now.
This commit is contained in:
@@ -457,6 +457,7 @@ func classifyDownloadErrorType(msg string) string {
|
||||
strings.Contains(lowerMsg, "needs verification") ||
|
||||
strings.Contains(lowerMsg, "session is not authenticated") ||
|
||||
strings.Contains(lowerMsg, "signed session is not authenticated") ||
|
||||
strings.Contains(lowerMsg, "signed session expired") ||
|
||||
strings.Contains(lowerMsg, "unauthorized") ||
|
||||
strings.Contains(lowerMsg, "precondition required") ||
|
||||
messageHasHTTPStatusCode(lowerMsg, "401") ||
|
||||
|
||||
@@ -695,7 +695,11 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
}
|
||||
GoLog("[DownloadWithExtensionFallback] Source extension %s failed: %v\n", req.Source, lastErr)
|
||||
|
||||
if strings.EqualFold(lastErrType, "verification_required") {
|
||||
sourceErrType := lastErrType
|
||||
if sourceErrType == "" && lastErr != nil {
|
||||
sourceErrType = classifyDownloadErrorType(lastErr.Error())
|
||||
}
|
||||
if strings.EqualFold(sourceErrType, "verification_required") {
|
||||
GoLog("[DownloadWithExtensionFallback] Source extension %s requires verification, not trying other providers\n", req.Source)
|
||||
return &DownloadResponse{
|
||||
Success: false,
|
||||
|
||||
@@ -457,6 +457,14 @@ func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, a
|
||||
script: script,
|
||||
timeout: DefaultJSTimeout,
|
||||
itemID: itemID,
|
||||
beforeRun: func() func() {
|
||||
// Drop any stale flag so the post-run check below only sees
|
||||
// verification requested by THIS call.
|
||||
if p.extension.runtime != nil {
|
||||
p.extension.runtime.consumeVerificationRequired()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}, func(perf *extensionCallPerf, result goja.Value) (*ExtAvailabilityResult, error) {
|
||||
if result == nil || goja.IsUndefined(result) || goja.IsNull(result) {
|
||||
return &ExtAvailabilityResult{Available: false, Reason: "not implemented"}, nil
|
||||
@@ -465,6 +473,19 @@ func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, a
|
||||
availability := parseExtensionAvailabilityValue(p.vm, result)
|
||||
perf.recordParse(time.Since(parseStartedAt))
|
||||
perf.setItems(1)
|
||||
// A signed-session call inside checkAvailability required
|
||||
// verification. Extensions often swallow that and report a plain
|
||||
// "not available", which would silently skip this provider's
|
||||
// challenge; surface it as an error so the fallback loop pauses and
|
||||
// opens the challenge instead.
|
||||
if !availability.Available && p.extension.runtime != nil {
|
||||
if p.extension.runtime.consumeVerificationRequired() != "" {
|
||||
return nil, fmt.Errorf(
|
||||
"VERIFY_REQUIRED: extension '%s' needs signed-session verification",
|
||||
p.extension.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
return &availability, nil
|
||||
})
|
||||
}
|
||||
@@ -533,6 +554,12 @@ func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID
|
||||
})()
|
||||
`, trackID, quality, outputPath)
|
||||
|
||||
if runtime != nil {
|
||||
// Drop any stale flag (pooled runtimes survive across downloads) so
|
||||
// the post-run check only sees verification from THIS call.
|
||||
runtime.consumeVerificationRequired()
|
||||
}
|
||||
|
||||
jsStartedAt := time.Now()
|
||||
result, err := RunWithTimeoutAndRecover(vm, script, ExtDownloadTimeout)
|
||||
perf.recordJS(time.Since(jsStartedAt))
|
||||
@@ -573,6 +600,19 @@ func (p *extensionProviderWrapper) Download(trackID, quality, outputPath, itemID
|
||||
downloadResult.DecryptionKey,
|
||||
)
|
||||
|
||||
// A signed-session call inside download() required verification but the
|
||||
// script reported a generic failure; tag the result so the fallback loop
|
||||
// pauses and opens this provider's challenge instead of skipping it.
|
||||
if runtime != nil && !downloadResult.Success {
|
||||
if runtime.consumeVerificationRequired() != "" &&
|
||||
!strings.EqualFold(downloadResult.ErrorType, "verification_required") {
|
||||
downloadResult.ErrorType = "verification_required"
|
||||
if downloadResult.ErrorMessage == "" {
|
||||
downloadResult.ErrorMessage = "Verification required"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &downloadResult, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,34 @@ type extensionRuntime struct {
|
||||
credentialsCache map[string]any
|
||||
credentialsLoaded bool
|
||||
storageFlushDelay time.Duration
|
||||
|
||||
// Set when a signed-session call inside the current script invocation
|
||||
// required verification. The provider wrapper consumes it after the
|
||||
// script returns, so verification surfaces even when the extension
|
||||
// script swallowed the needsVerification response (issue: fallback
|
||||
// skipped provider B's challenge and failed outright).
|
||||
verificationMu sync.Mutex
|
||||
verificationRequiredURL string
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) noteVerificationRequired(authURL string) {
|
||||
r.verificationMu.Lock()
|
||||
if authURL == "" {
|
||||
authURL = "pending"
|
||||
}
|
||||
r.verificationRequiredURL = authURL
|
||||
r.verificationMu.Unlock()
|
||||
}
|
||||
|
||||
// consumeVerificationRequired returns the noted auth URL (or "pending") and
|
||||
// clears the flag; "" means no verification was requested since the last
|
||||
// consume.
|
||||
func (r *extensionRuntime) consumeVerificationRequired() string {
|
||||
r.verificationMu.Lock()
|
||||
url := r.verificationRequiredURL
|
||||
r.verificationRequiredURL = ""
|
||||
r.verificationMu.Unlock()
|
||||
return url
|
||||
}
|
||||
|
||||
type privateIPCacheEntry struct {
|
||||
|
||||
@@ -1039,3 +1039,28 @@ func TestExtensionRuntimeUtilityAPIs(t *testing.T) {
|
||||
t.Fatalf("sanitize wrapper = %q", clean)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifySignedSessionExpiredAsVerification(t *testing.T) {
|
||||
got := classifyDownloadErrorType("Failed to resolve Deezer download: signed session expired")
|
||||
if got != "verification_required" {
|
||||
t.Fatalf("expected verification_required, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerificationRequiredNoteConsume(t *testing.T) {
|
||||
r := &extensionRuntime{}
|
||||
if got := r.consumeVerificationRequired(); got != "" {
|
||||
t.Fatalf("expected empty before note, got %q", got)
|
||||
}
|
||||
r.noteVerificationRequired("")
|
||||
if got := r.consumeVerificationRequired(); got != "pending" {
|
||||
t.Fatalf("expected pending sentinel, got %q", got)
|
||||
}
|
||||
if got := r.consumeVerificationRequired(); got != "" {
|
||||
t.Fatalf("expected cleared after consume, got %q", got)
|
||||
}
|
||||
r.noteVerificationRequired("https://x/auth")
|
||||
if got := r.consumeVerificationRequired(); got != "https://x/auth" {
|
||||
t.Fatalf("expected auth url, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +370,7 @@ func (r *extensionRuntime) signedSessionFetch(call goja.FunctionCall) goja.Value
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) signedSessionVerificationRequiredValue(authURL string) goja.Value {
|
||||
r.noteVerificationRequired(authURL)
|
||||
return r.vm.ToValue(map[string]any{
|
||||
"ok": false,
|
||||
"needsVerification": true,
|
||||
|
||||
Reference in New Issue
Block a user