v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.
세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
71
server/utils/analytics-request-limit.js
Normal file
71
server/utils/analytics-request-limit.js
Normal file
@@ -0,0 +1,71 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user