@@ -9,7 +9,40 @@ const upload = multer({
99 limits : { fileSize : 50 * 1024 * 1024 } ,
1010} ) ;
1111
12- router . post ( "/extract" , upload . single ( "bin" ) , async ( req , res ) => {
12+ const rateLimitMap = new Map ( ) ;
13+ const RATE_LIMIT = 30 ;
14+ const RATE_WINDOW = 60 * 1000 ;
15+
16+ function rateLimit ( req , res , next ) {
17+ const ip = req . ip || req . socket . remoteAddress || "unknown" ;
18+ const now = Date . now ( ) ;
19+ const entry = rateLimitMap . get ( ip ) ;
20+
21+ if ( ! entry || now - entry . windowStart > RATE_WINDOW ) {
22+ rateLimitMap . set ( ip , { count : 1 , windowStart : now } ) ;
23+ return next ( ) ;
24+ }
25+
26+ entry . count ++ ;
27+ if ( entry . count > RATE_LIMIT ) {
28+ const retryAfter = Math . ceil ( ( RATE_WINDOW - ( now - entry . windowStart ) ) / 1000 ) ;
29+ res . set ( "Retry-After" , retryAfter ) ;
30+ return res . status ( 429 ) . json ( {
31+ error : `Too many requests. Try again in ${ retryAfter } s.` ,
32+ } ) ;
33+ }
34+
35+ next ( ) ;
36+ }
37+
38+ setInterval ( ( ) => {
39+ const now = Date . now ( ) ;
40+ for ( const [ ip , entry ] of rateLimitMap . entries ( ) ) {
41+ if ( now - entry . windowStart > RATE_WINDOW ) rateLimitMap . delete ( ip ) ;
42+ }
43+ } , RATE_WINDOW ) ;
44+
45+ router . post ( "/extract" , rateLimit , upload . single ( "bin" ) , async ( req , res ) => {
1346 try {
1447 if ( ! req . file ) return res . status ( 400 ) . json ( { error : "No file uploaded" } ) ;
1548
@@ -29,7 +62,7 @@ router.post("/extract", upload.single("bin"), async (req, res) => {
2962 }
3063} ) ;
3164
32- router . post ( "/frame/:index" , upload . single ( "bin" ) , async ( req , res ) => {
65+ router . post ( "/frame/:index" , rateLimit , upload . single ( "bin" ) , async ( req , res ) => {
3366 try {
3467 if ( ! req . file ) return res . status ( 400 ) . json ( { error : "No file uploaded" } ) ;
3568
@@ -50,16 +83,15 @@ router.post("/frame/:index", upload.single("bin"), async (req, res) => {
5083
5184router . post (
5285 "/replace" ,
86+ rateLimit ,
5387 upload . fields ( [ { name : "bin" } , { name : "image" } ] ) ,
5488 async ( req , res ) => {
5589 try {
5690 const binFile = req . files ?. [ "bin" ] ?. [ 0 ] ;
5791 const imageFile = req . files ?. [ "image" ] ?. [ 0 ] ;
5892
5993 if ( ! binFile || ! imageFile ) {
60- return res
61- . status ( 400 )
62- . json ( { error : "Both bin and image files are required" } ) ;
94+ return res . status ( 400 ) . json ( { error : "Both bin and image files are required" } ) ;
6395 }
6496
6597 const frameIndex = parseInt ( req . body . frameIndex , 10 ) ;
@@ -79,6 +111,15 @@ router.post(
79111
80112 const rowSize = Math . ceil ( ( width * 3 ) / 4 ) * 4 ;
81113 const pixelDataSize = rowSize * height ;
114+ const minSize = 54 + pixelDataSize ;
115+
116+ if ( decompressedSize < minSize ) {
117+ return res . status ( 400 ) . json ( {
118+ error : `Frame ${ frameIndex } decompressedSize (${ decompressedSize } ) is smaller than ` +
119+ `minimum BMP size (${ minSize } ) for ${ width } ×${ height } . File may be corrupt.` ,
120+ } ) ;
121+ }
122+
82123 const bmpBuffer = Buffer . alloc ( decompressedSize ) ;
83124
84125 bmpBuffer [ 0 ] = 0x42 ;
0 commit comments