diff --git a/cmd/server/main.go b/cmd/server/main.go index 72cdf5d..9f8ba43 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -18,6 +18,7 @@ import ( "mal/internal/features/auth" "mal/internal/jikan" "mal/internal/server" + "mal/internal/shared/middleware" "mal/internal/worker" ) @@ -101,4 +102,5 @@ func gracefulShutdown(srv *http.Server, ctx context.Context) { if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("server shutdown failed: %v", err) } + middleware.StopCleanup() } diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 58b1e74..f7b089e 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -698,9 +698,9 @@ WHERE anime_id = ? ` type MarkAnimeFetchRetryFailedParams struct { - Datetime interface{} `json:"datetime"` - LastError string `json:"last_error"` - AnimeID int64 `json:"anime_id"` + Datetime string `json:"datetime"` + LastError string `json:"last_error"` + AnimeID int64 `json:"anime_id"` } func (q *Queries) MarkAnimeFetchRetryFailed(ctx context.Context, arg MarkAnimeFetchRetryFailedParams) error { diff --git a/internal/features/auth/auth.go b/internal/features/auth/auth.go index eedf640..87729b0 100644 --- a/internal/features/auth/auth.go +++ b/internal/features/auth/auth.go @@ -97,13 +97,13 @@ func (s *Service) ValidateSession(ctx context.Context, sessionID string) (*datab } func SetSessionCookie(w http.ResponseWriter, sessionID string, expiresAt time.Time) { - isProd := os.Getenv("ENV") == "production" + secure := os.Getenv("ENV") == "production" || os.Getenv("FORCE_SECURE_COOKIES") == "true" http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: sessionID, Expires: expiresAt, HttpOnly: true, - Secure: isProd, + Secure: secure, SameSite: http.SameSiteStrictMode, Path: "/", }) diff --git a/internal/features/playback/allanime_client.go b/internal/features/playback/allanime_client.go index 399bd9b..c577dda 100644 --- a/internal/features/playback/allanime_client.go +++ b/internal/features/playback/allanime_client.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "time" ) @@ -19,6 +20,7 @@ const ( allAnimeBaseURL = "https://api.allanime.day" allAnimeReferer = "https://allmanga.to" defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0" + allAnimeAESKey = "ALLANIME_AES_KEY" ) type searchResult struct { @@ -398,7 +400,15 @@ func decryptTobeparsed(encoded string) ([]byte, error) { iv := raw[:12] cipherText := raw[12 : len(raw)-16] tag := raw[len(raw)-16:] - key := sha256.Sum256([]byte("SimtVuagFbGR2K7P")) + + keyStr := os.Getenv(allAnimeAESKey) + if keyStr == "" { + keyStr = "SimtVuagFbGR2K7P" + } + if len(keyStr) < 16 { + return nil, fmt.Errorf("ALLANIME_AES_KEY must be at least 16 characters") + } + key := sha256.Sum256([]byte(keyStr)) block, err := aes.NewCipher(key[:]) if err != nil { diff --git a/internal/features/playback/handler.go b/internal/features/playback/handler.go index 3a3f699..9ed2280 100644 --- a/internal/features/playback/handler.go +++ b/internal/features/playback/handler.go @@ -236,7 +236,7 @@ func (h *Handler) HandleSaveProgress(w http.ResponseWriter, r *http.Request) { } var payload saveProgressRequest - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + if err := json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&payload); err != nil { http.Error(w, "invalid payload", http.StatusBadRequest) return } diff --git a/internal/jikan/client.go b/internal/jikan/client.go index 863ef0e..a2fc07a 100644 --- a/internal/jikan/client.go +++ b/internal/jikan/client.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "net" "net/http" "strconv" @@ -241,9 +242,14 @@ func (c *Client) getWithCache(ctx context.Context, cacheKey string, ttl time.Dur if err := c.fetchWithRetry(ctx, url, out); err != nil { if hasStale { - staleBytes, _ := json.Marshal(stale) - json.Unmarshal(staleBytes, out) - return nil + staleBytes, marshalErr := json.Marshal(stale) + if marshalErr == nil { + unmarshalErr := json.Unmarshal(staleBytes, out) + if unmarshalErr == nil { + return nil + } + } + log.Printf("jikan: stale cache unmarshal failed, falling back to error: %v", err) } return err } diff --git a/internal/shared/middleware/ratelimit.go b/internal/shared/middleware/ratelimit.go index fff305e..7e4b706 100644 --- a/internal/shared/middleware/ratelimit.go +++ b/internal/shared/middleware/ratelimit.go @@ -16,22 +16,31 @@ type visitor struct { var ( visitors = make(map[string]*visitor) mu sync.Mutex + quit = make(chan struct{}) ) func init() { go cleanupVisitors() } +func StopCleanup() { + close(quit) +} + func cleanupVisitors() { for { - time.Sleep(time.Minute) - mu.Lock() - for ip, v := range visitors { - if time.Since(v.lastSeen) > 3*time.Minute { - delete(visitors, ip) + select { + case <-quit: + return + case <-time.After(time.Minute): + mu.Lock() + for ip, v := range visitors { + if time.Since(v.lastSeen) > 3*time.Minute { + delete(visitors, ip) + } } + mu.Unlock() } - mu.Unlock() } }