세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
72 lines
2.0 KiB
JavaScript
72 lines
2.0 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { getClientIp } from './request-ip'
|
|
|
|
const ANALYTICS_WINDOW_MS = 60 * 1000
|
|
const ANALYTICS_PAGEVIEW_LIMIT = 120
|
|
const ANALYTICS_HEARTBEAT_LIMIT = 240
|
|
const ANALYTICS_BUCKET_LIMIT = 20000
|
|
const analyticsBuckets = new Map()
|
|
let lastCleanupAt = 0
|
|
|
|
/**
|
|
* 통계 제한 키에 사용할 IP 해시를 생성한다.
|
|
* @param {import('h3').H3Event} event - 요청 이벤트
|
|
* @param {'pageview'|'heartbeat'} kind - 통계 요청 종류
|
|
* @returns {string} 제한 버킷 키
|
|
*/
|
|
const createAnalyticsLimitKey = (event, kind) => createHash('sha256')
|
|
.update(`${kind}:${getClientIp(event)}`)
|
|
.digest('hex')
|
|
|
|
/**
|
|
* 만료된 통계 제한 버킷을 정리한다.
|
|
* @param {number} now - 현재 시각
|
|
* @returns {void}
|
|
*/
|
|
const cleanupAnalyticsBuckets = (now) => {
|
|
if (now - lastCleanupAt < ANALYTICS_WINDOW_MS && analyticsBuckets.size < ANALYTICS_BUCKET_LIMIT) {
|
|
return
|
|
}
|
|
|
|
lastCleanupAt = now
|
|
for (const [key, bucket] of analyticsBuckets) {
|
|
if (bucket.expiresAt <= now) {
|
|
analyticsBuckets.delete(key)
|
|
}
|
|
}
|
|
|
|
while (analyticsBuckets.size >= ANALYTICS_BUCKET_LIMIT) {
|
|
const oldestKey = analyticsBuckets.keys().next().value
|
|
if (!oldestKey) {
|
|
break
|
|
}
|
|
analyticsBuckets.delete(oldestKey)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 현재 IP의 통계 쓰기 요청을 허용할지 확인한다.
|
|
* @param {import('h3').H3Event} event - 요청 이벤트
|
|
* @param {'pageview'|'heartbeat'} kind - 통계 요청 종류
|
|
* @returns {boolean} 허용 여부
|
|
*/
|
|
export const shouldAcceptAnalyticsRequest = (event, kind) => {
|
|
const now = Date.now()
|
|
const limit = kind === 'heartbeat' ? ANALYTICS_HEARTBEAT_LIMIT : ANALYTICS_PAGEVIEW_LIMIT
|
|
const key = createAnalyticsLimitKey(event, kind)
|
|
|
|
cleanupAnalyticsBuckets(now)
|
|
|
|
const current = analyticsBuckets.get(key)
|
|
if (!current || current.expiresAt <= now) {
|
|
analyticsBuckets.set(key, {
|
|
count: 1,
|
|
expiresAt: now + ANALYTICS_WINDOW_MS
|
|
})
|
|
return true
|
|
}
|
|
|
|
current.count += 1
|
|
return current.count <= limit
|
|
}
|