@@ -13,36 +13,110 @@ import { Ratelimit } from '@upstash/ratelimit';
1313import { Redis } from '@upstash/redis' ;
1414
1515const middlewareSig = process . env . SECRET ; // Secret known to middleware only
16+ const bodyLimit = parseInt ( process . env . BODYLIMIT ) ;
1617
1718const cache = new Map ( ) ; // must be outside of your serverless function handler
1819
1920const ratelimit = new Ratelimit ( {
2021 redis : new Redis ( {
2122 url : process . env . UPSTASH_REDIS_REST_URL_CACHE ,
22- token : process . env . UPSTASH_REDIS_REST_TOKEN_CACHE ,
23+ token : process . env . UPSTASH_REDIS_REST_TOKEN_CACHE
2324 } ) ,
2425 ephemeralCache : cache ,
2526 analytics : false ,
2627 limiter : Ratelimit . slidingWindow ( parseInt ( process . env . RATELIMIT ) , process . env . RATELIMIT_WINDOW + ' s' ) ,
27- prefix : " rl:" ,
28+ prefix : ' rl:' ,
2829 enableProtection : true
29- } )
30-
31- // Forwards requests at path /pipe/* to /stream/* in index.js after trimming the request body and headers
32- // and returns the response. This is done in middleware.js because, unlike index.js, middleware.js doesn't have
33- // problems with the `Expect: 100-continue` header sent by `curl -T- <url>`. middleware.js also doesn't read the
34- // request body. index.js also has a set bodyLimit which is incompatible with payloads at /pipe/* of arbitrary size.
35- // /stream/ path is exposed only to middleware.
36- // For requests not at path /pipe/*, returns null.
37- async function pipeToStream ( request ) {
38- const pipeUrl = new URL ( request . url ) ;
39- if ( ! pipeUrl . pathname . startsWith ( '/pipe/' ) ) return null ;
40- const streamUrl = pipeUrl . href . replace ( '/pipe/' , '/stream/' ) ;
41- return fetch ( streamUrl , { method : request . method , headers : { 'x-middleware' : middlewareSig } , redirect : 'manual' } ) ;
30+ } ) ;
31+
32+ const allowedMethods = [ 'HEAD' , 'GET' , 'POST' , 'PATCH' , 'DELETE' ] ;
33+
34+ const statusCodes = {
35+ 400 : 'Bad Request' ,
36+ 405 : 'Method Not Allowed' ,
37+ 417 : 'Expectation Failed' ,
38+ 429 : 'Too Many Requests'
39+ } ;
40+
41+ // Returns Response object with JSON body containing error details for given statusCode
42+ // message parameter is optional
43+ function errorResponse ( statusCode , message ) {
44+ const statusText = statusCodes [ statusCode ] ;
45+ const supportedMethods = allowedMethods . join ( ',' ) ;
46+ return Response . json (
47+ { message : message ?? statusText , error : statusText , statusCode } ,
48+ {
49+ status : statusCode ,
50+ statusText,
51+ headers : {
52+ 'Access-Control-Allow-Origin' : '*' ,
53+ 'Access-Control-Allow-Methods' : supportedMethods ,
54+ Allow : supportedMethods
55+ }
56+ }
57+ ) ;
4258}
4359
44- export default async function middleware ( request ) {
45- const fromMiddleware = request . headers . get ( 'x-middleware' ) === middlewareSig ;
60+ // Returns a response from the backend (serverless-function) to the piped request modified to have no body.
61+ // Prevents Fastify in the backend from parsing the original request.body unnecessarily.
62+ // May not be needed when Fastify dependency is removed from the backend.
63+ async function processPipe ( request ) {
64+ // No need to worry with GET/HEAD requests as Fastify at backend won't parse content for these methods
65+ if ( request . method . match ( / ( G E T | H E A D ) / gi) ) return next ( ) ;
66+ return fetch ( request . url , {
67+ method : request . method ,
68+ headers : { 'x-middleware-auth' : middlewareSig } ,
69+ redirect : 'manual'
70+ } ) ;
71+ }
72+
73+ // Performs basic validation and limiting
74+ // Returns a Response object or Promise that resolves to a Response
75+ // Calling next() actually returns a Response with added header 'x-middleware-next'
76+ // Ref: @vercel /edge source - https://www.npmjs.com/package/@vercel/edge?activeTab=code
77+ export default async function middleware ( request ) {
78+ // Block requests with Expect headers
79+ if ( request . headers . has ( 'expect' ) ) return errorResponse ( 417 , 'Expect header is not allowed' ) ;
80+
81+ // Detect a pipe request to let it have any Content-Type and Length, including `Transfer-Encoding: chunked` header
82+ const isPiped = new URL ( request . url ) . pathname . startsWith ( '/pipe/' ) ;
83+
84+ // Block unallowed methods and chunked transfer if not pipe
85+ if ( ! isPiped ) {
86+ if ( allowedMethods . includes ( request . method . toUpperCase ( ) ) === false ) {
87+ return errorResponse ( 405 , `Method: ${ request . method } , is not allowed` ) ;
88+ }
89+ if ( request . headers . get ( 'transfer-encoding' ) ?. toLowerCase ( ) ?. includes ( 'chunked' ) ) {
90+ return errorResponse ( 400 , 'Provide content-length header instead of chunked transfer' ) ;
91+ }
92+ }
93+
94+ const contentType = request . headers . get ( 'content-type' ) ?? '' ;
95+ const contentLength = parseInt ( request . headers . get ( 'content-length' ) ?? 0 ) ;
96+ // Absent content-length is as good as 0, as Transfer-Encoding: chunked is not allowed
97+
98+ switch ( true ) {
99+ case isPiped :
100+ break ; // Allowed to have any Content-Type and Length, including `Transfer-Encoding: chunked` header
101+ case contentLength === 0 :
102+ break ; // Doesn't matter what the content-type is as it wont be parsed
103+ case contentType . includes ( 'application/json' ) :
104+ case contentType . includes ( 'application/x-www-form-urlencoded' ) :
105+ case contentType . includes ( 'text/plain' ) :
106+ case contentType . includes ( 'text/html' ) :
107+ if ( contentLength > bodyLimit ) {
108+ return errorResponse ( 400 , `Content-Length: ${ contentLength } , is not within ${ bodyLimit } ` ) ;
109+ } else {
110+ break ;
111+ }
112+ default :
113+ return errorResponse ( 400 , `Content-Type: '${ contentType } ', is not allowed` ) ;
114+ }
115+
116+ // If one invocation of this middleware modifies the Request and resends,
117+ // another invocation of this middleware will intercept it before it reaches the backend.
118+ // The following flag tells the latter invocation that it need not re-process the request.
119+ const isFromMiddleware = request . headers . get ( 'x-middleware-auth' ) === middlewareSig ;
46120
47121 // Ratelimiting by ip is too restrictive: may block users accessing internet from the same router
48122 const ratelimitBy = [
@@ -52,21 +126,16 @@ export default async function middleware(request) {
52126 ] . join ( '@' ) ;
53127
54128 // For requests from middleware, no rate-limiting is necessary
55- const { success, reset } = fromMiddleware ? { success : true , reset : 0 } : await ratelimit . limit ( ratelimitBy ) ;
129+ const { success, reset } = isFromMiddleware ? { success : true , reset : 0 } : await ratelimit . limit ( ratelimitBy ) ;
56130
57131 if ( success ) {
58- const streamResponse = await pipeToStream ( request ) ;
59- // For non-pipe requests, streamResponse is null.
60- return streamResponse ?? next ( ) ;
61- }
62- else {
63- return Response . json (
64- { message : `Try after ${ ( reset - Date . now ( ) ) / 1000 } seconds` , error : "Too Many Requests" , statusCode : 429 } ,
65- {
66- status : 429 ,
67- statusText : "Too Many Requests" ,
68- headers : { "Access-Control-Allow-Origin" :"*" }
69- } ,
70- )
132+ switch ( true ) {
133+ case isPiped :
134+ if ( ! isFromMiddleware ) return processPipe ( request ) ;
135+ default :
136+ return next ( ) ;
137+ }
138+ } else {
139+ return errorResponse ( 429 , `Try after ${ ( reset - Date . now ( ) ) / 1000 } seconds` ) ;
71140 }
72141}
0 commit comments