Sì, ovviamente è possibile in NGINX!
Quello che potresti fare è implementare il seguente DFA :
Implementare la limitazione della velocità, in base $http_referer
, eventualmente usando un po 'di regex attraverso a map
per normalizzare i valori. Quando viene superato il limite, viene sollevata una pagina di errore interna, che è possibile rilevare attraverso un error_page
gestore come da una domanda correlata , andando in una nuova posizione interna come reindirizzamento interno (non visibile al client).
Nella posizione sopra per limiti superati, si esegue una richiesta di avviso, lasciando che la logica esterna esegua la notifica; questa richiesta viene successivamente memorizzata nella cache, assicurando che otterrai solo 1 richiesta unica per una determinata finestra temporale.
Cattura il codice di stato HTTP della richiesta precedente (restituendo un codice di stato ≥ 300 e utilizzando proxy_intercept_errors on
o, in alternativa, utilizza il valore predefinito non predefinito auth_request
o add_after_body
per effettuare una richiesta secondaria "gratuita") e completa la richiesta originale come se il passo precedente non era coinvolto. Si noti che è necessario abilitare la error_page
gestione ricorsiva affinché funzioni.
Ecco il mio PoC e un MVP, anche su https://github.com/cnst/StackOverflow.cnst.nginx.conf/blob/master/sf.432636.detecting-slashdot-effect-in-nginx.conf :
limit_req_zone $http_referer zone=slash:10m rate=1r/m; # XXX: how many req/minute?
server {
listen 2636;
location / {
limit_req zone=slash nodelay;
#limit_req_status 429; #nginx 1.3.15
#error_page 429 = @dot;
error_page 503 = @dot;
proxy_pass http://localhost:2635;
# an outright `return 200` has a higher precedence over the limit
}
recursive_error_pages on;
location @dot {
proxy_pass http://127.0.0.1:2637/?ref=$http_referer;
# if you don't have `resolver`, no URI modification is allowed:
#proxy_pass http://localhost:2637;
proxy_intercept_errors on;
error_page 429 = @slash;
}
location @slash {
# XXX: placeholder for your content:
return 200 "$uri: we're too fast!\n";
}
}
server {
listen 2635;
# XXX: placeholder for your content:
return 200 "$uri: going steady\n";
}
proxy_cache_path /tmp/nginx/slashdotted inactive=1h
max_size=64m keys_zone=slashdotted:10m;
server {
# we need to flip the 200 status into the one >=300, so that
# we can then catch it through proxy_intercept_errors above
listen 2637;
error_page 429 @/.;
return 429;
location @/. {
proxy_cache slashdotted;
proxy_cache_valid 200 60s; # XXX: how often to get notifications?
proxy_pass http://localhost:2638;
}
}
server {
# IRL this would be an actual script, or
# a proxy_pass redirect to an HTTP to SMS or SMTP gateway
listen 2638;
return 200 authorities_alerted\n;
}
Si noti che funziona come previsto:
% sh -c 'rm /tmp/slashdotted.nginx/*; mkdir /tmp/slashdotted.nginx; nginx -s reload; for i in 1 2 3; do curl -H "Referer: test" localhost:2636; sleep 2; done; tail /var/log/nginx/access.log'
/: going steady
/: we're too fast!
/: we're too fast!
127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.1" 200 16 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.0" 200 16 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 200 20 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"
%
Puoi vedere che la prima richiesta si traduce in un hit front-end e in un back-end, come previsto (ho dovuto aggiungere un backend fittizio nella posizione che ha limit_req
, poiché return 200
avrebbe la precedenza sui limiti, non è necessario un vero backend per il resto del trattamento).
La seconda richiesta è al di sopra del limite, quindi, inviamo l'avviso (ricevendo 200
) e lo memorizziamo nella cache, restituendo 429
(ciò è necessario a causa della suddetta limitazione che le richieste inferiori a 300 non possono essere catturate), che viene successivamente catturato dal front-end , che ora è libero di fare ciò che vuole.
La terza richiesta supera ancora il limite, ma abbiamo già inviato l'avviso, quindi non viene inviato alcun nuovo avviso.
Fatto! Non dimenticare di rovesciarlo su GitHub!