Skip to content

Commit 3df6871

Browse files
committed
refactor: Enhance destination path validation and sanitization in download functions; add support for episode switching without updater
1 parent 8629d27 commit 3df6871

4 files changed

Lines changed: 120 additions & 5 deletions

File tree

internal/api/allanime_smart.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,28 @@ func DownloadAllAnimeSmartRange(anime *models.Anime, startEp, endEp int, quality
7979

8080
// smartDownload chooses the best method to download AllAnime links (HLS/hosters)
8181
func smartDownload(url, dest string) error {
82+
// Sanitize and validate destination path under the downloads root
83+
safeDest, err := sanitizeSmartDest(dest)
84+
if err != nil {
85+
return err
86+
}
8287
// Ensure destination directory exists
83-
if err := os.MkdirAll(filepath.Dir(dest), 0700); err != nil {
88+
if err := os.MkdirAll(filepath.Dir(safeDest), 0700); err != nil {
8489
return err
8590
}
8691

8792
// Use yt-dlp for HLS/known hosters
8893
if shouldUseYtDlp(url) {
8994
ctx := context.Background()
9095
ytdlp.MustInstall(ctx, nil)
91-
dl := ytdlp.New().Output(dest)
96+
dl := ytdlp.New().Output(safeDest)
9297
_, err := dl.Run(ctx, url)
9398
if err != nil {
9499
return fmt.Errorf("yt-dlp failed: %w", err)
95100
}
96101
// Verify
97-
if st, err := os.Stat(dest); err != nil || st.Size() < 1024 {
98-
return fmt.Errorf("download verification failed for %s", dest)
102+
if st, err := os.Stat(safeDest); err != nil || st.Size() < 1024 {
103+
return fmt.Errorf("download verification failed for %s", safeDest)
99104
}
100105
return nil
101106
}
@@ -114,7 +119,8 @@ func smartDownload(url, dest string) error {
114119
if resp.StatusCode != http.StatusOK {
115120
return fmt.Errorf("bad status: %s", resp.Status)
116121
}
117-
out, err := os.Create(dest)
122+
// #nosec G304: path validated by sanitizeSmartDest to remain within the GoAnime downloads root
123+
out, err := os.Create(safeDest)
118124
if err != nil {
119125
return err
120126
}
@@ -191,6 +197,38 @@ func sanitizeSmart(name string) string {
191197
return name
192198
}
193199

200+
// sanitizeSmartDest ensures destination path is within the GoAnime downloads root under the user's home
201+
func sanitizeSmartDest(p string) (string, error) {
202+
if strings.TrimSpace(p) == "" {
203+
return "", fmt.Errorf("empty destination path")
204+
}
205+
if strings.HasPrefix(p, "-") || strings.ContainsAny(p, "\x00\n\r") {
206+
return "", fmt.Errorf("invalid destination path")
207+
}
208+
cleaned := filepath.Clean(p)
209+
home, err := os.UserHomeDir()
210+
if err != nil {
211+
return "", err
212+
}
213+
root := filepath.Join(home, ".local", "goanime", "downloads", "anime")
214+
absRoot, err := filepath.Abs(root)
215+
if err != nil {
216+
return "", err
217+
}
218+
absFile, err := filepath.Abs(cleaned)
219+
if err != nil {
220+
return "", err
221+
}
222+
rel, err := filepath.Rel(absRoot, absFile)
223+
if err != nil {
224+
return "", err
225+
}
226+
if strings.HasPrefix(rel, "..") {
227+
return "", fmt.Errorf("destination escapes downloads root: %s", cleaned)
228+
}
229+
return absFile, nil
230+
}
231+
194232
// Helpers to reduce complexity
195233

196234
// validateSmartRangeInputs ensures correct source and quality defaulting

internal/player/player.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import (
2727
"github.com/pkg/errors"
2828
)
2929

30+
// lastAnimeURL stores the most recent anime URL/ID to support navigation when no updater is present
31+
var lastAnimeURL string
32+
3033
const (
3134
padding = 2
3235
)
@@ -332,6 +335,8 @@ func HandleDownloadAndPlay(
332335
animeMalID int,
333336
updater *discord.RichPresenceUpdater,
334337
) error {
338+
// Persist the anime URL/ID to aid episode switching when updater is nil (e.g., Discord disabled)
339+
lastAnimeURL = animeURL
335340
downloadOption := askForDownload()
336341
switch downloadOption {
337342
case 1:

internal/player/playvideo.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,15 @@ func switchEpisode(newIndex int, episodes []models.Episode, anilistID int, updat
674674
anime = updater.GetAnime()
675675
}
676676

677+
// If no updater/anime context, try to synthesize from lastAnimeURL
678+
if anime == nil && lastAnimeURL != "" {
679+
guessedSource := ""
680+
if (len(lastAnimeURL) < 30 && !strings.Contains(lastAnimeURL, "http")) || strings.Contains(lastAnimeURL, "allanime") {
681+
guessedSource = "AllAnime"
682+
}
683+
anime = &models.Anime{URL: lastAnimeURL, Source: guessedSource}
684+
}
685+
677686
targetURL, err := GetVideoURLForEpisodeEnhanced(&target, anime)
678687
if err != nil {
679688
return fmt.Errorf("failed to get video URL: %w", err)

internal/player/scraper.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,40 @@ func GetVideoURLForEpisode(episodeURL string) (string, error) {
259259

260260
// GetVideoURLForEpisodeEnhanced gets the video URL using the enhanced API with AllAnime navigation support
261261
func GetVideoURLForEpisodeEnhanced(episode *models.Episode, anime *models.Anime) (string, error) {
262+
// If we don't have anime context, decide safely how to resolve
263+
if anime == nil {
264+
// If it's a normal HTTP URL, use legacy extraction
265+
if strings.Contains(episode.URL, "http") {
266+
if util.IsDebug {
267+
util.Debugf("No anime context; using legacy extraction for HTTP URL, episode %s", episode.Number)
268+
}
269+
return GetVideoURLForEpisode(episode.URL)
270+
}
271+
272+
// If episode.URL looks like an AllAnime ID, synthesize minimal anime context
273+
if isLikelyAllAnimeID(episode.URL) {
274+
if util.IsDebug {
275+
util.Debugf("No anime context; detected AllAnime ID '%s'. Using enhanced API with synthetic anime context.", episode.URL)
276+
}
277+
tmpAnime := &models.Anime{
278+
URL: episode.URL,
279+
Source: "AllAnime",
280+
Name: "[AllAnime]",
281+
}
282+
// Ensure episode number is set
283+
if episode.Number == "" && episode.Num > 0 {
284+
episode.Number = fmt.Sprintf("%d", episode.Num)
285+
}
286+
if episode.Number == "" {
287+
episode.Number = "1"
288+
}
289+
return api.GetEpisodeStreamURLEnhanced(episode, tmpAnime, util.GlobalQuality)
290+
}
291+
292+
// If it's likely just an episode number without anime context, we cannot resolve via enhanced API
293+
return "", fmt.Errorf("cannot resolve stream without anime context for episode %s; missing anime identifier", episode.Number)
294+
}
295+
262296
// Try AllAnime enhanced navigation first if applicable
263297
if isAllAnimeSourcePlayer(anime) {
264298
streamURL, err := api.GetEpisodeStreamURLEnhanced(episode, anime, util.GlobalQuality)
@@ -283,6 +317,9 @@ func GetVideoURLForEpisodeEnhanced(episode *models.Episode, anime *models.Anime)
283317

284318
// Helper function to check if anime is from AllAnime source (player module)
285319
func isAllAnimeSourcePlayer(anime *models.Anime) bool {
320+
if anime == nil {
321+
return false
322+
}
286323
if anime.Source == "AllAnime" {
287324
return true
288325
}
@@ -300,6 +337,32 @@ func isAllAnimeSourcePlayer(anime *models.Anime) bool {
300337
return false
301338
}
302339

340+
// Helper: detect if a string is purely numeric (e.g., "12" or "12.5")
341+
func isNumericString(s string) bool {
342+
if s == "" {
343+
return false
344+
}
345+
re := regexp.MustCompile(`^\d+(?:\.\d+)?$`)
346+
return re.MatchString(s)
347+
}
348+
349+
// Helper: detect if the value looks like an AllAnime ID (short, non-HTTP, alphanumeric with letters)
350+
func isLikelyAllAnimeID(s string) bool {
351+
if strings.Contains(s, "http") {
352+
return false
353+
}
354+
if isNumericString(s) {
355+
return false
356+
}
357+
// Typical AllAnime IDs are short-ish alphanumeric strings
358+
if len(s) >= 6 && len(s) < 30 {
359+
// Must contain at least one letter
360+
re := regexp.MustCompile(`[A-Za-z]`)
361+
return re.MatchString(s)
362+
}
363+
return false
364+
}
365+
303366
func extractVideoURL(url string) (string, error) {
304367
if util.IsDebug {
305368
util.Debugf("Extracting video URL from page: %s", url)

0 commit comments

Comments
 (0)