feat(download): source typing + torrent file-count guard (#227)

Distinguishes plain URLs from BitTorrent sources at task creation:
src_file must name a .torrent, magnet links auto-pick a BT-capable
provider (qBittorrent preferred, aria2 fallback), and ytdlp is rejected
for torrent sources. validateFiles now caps selected files at 10k to
block crafted-torrent entity floods.

Generated with Devin
pull/3589/head
Tomáš Dvořák 2 weeks ago committed by GitHub
parent 1f4c32f2d5
commit 1b9a6850ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -225,6 +225,7 @@ Order = user-visible value first; each ships with backend + UI + tests.
- [x] Tencent Captcha (upstream #2178) — `captcha_type=tcaptcha` now performs real verification: `pkg/tcaptcha` calls Tencent Cloud `DescribeCaptchaResult` with full TC3-HMAC-SHA256 request signing (CaptchaAppId/AppSecretKey + SecretId/SecretKey, CaptchaType 9, client IP propagated); login/register/forgot-password flows emit `{ticket, randstr}` from the TCaptcha.js popup widget via a new `TCaptcha` verify-button component; admin Captcha section gains the provider option + four credential fields; en+zh locales - [x] Tencent Captcha (upstream #2178) — `captcha_type=tcaptcha` now performs real verification: `pkg/tcaptcha` calls Tencent Cloud `DescribeCaptchaResult` with full TC3-HMAC-SHA256 request signing (CaptchaAppId/AppSecretKey + SecretId/SecretKey, CaptchaType 9, client IP propagated); login/register/forgot-password flows emit `{ticket, randstr}` from the TCaptcha.js popup widget via a new `TCaptcha` verify-button component; admin Captcha section gains the provider option + four credential fields; en+zh locales
- [x] Localized admin strings (#25/#2691) — `setting.Provider.Localized` resolves any `<key>_i18n` JSON map by language tag (exact → bare primary subtag → wildcard `*` → base value); `SiteBasicLocalized` covers site name/title/description; consumed by site config, announcement endpoint, share-preview OG tags, index.html placeholders, WOPI breadcrumb, and email templates (recipient language); SKU gains `name_i18n`/`des_i18n` columns resolved per buyer language in the shop; admin gets a reusable `LocalizedFields` accordion (per-language inputs) wired into site name/description/announcement and SKU name/description; en+zh locales - [x] Localized admin strings (#25/#2691) — `setting.Provider.Localized` resolves any `<key>_i18n` JSON map by language tag (exact → bare primary subtag → wildcard `*` → base value); `SiteBasicLocalized` covers site name/title/description; consumed by site config, announcement endpoint, share-preview OG tags, index.html placeholders, WOPI breadcrumb, and email templates (recipient language); SKU gains `name_i18n`/`des_i18n` columns resolved per buyer language in the shop; admin gets a reusable `LocalizedFields` accordion (per-language inputs) wired into site name/description/announcement and SKU name/description; en+zh locales
- [x] Weighted policy selection (upstream #2178 item 2) — `GroupSetting.WeightedPolicies` spreads uploads across the group's allowed policies by free capacity: `pickByFreeCapacity` picks the member with the most remaining `MaxTotalSize` headroom that fits the file (uncapped/suspended members not weighed); explicit directory/user preferences still win; size-aware `getPreferredPolicyForSize` wired into both upload paths; admin group editor gains a switch, en+zh locales - [x] Weighted policy selection (upstream #2178 item 2) — `GroupSetting.WeightedPolicies` spreads uploads across the group's allowed policies by free capacity: `pickByFreeCapacity` picks the member with the most remaining `MaxTotalSize` headroom that fits the file (uncapped/suspended members not weighed); explicit directory/user preferences still win; size-aware `getPreferredPolicyForSize` wired into both upload paths; admin group editor gains a switch, en+zh locales
- [x] Download source typing + torrent bomb guard (upstream #2178 离线下载) — `CreateDownloadTask` distinguishes plain URLs from BitTorrent sources: `src_file` must name a `.torrent`, `magnet:` links auto-pick a BT-capable provider (qBittorrent preferred, aria2 fallback) and fail fast when only non-BT nodes exist; explicit `provider=ytdlp` with a torrent source is rejected; `validateFiles` caps selected files per task at `maxDownloadFiles` (10k) with `queue.CriticalErr` so crafted torrents cannot flood the entity table
## 6. Phase D — desktop, all platforms ## 6. Phase D — desktop, all platforms

@ -78,6 +78,11 @@ const (
GetTaskStatusMaxTries = 5 GetTaskStatusMaxTries = 5
// maxDownloadFiles bounds the number of files a single download task
// may import, guarding against crafted torrents that declare absurd
// file counts.
maxDownloadFiles = 10_000
SummaryKeyDownloadStatus = "download" SummaryKeyDownloadStatus = "download"
SummaryKeySrcStr = "src_str" SummaryKeySrcStr = "src_str"
@ -761,6 +766,12 @@ func (m *RemoteDownloadTask) validateFiles(ctx context.Context, dep dependency.D
return fmt.Errorf("no selected file found in download task") return fmt.Errorf("no selected file found in download task")
} }
// Bound the entity count a single task can create — a crafted torrent
// declaring hundreds of thousands of files is a database bomb.
if len(selectedFiles) > maxDownloadFiles {
return fmt.Errorf("download task selects %d files, exceeds the %d limit", len(selectedFiles), maxDownloadFiles)
}
validateArgs := lo.Map(selectedFiles, func(f downloader.TaskFile, _ int) fs.PreValidateFile { validateArgs := lo.Map(selectedFiles, func(f downloader.TaskFile, _ int) fs.PreValidateFile {
return fs.PreValidateFile{ return fs.PreValidateFile{
Name: sanitizeFileName(f.Name), Name: sanitizeFileName(f.Name),

@ -200,6 +200,37 @@ func (service *DownloadWorkflowService) CreateDownloadTask(c *gin.Context) ([]*T
} }
} }
// Distinguish BitTorrent sources (magnet links, .torrent files) from
// plain URLs: they require a BT-capable provider, and src_file must
// actually name a .torrent.
if service.SrcFile != "" && !strings.HasSuffix(strings.ToLower(service.SrcFile), ".torrent") {
return nil, serializer.NewError(serializer.CodeParamErr, "Source file must be a .torrent", nil)
}
torrentSrc := service.SrcFile != ""
for _, s := range service.Src {
if strings.HasPrefix(strings.ToLower(s), "magnet:") {
torrentSrc = true
break
}
}
if torrentSrc {
if service.Provider == string(types.DownloaderProviderYtDlp) {
return nil, serializer.NewError(serializer.CodeParamErr, "Downloader does not support torrent sources", nil)
}
if service.Provider == "" {
// Auto-pick a BitTorrent-capable provider, preferring qBittorrent.
for _, p := range []types.DownloaderProvider{types.DownloaderProviderQBittorrent, types.DownloaderProviderAria2} {
if providerNodeAvailable(c, dep, string(p)) != 0 {
service.Provider = string(p)
break
}
}
if service.Provider == "" {
return nil, serializer.NewError(serializer.CodeParamErr, "No BitTorrent-capable downloader node available", nil)
}
}
}
// Validate a requested downloader provider against what the node pool // Validate a requested downloader provider against what the node pool
// actually offers. // actually offers.
if service.Provider != "" && providerNodeAvailable(c, dep, service.Provider) == 0 { if service.Provider != "" && providerNodeAvailable(c, dep, service.Provider) == 0 {

Loading…
Cancel
Save