commit 7f132979c4fead72ec9e2bc4330694989221871b Author: Fijxu Date: Tue Feb 11 20:13:49 2025 -0300 init openresty config diff --git a/configs/general.conf b/configs/general.conf new file mode 100644 index 0000000..d1f5d01 --- /dev/null +++ b/configs/general.conf @@ -0,0 +1,17 @@ +# ZSTD +# https://github.com/tokers/zstd-nginx-module +# zstd on; +# zstd_comp_level 1; +# zstd_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + +# BROTLI +# brotli on; +# brotli_comp_level 6; +# brotli_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + +# GZIP +gzip on; +gzip_vary on; +gzip_proxied any; +gzip_comp_level 6; +gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; diff --git a/configs/listen.conf b/configs/listen.conf new file mode 100644 index 0000000..e4a41ea --- /dev/null +++ b/configs/listen.conf @@ -0,0 +1,9 @@ +# Netmaker internal IP +set_real_ip_from 100.64.0.0/24; +real_ip_header X-Forwarded-For; +real_ip_recursive on; + +listen 443 ssl; +# Port that is only accessed via Netmaker and Tor, used to proxy the traffic to another server +listen 4080; +http2 on; diff --git a/configs/proxy.conf b/configs/proxy.conf new file mode 100644 index 0000000..9da3006 --- /dev/null +++ b/configs/proxy.conf @@ -0,0 +1,24 @@ +#Keep-alive +# www.f5.com/company/blog/nginx/avoiding-top-10-nginx-configuration-mistakes#no-keepalives +proxy_http_version 1.1; +proxy_set_header Connection $connection_upgrade; # Defined in /snippets/maps.conf + +#proxy_cache_bypass $http_upgrade; + +# Proxy SSL +proxy_ssl_server_name on; + +# Proxy headers +proxy_set_header Upgrade $http_upgrade; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header Forwarded $proxy_add_forwarded; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; +proxy_set_header X-Forwarded-Port $server_port; +proxy_set_header Host $host; + +# Proxy timeouts +#proxy_connect_timeout 60s; +#proxy_send_timeout 60s; +#proxy_read_timeout 60s; diff --git a/configs/robots.conf b/configs/robots.conf new file mode 100644 index 0000000..74e7286 --- /dev/null +++ b/configs/robots.conf @@ -0,0 +1,58 @@ +if ($http_user_agent ~* "(AdsBot-Google|Amazonbot|anthropic-ai|Applebot|Applebot-Extended|AwarioRssBot|AwarioSmartBot|Bytespider|CCBot|ChatGPT-User|ClaudeBot|Claude-Web|cohere-ai|DataForSeoBot|Diffbot|FacebookBot|FriendlyCrawler|Google-Extended|GoogleOther|GPTBot|img2dataset|ImagesiftBot|magpie-crawler|Meltwater|omgili|omgilibot|peer39_crawler|peer39_crawler/1.0|PerplexityBot|PiplBot|scoop.it|Seekr|YouBot|facebookexternalhit|OpenAI)"){ + return 444; +} + +location /robots.txt { return 200 " + User-agent: AhrefsBot + Disallow: / + + User-agent: dotbot + Disallow: / + + User-agent: SiteAuditBot + Disallow: / + + User-agent: SemrushBot-BA + Disallow: / + + User-agent: SemrushBot-SI + Disallow: / + + User-agent: SemrushBot-SWA + Disallow: / + + User-agent: SemrushBot-CT + Disallow: / + + User-agent: SplitSignalBot + Disallow: / + + User-agent: SemrushBot-COUB + Disallow: / + + User-agent: AdsBot-Google + User-agent: Amazonbot + User-agent: anthropic-ai + User-agent: Applebot-Extended + User-agent: Bytespider + User-agent: CCBot + User-agent: ChatGPT-User + User-agent: ClaudeBot + User-agent: Claude-Web + User-agent: cohere-ai + User-agent: Diffbot + User-agent: FacebookBot + User-agent: FriendlyCrawler + User-agent: Google-Extended + User-agent: GoogleOther + User-agent: GPTBot + User-agent: img2dataset + User-agent: omgili + User-agent: omgilibot + User-agent: peer39_crawler + User-agent: peer39_crawler/1.0 + User-agent: PerplexityBot + User-agent: YouBot + User-agent: facebookexternalhit/1.1 + Disallow: /"; +} diff --git a/configs/robotsNone.conf b/configs/robotsNone.conf new file mode 100644 index 0000000..41d5193 --- /dev/null +++ b/configs/robotsNone.conf @@ -0,0 +1 @@ +location /robots.txt { return 200 "User-agent: *\nDisallow: /";} diff --git a/configs/security.conf b/configs/security.conf new file mode 100644 index 0000000..81a0b70 --- /dev/null +++ b/configs/security.conf @@ -0,0 +1,6 @@ +# security headers +add_header X-Content-Type-Options "nosniff" always; +add_header Referrer-Policy "same-origin" always; +add_header X-Frame-Options "sameorigin" always; +add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" always; +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; diff --git a/configs/ssl.conf b/configs/ssl.conf new file mode 100644 index 0000000..8f7e28d --- /dev/null +++ b/configs/ssl.conf @@ -0,0 +1,5 @@ +# ECDSA +ssl_certificate /etc/ssl/nadeko.net/fullchain.cer; +ssl_certificate_key /etc/ssl/nadeko.net/nadeko.net.key; + +include configs/sslConfig.conf; diff --git a/configs/sslConfig.conf b/configs/sslConfig.conf new file mode 100644 index 0000000..0ba77d2 --- /dev/null +++ b/configs/sslConfig.conf @@ -0,0 +1,13 @@ +# SSL +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; +ssl_prefer_server_ciphers off; +ssl_session_timeout 1d; +ssl_session_cache shared:SSL:10m; +ssl_session_tickets off; +ssl_early_data on; +#ssl_conf_command Options KTLS; + +# Custom 4096bits Diffie-Hellman parameter for DHE ciphersuites (Not the one bundled with letsencrypt +# Changed to a custom one for trust purposes +ssl_dhparam /etc/nginx/dhparam.pem; diff --git a/configs/upstreams.conf b/configs/upstreams.conf new file mode 100644 index 0000000..115a8ee --- /dev/null +++ b/configs/upstreams.conf @@ -0,0 +1,223 @@ +upstream php-fpm-8.3 { + server unix:/run/php-fpm/php-fpm.sock; +} + +lua_shared_dict servers 12k; + +upstream inv { + # hash $remote_addr consistent; + # ip_hash; + #server unix:/run/invidious-haproxy/invidious.sock max_fails=1 fail_timeout=10s; + #server unix:/run/invidious-haproxy/invidious-vpn.sock max_fails=1 fail_timeout=10s; + + # server 127.0.0.1:10060; + # server 127.0.0.1:10070; + # server 127.0.0.1:10080; + # balancer_by_lua_file "conf/lua/invidious-sticky.lua"; + + server 127.0.0.1:11101; + server 127.0.0.1:11102; + server 127.0.0.1:11103; + server 127.0.0.1:11104; + server 127.0.0.1:11105; + balancer_by_lua_block { + local sticky = require "invidious-sticky" + local servers = { + { "127.0.0.1", 11101, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 11102, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 11103, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 11104, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 11105, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + } + local s = ngx.shared.servers + s:set("inv-backends", #servers) + sticky.run(servers, "invidious", "clearnet"); + } +} + +upstream invidious-1 { + keepalive 32; + server 127.0.0.1:11101; +} + +upstream invidious-2 { + keepalive 32; + server 127.0.0.1:11102; +} + +upstream invidious-3 { + keepalive 32; + server 127.0.0.1:11103; +} + +upstream invidious-4 { + keepalive 32; + server 127.0.0.1:11104; +} + +upstream invidious-5 { + keepalive 32; + server 127.0.0.1:11105; +} + +# upstream invidious-5 { +# server 127.0.0.1:11105; +# keepalive 2; +# } +# upstream inv-t1 { +# server 127.0.0.1:20201; +# } + +# upstream inv-tor { +# server 127.0.0.1:10062; +# server 127.0.0.1:10072; +# server 127.0.0.1:10082; +# server 127.0.0.1:20102; +# balancer_by_lua_block { +# local sticky = require "invidious-sticky" +# local servers = { +# { "127.0.0.1", 10062, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +# { "127.0.0.1", 10072, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +# { "127.0.0.1", 10082, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +# { "127.0.0.1", 20102, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +# } +# sticky.run(servers, "invidious-tor", "clearnet"); +# } +# } +# +# upstream inv-i2p { +# server 127.0.0.1:10063; +# server 127.0.0.1:10073; +# server 127.0.0.1:10083; +# server 127.0.0.1:20103; +# balancer_by_lua_block { +# local sticky = require "invidious-sticky" +# local servers = { +# { "127.0.0.1", 10063, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +# { "127.0.0.1", 10073, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +# { "127.0.0.1", 10083, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +# { "127.0.0.1", 20103, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +# } +# sticky.run(servers, "invidious-i2p", "clearnet"); +# } +# } + +upstream inv-feed-receiver { + server 127.0.0.1:20090; +} + +upstream http3-ytproxy { + #hash $remote_addr consistent; + # ip_hash; + #server unix:/run/invidious-haproxy/http3-proxy.sock; + #server unix:/run/invidious-haproxy/http3-proxy-vpn.sock; + server unix:/tmp/http3-ytproxy.sock; + keepalive 128; + # server 127.0.0.1:10061; + # server 127.0.0.1:10071; + # server 127.0.0.1:10081; + # server 127.0.0.1:20101; + # balancer_by_lua_block { + # local sticky = require "invidious-sticky" + # local servers = { + # { "unix:/tmp/http3-ytproxy.sock", 10061, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 } + # } + # sticky.run(servers, "http3-ytproxy", "clearnet"); + # } +} + +# upstream materialious { +# server 127.0.0.1:10013; +# } +# +# upstream materialious-tor { +# server 127.0.0.1:10070; +# } +# +# upstream syncious { +# server 127.0.0.1:10014; +# } +# +# upstream peerjs { +# server 127.0.0.1:10015; +# } + +upstream rimgo { + keepalive 128; + server 127.0.0.1:10001; +} + +upstream redlib { + keepalive 128; + server 127.0.0.1:10006; +} + +upstream breezewiki { + keepalive 16; + server 127.0.0.1:10007; +} + +upstream privatebin { + keepalive 4; + server 127.0.0.1:10002; +} + +upstream rustlog { + keepalive 4; + server 127.0.0.1:10003; +} + +upstream matrix { + keepalive 16; + server 127.0.0.1:10020; +} + +upstream matrix-nadeko { + keepalive 16; + server 127.0.0.1:10022; +} + +upstream peertube { + keepalive 16; + server 127.0.0.1:10016; +} + +upstream umami { + keepalive 16; + server 127.0.0.1:10005; +} + +upstream grafana { + keepalive 16; + server 127.0.0.1:20002; +} + +upstream forgejo { + keepalive 64; + server 127.0.0.1:10004; + server unix:/run/forgejo/forgejo.sock backup; +} + +# DEVELOPMENT +upstream inv-debug { + server 127.0.0.1:10060; + server 127.0.0.1:10070; + server 127.0.0.1:10080; + server 127.0.0.1:20100; + balancer_by_lua_block { + local sticky = require "stickydebug" + local servers = { + { "127.0.0.1", 10060, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10070, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10080, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 20100, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 20200, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + } + sticky.run(servers, "invidious-debug", "clearnet"); + } +} + +upstream http3-ytproxy-debug { + server 127.0.0.1:10078; + server 127.0.0.1:10080; +} diff --git a/http.d/4get.conf b/http.d/4get.conf new file mode 100644 index 0000000..bf48c0d --- /dev/null +++ b/http.d/4get.conf @@ -0,0 +1,76 @@ +# CLEARNET +server { + server_name + 4get.nadeko.net + nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; + include configs/listen.conf; + include configs/general.conf; + include configs/robotsNone.conf; + include configs/security.conf; + + location @upstream { + proxy_pass http://127.0.0.1:10031; + proxy_intercept_errors on; + include configs/proxy.conf; + } + + location / { + try_files $uri @upstream; + } + + # location /web { + # try_files $uri @upstream; + # + # if ($server_protocol ~* "HTTP/1.1") { + # return 444; + # } + # + # include snippets/torblacklist.conf; + # error_page 403 =302 /torisblocked; + # error_page 429 =302 /rl; + # } + # + # location /torisblocked { + # alias errors/$request_uri.txt; + # } + # + # location /rl { + # alias errors/$request_uri.txt; + # } + # + # location /data { + # return 444; + # } + + # Tor Header + add_header Onion-Location http://4get.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion$request_uri; + + # QUIC + # include configs/http3.conf; + + +} + +# TOR +# server { +# listen 10040; +# server_name 4get.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion 4get.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; +# include configs/listen.conf; +# root /var/www/4get-zzls; +# +# location @upstream { +# try_files $uri.php $uri/index.php =404; +# fastcgi_pass php-fpm-8.1; +# fastcgi_index index.php; +# include fastcgi.conf; +# fastcgi_intercept_errors on; +# } +# +# location / { +# try_files $uri @upstream; +# } +# +# location ~* ^(.*)\.php$ { +# return 301 $1; +# } +# } diff --git a/http.d/breezewiki.conf b/http.d/breezewiki.conf new file mode 100644 index 0000000..06f7746 --- /dev/null +++ b/http.d/breezewiki.conf @@ -0,0 +1,20 @@ +server { + server_name + breezewiki.nadeko.net + breezewiki.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; + include configs/listen.conf; + include configs/general.conf; + # Security headers are added by redlib! + #include configs/security.conf; + include configs/robotsNone.conf; + + location / { + if ($blocked_agent = 1) { + return 200 " + 1. The Industrial Revolution and its consequences have been a disaster for the human race. They have greatly increased the life-expectancy of those of us who live in “advanced” countries, but they have destabilized society, have made life unfulfilling, have subjected human beings to indignities, have led to widespread psychological suffering (in the Third World to physical suffering as well) and have inflicted severe damage on the natural world. The continued development of technology will worsen the situation. It will certainly subject human beings to greater indignities and inflict greater damage on the natural world, it will probably lead to greater social disruption and psychological suffering, and it may lead to increased physical suffering even in “advanced” countries. + "; + } + proxy_pass http://breezewiki; + include configs/proxy.conf; + } +} diff --git a/http.d/datamining.conf b/http.d/datamining.conf new file mode 100644 index 0000000..4e24f42 --- /dev/null +++ b/http.d/datamining.conf @@ -0,0 +1,14 @@ +# CLEARNET +server { + access_log /var/log/nginx/datamining.nadeko.net.access.log; + server_name datamining.nadeko.net; + include configs/listen.conf; + include configs/general.conf; + include configs/robotsNone.conf; + #include configs/security.conf; + + location / { + proxy_pass http://umami; + include configs/proxy.conf; + } +} diff --git a/http.d/default.conf b/http.d/default.conf new file mode 100644 index 0000000..09bdedb --- /dev/null +++ b/http.d/default.conf @@ -0,0 +1,20 @@ +server { + access_log /var/log/nginx/default.access.log; + server_name sf.nadeko.net; + include configs/general.conf; + include configs/security.conf; + include configs/robotsNone.conf; + + location / { + return 200 " + Hi, you just hit the seflhosted server of nadeko.net! + There is nothing here, this is just a landing page for services that don't exist + or deprecated services. If you think there is something missing, contact me! + + https://nadeko.net/contact + "; + } + + listen 443 ssl default_server reuseport; + http2 on; +} diff --git a/http.d/git.conf b/http.d/git.conf new file mode 100644 index 0000000..7a7de28 --- /dev/null +++ b/http.d/git.conf @@ -0,0 +1,20 @@ +server { + access_log /var/log/nginx/git.access.log; + error_log /var/log/nginx/git.error.log; + server_name git.nadeko.net; + include configs/listen.conf; + include configs/general.conf; + include configs/security.conf; + include configs/robots.conf; + + location / { + if ($blocked_agent = 1) { + return 200 " + 1. The Industrial Revolution and its consequences have been a disaster for the human race. They have greatly increased the life-expectancy of those of us who live in “advanced” countries, but they have destabilized society, have made life unfulfilling, have subjected human beings to indignities, have led to widespread psychological suffering (in the Third World to physical suffering as well) and have inflicted severe damage on the natural world. The continued development of technology will worsen the situation. It will certainly subject human beings to greater indignities and inflict greater damage on the natural world, it will probably lead to greater social disruption and psychological suffering, and it may lead to increased physical suffering even in “advanced” countries. + "; + } + proxy_pass http://forgejo; + include configs/proxy.conf; + client_max_body_size 1024M; + } +} diff --git a/http.d/inv.conf b/http.d/inv.conf new file mode 100644 index 0000000..5db0a37 --- /dev/null +++ b/http.d/inv.conf @@ -0,0 +1,180 @@ +map $host $invidious_backend { + default inv; + inv1.nadeko.net invidious-1; + inv2.nadeko.net invidious-2; + inv3.nadeko.net invidious-3; + inv4.nadeko.net invidious-4; + inv5.nadeko.net invidious-5; +} + +map $invidious_backend $cache_case { + default ""; + inv $cookie_INVIDIOUS_SERVER_ID; + invidious-* $invidious_backend; +} + +# CLEARNET +server { + error_log /var/log/nginx/inv.nadeko.net.error.log; + server_name + inv.nadeko.net + ~^inv([1-5])\.nadeko\.net$ + inv.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion ~^inv([1-5])\.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd\.onion$; + include configs/listen.conf; + include configs/robotsNone.conf; + # MAINTENANCE MODE + # include configs/maintenance-mode.conf; + + # The messed up invidious configuration + include http.d/locations/inv.conf; + + if ($http_user_agent = "") { + return 444; + } + + if ($request_method = OPTIONS) { + return 204; + } + + #rewrite_by_lua_file conf/lua/rewrite-invidious/init.lua; + + location = /503.html { + content_by_lua_file conf/lua/503-invidious.lua; + } + + location = /502.html { + content_by_lua_file conf/lua/502-invidious.lua; + } + + location = /switchbackend { + content_by_lua_file conf/lua/switchbackend.lua; + } + + # header_filter_by_lua_block { + # ngx.header["Onion-Location"] = "http://inv.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion" .. ngx.var.request_uri + # } + listen 10040; +} + +# TOR +# server { +# listen 10040; +# +# error_log /var/log/nginx/inv.nadeko.net.tor.error.log; +# #access_log /var/log/nginx/inv.nadeko.net.tor.access.log; +# server_name inv.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion inv.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; +# include configs/listen.conf; +# #include configs/general.conf; +# #include configs/robotsNone.conf; +# +# include http.d/locations/inv-tor.conf; +# +# rewrite_by_lua_file conf/lua/rewrite-invidious/init.lua; +# +# location = /503.html { +# content_by_lua_file conf/lua/503-invidious.lua; +# } +# +# location = /502.html { +# content_by_lua_file conf/lua/502-invidious.lua; +# } +# +# location = /switchbackend { +# content_by_lua_file conf/lua/switchbackend.lua; +# } +# } + +# TOR +# server { +# error_log /var/log/nginx/inv.nadeko.net.tor.error.log; +# #access_log /var/log/nginx/inv.nadeko.net.tor.access.log; +# server_name inv.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; +# include configs/listen.conf; +# include configs/robotsNone.conf; +# # MAINTENANCE MODE +# # include configs/maintenance-mode.conf; +# +# # The messed up invidious configuration +# include http.d/locations/inv.conf; +# +# if ($http_user_agent = "") { +# return 444; +# } +# +# if ($request_method = OPTIONS) { +# return 204; +# } +# +# rewrite_by_lua_file conf/lua/rewrite-invidious/init.lua; +# +# location = /503.html { +# content_by_lua_file conf/lua/503-invidious.lua; +# } +# +# location = /502.html { +# content_by_lua_file conf/lua/502-invidious.lua; +# } +# +# location = /switchbackend { +# content_by_lua_file conf/lua/switchbackend.lua; +# } +# +# listen 10040; +# } + +# I2P +# server { +# error_log /var/log/nginx/inv.nadeko.net.i2p.error.log; +# access_log /var/log/nginx/inv.nadeko.net.i2p.access.log; +# server_name inv.zzls.i2p zzlsbhhfvwg3oh36tcvx4r7n6jrw7zibvyvfxqlodcwn3mfrvzuq.b32.i2p; +# include configs/listen.conf; +# include configs/robotsNone.conf; +# # MAINTENANCE MODE +# # include configs/maintenance-mode.conf; +# +# # The messed up invidious configuration +# include http.d/locations/inv.conf; +# +# if ($http_user_agent = "") { +# return 444; +# } +# +# rewrite_by_lua_file conf/lua/rewrite-invidious/init.lua; +# +# location = /503.html { +# content_by_lua_file conf/lua/503-invidious.lua; +# } +# +# location = /502.html { +# content_by_lua_file conf/lua/502-invidious.lua; +# } +# +# location = /switchbackend { +# content_by_lua_file conf/lua/switchbackend.lua; +# } +# +# listen 10051; +# } + +# CLEARNET FEED +server { + error_log /var/log/nginx/inv.nadeko.net.feed.error.log; + access_log /var/log/nginx/inv.nadeko.net.feed.access.log; + server_name feed-inv.nadeko.net; + include configs/listen.conf; + include configs/robotsNone.conf; + + # The messed up invidious configuration + include http.d/locations/inv-feed.conf; +} + +server { + server_name inv-cl1.nadeko.net; + include configs/listen.conf; + include configs/robotsNone.conf; + + location / { + proxy_pass http://127.0.0.1:10080; + include configs/proxy.conf; + } +} diff --git a/http.d/locations/inv.conf b/http.d/locations/inv.conf new file mode 100644 index 0000000..961b56d --- /dev/null +++ b/http.d/locations/inv.conf @@ -0,0 +1,261 @@ +location @upstream { + proxy_pass http://$invidious_backend; + limit_rate 1000k; + # To reduce the load in the main storage + proxy_buffering off; + proxy_request_buffering off; + #proxy_cache off; + + # To let invidious know the Host header, needed for alternative_domains. + # https://git.nadeko.net/Fijxu/invidious/commit/35f28b508ea049118cb6a0b3062b6c7ce2c4009f + proxy_set_header Host $host; + + proxy_pass_request_headers on; + proxy_intercept_errors on; + proxy_connect_timeout 10s; + + # To keep-alive + proxy_http_version 1.1; + proxy_set_header Connection ""; + + error_page 502 /502.html; + error_page 503 /503.html; + + if ($request_method = OPTIONS) { + return 204; + } + + proxy_hide_header Access-Control-Allow-Origin; + add_header Access-Control-Allow-Credentials true; + add_header Access-Control-Allow-Origin "https://materialious.nadeko.net" always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE" always; + add_header Access-Control-Allow-Headers "User-Agent, Authorization, Content-Type" always; +} + +location @upstream-api { + proxy_pass http://inv; + limit_rate 1000k; + # To reduce the load in the main storage + proxy_buffering on; + proxy_request_buffering off; + #proxy_cache off; + + proxy_cache invidious-api-cache; + proxy_cache_valid 200 240m; + add_header X-Cache $upstream_cache_status; + proxy_cache_key "$host$request_uri$cache_case"; + + # To let invidious know the Host header, needed for alternative_domains. + # https://git.nadeko.net/Fijxu/invidious/commit/35f28b508ea049118cb6a0b3062b6c7ce2c4009f + proxy_set_header Host $host; + + proxy_pass_request_headers on; + proxy_intercept_errors on; + proxy_connect_timeout 10s; + + # To keep-alive + proxy_http_version 1.1; + proxy_set_header Connection ""; + + error_page 502 /502.html; + error_page 503 /503.html; + + if ($request_method = OPTIONS) { + return 204; + } + + proxy_hide_header Access-Control-Allow-Origin; + add_header Access-Control-Allow-Credentials true; + add_header Access-Control-Allow-Origin "https://materialious.nadeko.net" always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE" always; + add_header Access-Control-Allow-Headers "User-Agent, Authorization, Content-Type" always; +} + +location @upstream-latest_version { + proxy_pass http://inv; + limit_rate 1000k; + # To reduce the load in the main storage + proxy_buffering on; + proxy_request_buffering off; + #proxy_cache off; + + proxy_cache invidious-latest_version-cache; + proxy_cache_valid 302 240m; + add_header X-Cache $upstream_cache_status; + proxy_cache_key "$host$request_uri$cache_case"; + + # To let invidious know the Host header, needed for alternative_domains. + # https://git.nadeko.net/Fijxu/invidious/commit/35f28b508ea049118cb6a0b3062b6c7ce2c4009f + proxy_set_header Host $host; + + proxy_pass_request_headers on; + proxy_intercept_errors on; + proxy_connect_timeout 10s; + + # To keep-alive + proxy_http_version 1.1; + proxy_set_header Connection ""; + + error_page 502 /502.html; + error_page 503 /503.html; + + proxy_hide_header Access-Control-Allow-Origin; + add_header Access-Control-Allow-Credentials true; + add_header Access-Control-Allow-Origin "https://materialious.nadeko.net" always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE" always; + add_header Access-Control-Allow-Headers "User-Agent, Authorization, Content-Type" always; +} + +location @http3-proxy { + proxy_pass http://http3-ytproxy; + limit_rate 600k; + # proxy_intercept_errors on; + # To reduce the load in the main storage + proxy_buffering off; + proxy_request_buffering off; + proxy_cache off; + + sendfile_max_chunk 512k; + proxy_set_header X-Forwarded-For ""; + proxy_set_header Connection "keep-alive"; + proxy_hide_header "alt-svc"; + proxy_hide_header Cache-Control; + proxy_hide_header ETag; + proxy_pass_request_headers on; + proxy_http_version 1.1; +} + +location @xd { + proxy_pass http://http3-ytproxy; + limit_rate 600k; + # Proxy buffering needs to be on in order + # to make the cache work + proxy_buffering on; + proxy_request_buffering off; + + proxy_cache invidious-image-cache; + proxy_cache_valid 200 48h; + add_header X-Cache $upstream_cache_status; + + proxy_http_version 1.1; +} + +location ~ (^/videoplayback) { + return 403 ""; + try_files $uri @http3-proxy; +} + +location ~ (^/vi/) { + try_files $uri @xd; +} + +location ~ (^/ggpht/) { + try_files $uri @xd; +} + +location /latest_version { + try_files $uri @upstream-latest_version; + limit_req zone=invidious-latestversionrl nodelay burst=12; +} + +location /index.html { + return 301 $scheme://$host/; +} + +location / { + try_files $uri @upstream; +} + +location /search { + try_files $uri @upstream; + limit_req zone=invidious-searchrl nodelay burst=3; +} + +location /watch { + try_files $uri @upstream; + limit_req zone=invidious-watchrl nodelay burst=3; +} + +location /api/v1 { + limit_req zone=invidious-apirl nodelay burst=100; + try_files $uri @upstream; +} + +location ~ ^/api/v1/(videos|channels|search|mixes|trending) { + if ($http_user_agent !~ "Clipious/") { + return 401 "API disabled"; + } + try_files $uri @upstream-api; +} + +location /api/v1/comments { + try_files $uri @upstream-api; +} + +location /api/v1/auth/notifications { + return 403 "Endpoint disabled"; +} + +location /api/v1/auth/subscriptions { + return 403 "Endpoint disabled"; +} + +location /feed/playlist { + limit_req zone=invidious-feedplaylist nodelay burst=24; + try_files $uri @upstream; +} + +# location /api/v1/storyboards { +# try_files $uri @upstream; +# } +# +# location /api/v1/comments { +# try_files $uri @upstream; +# } + +# location ~ (^/api/v1/videos| { +# return 401 "API disabled"; +# } +# location /api/v1/channels { +# return 401 "API disabled"; +# } +# location /api/v1/search { +# return 401 "API disabled"; +# } +# location /api/v1/mixes { +# return 401 "API disabled"; +# } +# location /api/v1/trending { +# return 401 "API disabled"; +# } +# +# location /api/v1/videos { +# limit_req zone=invidious-apivideosrl nodelay burst=3; +# try_files $uri @upstream-api; +# if ($http_user_agent = "Mozilla/5.0") { +# return 444; +# } +# if ($http_user_agent ~* "python") { +# return 444; +# } +# } + +# location /api/v1/channels { +# limit_req zone=invidious-apichannelsrl nodelay burst=32; +# try_files $uri @upstream; +# } + +# +# location /api/v1/captions { +# try_files $uri @upstream; +# } +# + +#location ~ ^/api/v1/channels/(.+)/shorts { +# try_files $uri @upstream; +#} + +#location @fallback { +# root /etc/nginx/errors; +# try_files $uri /502.html = 502; +#} diff --git a/http.d/luna.conf b/http.d/luna.conf new file mode 100644 index 0000000..0e05f3b --- /dev/null +++ b/http.d/luna.conf @@ -0,0 +1,38 @@ +server { + access_log /var/log/nginx/luna.access.log; + error_log /var/log/nginx/luna.error.log; + server_name luna.nadeko.net; + include configs/listen.conf; + # index index.php /_h5ai/public/index.php; + # root /mnt/960gb_ssd/luna; + include configs/general.conf; + include configs/security.conf; + # default_type "application/octet-stream"; + + # location / { + # proxy_pass http://127.0.0.1:20001; + # } + + # location /_h5ai/private { + # return 403; + # } + # + # location ~ [^/]\.php(/|$) { + # fastcgi_split_path_info ^(.+?\.php)(/.*)$; + # if (!-f $document_root$fastcgi_script_name) { + # return 404; + # } + # fastcgi_param HTTP_PROXY ""; + # fastcgi_pass php-fpm-8.1; + # fastcgi_index index.php; + # include fastcgi_params; + # fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + # fastcgi_param PATH_INFO $fastcgi_path_info; + # add_header Access-Control-Allow-Origin *; + # } + + location / { + proxy_pass http://127.0.0.1:10008; + include configs/proxy.conf; + } +} diff --git a/http.d/matrix.conf b/http.d/matrix.conf new file mode 100644 index 0000000..a056181 --- /dev/null +++ b/http.d/matrix.conf @@ -0,0 +1,109 @@ +# server { +# server_name matrix.zzls.xyz; +# include configs/listen.conf; +# include configs/general.conf; +# include configs/robotsNone.conf; +# include configs/security.conf; +# +# location /.well-known/matrix/server { +# return 200 '{ "m.server": "matrix.zzls.xyz:8448" }'; +# } +# +# location /.well-known/matrix/client { +# default_type application/json; +# add_header Access-Control-Allow-Origin '*'; +# return 200 '{ "m.homeserver": { "base_url": "https://matrix.zzls.xyz" }, "org.matrix.msc3575.proxy": {"url": "https://matrix.zzls.xyz"}}'; +# } +# +# #location ~ ^/(client/|_matrix/client/unstable/org.matrix.msc3575/sync) { +# # proxy_pass http://127.0.0.1:40022; +# # proxy_set_header X-Forwarded-For $remote_addr; +# # proxy_set_header X-Forwarded-Proto $scheme; +# # proxy_set_header Host $host; +# #} +# +# location ~ ^(/_matrix|/_synapse/client|/health|/_synapse/metrics) { +# proxy_pass http://matrix; +# include configs/proxy.conf; +# client_max_body_size 64M; +# } +# +# # QUIC +# include configs/http3.conf; +# +# +# listen 8448 ssl; +# listen 8448 quic; +# +# } +# +server { + server_name matrix.nadeko.net; + include configs/listen.conf; + include configs/general.conf; + include configs/robotsNone.conf; + include configs/security.conf; + + location /.well-known/matrix/support { + default_type application/json; + add_header Access-Control-Allow-Origin '*'; + return 200 '{"contacts": [{"matrix_id": "@fijxu:nadeko.net","email_address": "admin@nadeko.net","role": "m.role.admin"},{"email_address": "admin@nadeko.net","role": "m.role.security"}],"support_page": "https://nadeko.net/contact" }'; + } + + location /.well-known/matrix/server { + default_type application/json; + add_header Access-Control-Allow-Origin '*'; + return 200 '{ "m.server": "matrix.nadeko.net:443" }'; + } + + location /.well-known/matrix/client { + default_type application/json; + add_header Access-Control-Allow-Origin '*'; + return 200 '{ "m.homeserver": { "base_url": "https://matrix.nadeko.net" }}'; + } + + location ~ ^(/_matrix|/_synapse/client|/health|/_synapse/metrics|/_synapse/admin) { + proxy_pass http://127.0.0.1:10022; + include configs/proxy.conf; + client_max_body_size 32M; + } + + location / { + proxy_pass http://127.0.0.1:10023; + include configs/proxy.conf; + client_max_body_size 32M; + } + + #listen 443 ssl reuseport; + #listen 8448 ssl default_server reuseport; + + # Port that is only accessed via netmaker, used to proxy the traffic to another + # server +} + +server { + server_name synapse-admin.nadeko.net; + include configs/listen.conf; + include configs/general.conf; + include configs/security.conf; + + location / { + proxy_pass http://127.0.0.1:10025; + include configs/proxy.conf; + } +} + +# server { +# server_name matrix-auth.nadeko.net; +# include configs/listen.conf; +# include configs/general.conf; +# include configs/security.conf; +# +# location / { +# proxy_pass http://127.0.0.1:10026; +# include configs/proxy.conf; +# } +# +# +# +# } diff --git a/http.d/pbin.conf b/http.d/pbin.conf new file mode 100644 index 0000000..dac6540 --- /dev/null +++ b/http.d/pbin.conf @@ -0,0 +1,16 @@ +# CLEARNET +server { + server_name + pbin.nadeko.net + pbin.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; + include configs/listen.conf; + include configs/general.conf; + include configs/security.conf; + include configs/robotsNone.conf; + client_max_body_size 128M; + + location / { + proxy_pass http://privatebin; + include configs/proxy.conf; + } +} diff --git a/http.d/redlib.conf b/http.d/redlib.conf new file mode 100644 index 0000000..b4973ab --- /dev/null +++ b/http.d/redlib.conf @@ -0,0 +1,23 @@ +server { + server_name + redlib.nadeko.net + redlib.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; + include configs/listen.conf; + include configs/general.conf; + # Security headers are added by redlib! + #include configs/security.conf; + include configs/robotsNone.conf; + + location / { + if ($blocked_agent = 1) { + return 200 " + 1. The Industrial Revolution and its consequences have been a disaster for the human race. They have greatly increased the life-expectancy of those of us who live in “advanced” countries, but they have destabilized society, have made life unfulfilling, have subjected human beings to indignities, have led to widespread psychological suffering (in the Third World to physical suffering as well) and have inflicted severe damage on the natural world. The continued development of technology will worsen the situation. It will certainly subject human beings to greater indignities and inflict greater damage on the natural world, it will probably lead to greater social disruption and psychological suffering, and it may lead to increased physical suffering even in “advanced” countries. + "; + } + proxy_pass http://redlib; + proxy_hide_header Strict-Transport-Security; + add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + include configs/proxy.conf; + } +} diff --git a/http.d/rimgo.conf b/http.d/rimgo.conf new file mode 100644 index 0000000..6c4f022 --- /dev/null +++ b/http.d/rimgo.conf @@ -0,0 +1,15 @@ +server { + server_name + ri.nadeko.net + ri.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; + include configs/listen.conf; + include configs/general.conf; + # Security headers added by rimgo! + #include configs/security.conf; + include configs/robots.conf; + + location / { + proxy_pass http://rimgo; + include configs/proxy.conf; + } +} diff --git a/lua/502-invidious.lua b/lua/502-invidious.lua new file mode 100644 index 0000000..b6df030 --- /dev/null +++ b/lua/502-invidious.lua @@ -0,0 +1,72 @@ +ngx.header.content_type = 'text/html'; +local backend_num = ngx.shared.servers:get("inv-backends"); +-- local backend_num = 4; + +local function generate_backend_list() + local html = "" + for i=1, backend_num do + html = html .. "

Backend " .. i .. "

" + end + return html +end + +ngx.say([[ + + + + + + + + + 502 Bad Gateway + + + +
+

502 Bad Gateway

+

Looks like you got a dead backend...

+

If you don't get an alive backend after 1 minute, it's because something is wrong...

+

Refreshing automatically in 2 seconds

+

Do you want to switch to another backend?

+ ]] .. generate_backend_list() .. [[ +
+

+ If this page doesn't redirect you to any alive backend + then there is no available backends alive. + +

+
+
+ + + +]]) diff --git a/lua/503-invidious.lua b/lua/503-invidious.lua new file mode 100644 index 0000000..8eadc40 --- /dev/null +++ b/lua/503-invidious.lua @@ -0,0 +1,72 @@ +ngx.header.content_type = 'text/html'; +-- local backend_num = ngx.shared.servers:get("invidious-servers"); +local backend_num = 4; + +local function generate_backend_list() + local html = "" + for i=1, backend_num do + html = html .. "

Backend " .. i .. "

" + end + return html +end + +ngx.say([[ + + + + + + + + + 503 Service Unavailable + + + +
+

503 Service Unavailable

+

Invidious is currently restarting

+

If you don't get an alive backend after 1 minute, it's because something is wrong...

+

Refreshing automatically in 2 seconds

+

Do you want to switch to another backend?

+ ]] .. generate_backend_list() .. [[ +
+

+ If this page doesn't redirect you to any alive backend + then there is no available backends alive. + +

+
+
+ + + +]]) diff --git a/lua/init.lua b/lua/init.lua new file mode 100644 index 0000000..070201f --- /dev/null +++ b/lua/init.lua @@ -0,0 +1,9 @@ +cs = require "crowdsec" + +local ok, err = cs.init("/etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf", "crowdsec-nginx-bouncer/v1.0.8") +if ok == nil then + ngx.log(ngx.ERR, "[Crowdsec] " .. err) + error() +end + +ngx.log(ngx.ALERT, "[Crowdsec] Initialisation done") diff --git a/lua/invidious-sticky.lua b/lua/invidious-sticky.lua new file mode 100644 index 0000000..ae9961c --- /dev/null +++ b/lua/invidious-sticky.lua @@ -0,0 +1,115 @@ +-- Based on https://github.com/Klaessen/openresty-loadbalancers/blob/main/sticky-balancer.lua + +local _M = {} + +local balancer = require "ngx.balancer" +local cookie_name = "INVIDIOUS_SERVER_ID" +local servers +local weighted_servers +local domain + +-- Generate a weighted server list based on weights +local function generate_weighted_server_list(servers) + local weighted_servers = {} + for _, server in ipairs(servers) do + for i = 1, server.weight do + table.insert(weighted_servers, server) + end + end + return weighted_servers +end + +-- Hash function to select server +local function hash(key, num_buckets) + local hash = ngx.crc32_long(key) + return (hash % num_buckets) + 1 +end + +-- Select server based on cookie or assign a new one +local function select_server() + local cookie = ngx.var["cookie_" .. cookie_name] + local server_index + + + math.randomseed(os.time()) + + if cookie then + server_index = tonumber(cookie) + ngx.header["X-Server-Id"] = server_index + else + server_index = math.random(#servers) + -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#partitioned + ngx.header["Set-Cookie"] = cookie_name .. "=" .. server_index .. "; domain=" .. domain .. "; Path=/; HttpOnly; SameSite=None; Secure; Partitioned" + ngx.header["X-Server-Id"] = server_index + end + + local server = weighted_servers[server_index] + return server +end + +function _M.run(upstreams) + domain = ".nadeko.net" + local host = ngx.req.get_headers()["Host"] + + -- TOR Support + if string.match(host, ".onion") then + domain = host + end + + servers = upstreams + weighted_servers = generate_weighted_server_list(servers) + local ok, err + local args = ngx.req.get_uri_args() + + if args then + for key, server_index in pairs(args) do + if key == "backend" then + server_index = tonumber(server_index) + + -- To redirect to another backend if user inputs a backend that doesn't exists + -- Ex: ?backend=5 will give you X-Server-Id=1 (Backend 1) + val = val % #servers + if val == 0 then + val = #servers + end + + ok, err = balancer.set_current_peer(servers[val][1], servers[val][2]) + + if not ok then + -- ngx.say("No peer available") + return ngx.exit(502) + end + + ngx.header["Set-Cookie"] = cookie_name .. "=" .. server_index .. "; domain=" .. domain .. "; Path=/; HttpOnly; SameSite=None; Secure; Partitioned" + ngx.header["X-Server-Id"] = server_index + end + end + end + + local server = select_server() + if not server then + -- ngx.say("No peer available") + return ngx.exit(502) + end + + if string.match(server[1], 'unix:') then + ok, err = balancer.set_current_peer(server[1]) + else + ok, err = balancer.set_current_peer(server[1], server[2]) + end + + if not ok then + ngx.log(ngx.ERR, "Failed to set the current peer: ", err) + -- ngx.say("Failed to set the current peer") + return ngx.exit(500) + end + + -- https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/balancer.md#enable_keepalive + ok, err = balancer.enable_keepalive(60, 1000) + if not ok then + ngx.log(ngx.ERR, "failed to set keepalive: ", err) + return + end +end + +return _M diff --git a/lua/plugins/crowdsec/crowdsec.lua b/lua/plugins/crowdsec/crowdsec.lua new file mode 100644 index 0000000..5b507c0 --- /dev/null +++ b/lua/plugins/crowdsec/crowdsec.lua @@ -0,0 +1,769 @@ +package.path = package.path .. ";./?.lua" + +local config = require "plugins.crowdsec.config" +local iputils = require "plugins.crowdsec.iputils" +local http = require "resty.http" +local cjson = require "cjson" +local captcha = require "plugins.crowdsec.captcha" +local flag = require "plugins.crowdsec.flag" +local utils = require "plugins.crowdsec.utils" +local ban = require "plugins.crowdsec.ban" +local url = require "plugins.crowdsec.url" +local bit +if _VERSION == "Lua 5.1" then bit = require "bit" else bit = require "bit32" end + +-- contain runtime = {} +local runtime = {} +-- remediations are stored in cache as int (shared dict tags) +-- we need to translate IDs to text with this. +runtime.remediations = {} +runtime.remediations["1"] = "ban" +runtime.remediations["2"] = "captcha" + + +runtime.timer_started = false + +local csmod = {} + +local PASSTHROUGH = "passthrough" +local DENY = "deny" + +local APPSEC_API_KEY_HEADER = "x-crowdsec-appsec-api-key" +local APPSEC_IP_HEADER = "x-crowdsec-appsec-ip" +local APPSEC_HOST_HEADER = "x-crowdsec-appsec-host" +local APPSEC_VERB_HEADER = "x-crowdsec-appsec-verb" +local APPSEC_URI_HEADER = "x-crowdsec-appsec-uri" +local APPSEC_USER_AGENT_HEADER = "x-crowdsec-appsec-user-agent" +local REMEDIATION_API_KEY_HEADER = 'x-api-key' + + +-- init function +function csmod.init(configFile, userAgent) + local conf, err = config.loadConfig(configFile) + if conf == nil then + return nil, err + end + runtime.conf = conf + runtime.userAgent = userAgent + runtime.cache = ngx.shared.crowdsec_cache + runtime.fallback = runtime.conf["FALLBACK_REMEDIATION"] + + if runtime.conf["ENABLED"] == "false" then + return "Disabled", nil + end + + if runtime.conf["REDIRECT_LOCATION"] == "/" then + ngx.log(ngx.ERR, "redirect location is set to '/' this will lead into infinite redirection") + end + + local captcha_ok = true + local err = captcha.New(runtime.conf["SITE_KEY"], runtime.conf["SECRET_KEY"], runtime.conf["CAPTCHA_TEMPLATE_PATH"], runtime.conf["CAPTCHA_PROVIDER"]) + if err ~= nil then + ngx.log(ngx.ERR, "error loading captcha plugin: " .. err) + captcha_ok = false + end + local succ, err, forcible = runtime.cache:set("captcha_ok", captcha_ok) + if not succ then + ngx.log(ngx.ERR, "failed to add captcha state key in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + + + local err = ban.new(runtime.conf["BAN_TEMPLATE_PATH"], runtime.conf["REDIRECT_LOCATION"], runtime.conf["RET_CODE"]) + if err ~= nil then + ngx.log(ngx.ERR, "error loading ban plugins: " .. err) + end + + if runtime.conf["REDIRECT_LOCATION"] ~= "" then + table.insert(runtime.conf["EXCLUDE_LOCATION"], runtime.conf["REDIRECT_LOCATION"]) + end + + if runtime.conf["SSL_VERIFY"] == "false" then + runtime.conf["SSL_VERIFY"] = false + else + runtime.conf["SSL_VERIFY"] = true + end + + if runtime.conf["ALWAYS_SEND_TO_APPSEC"] == "false" then + runtime.conf["ALWAYS_SEND_TO_APPSEC"] = false + else + runtime.conf["ALWAYS_SEND_TO_APPSEC"] = true + end + + runtime.conf["APPSEC_ENABLED"] = false + + if runtime.conf["APPSEC_URL"] ~= "" then + local u = url.parse(runtime.conf["APPSEC_URL"]) + runtime.conf["APPSEC_ENABLED"] = true + runtime.conf["APPSEC_HOST"] = u.host + if u.port ~= nil then + runtime.conf["APPSEC_HOST"] = runtime.conf["APPSEC_HOST"] .. ":" .. u.port + end + ngx.log(ngx.ERR, "APPSEC is enabled on '" .. runtime.conf["APPSEC_HOST"] .. "'") + end + + + -- if stream mode, add callback to stream_query and start timer + if runtime.conf["MODE"] == "stream" then + local succ, err, forcible = runtime.cache:set("startup", true) + if not succ then + ngx.log(ngx.ERR, "failed to add startup key in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + local succ, err, forcible = runtime.cache:set("first_run", true) + if not succ then + ngx.log(ngx.ERR, "failed to add first_run key in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + end + + if runtime.conf["API_URL"] == "" and runtime.conf["APPSEC_URL"] == "" then + ngx.log(ngx.ERR, "Neither API_URL or APPSEC_URL are defined, remediation component will not do anything") + end + + if runtime.conf["API_URL"] == "" and runtime.conf["APPSEC_URL"] ~= "" then + ngx.log(ngx.ERR, "Only APPSEC_URL is defined, local API decisions will be ignored") + end + + + + return true, nil +end + + +function csmod.validateCaptcha(captcha_res, remote_ip) + return captcha.ValidateMCaptcha(captcha_res, remote_ip) +end + + +local function get_remediation_http_request(link) + local httpc = http.new() + if runtime.conf['MODE'] == 'stream' then + httpc:set_timeout(runtime.conf['STREAM_REQUEST_TIMEOUT']) + else + httpc:set_timeout(runtime.conf['REQUEST_TIMEOUT']) + end + local res, err = httpc:request_uri(link, { + method = "GET", + headers = { + ['Connection'] = 'close', + [REMEDIATION_API_KEY_HEADER] = runtime.conf["API_KEY"], + ['User-Agent'] = runtime.userAgent + }, + ssl_verify = runtime.conf["SSL_VERIFY"] + }) + httpc:close() + return res, err +end + +local function parse_duration(duration) + local match, err = ngx.re.match(duration, "^((?[0-9]+)h)?((?[0-9]+)m)?(?[0-9]+)") + local ttl = 0 + if not match then + if err then + return ttl, err + end + end + if match["hours"] ~= nil and match["hours"] ~= false then + local hours = tonumber(match["hours"]) + ttl = ttl + (hours * 3600) + end + if match["minutes"] ~= nil and match["minutes"] ~= false then + local minutes = tonumber(match["minutes"]) + ttl = ttl + (minutes * 60) + end + if match["seconds"] ~= nil and match["seconds"] ~= false then + local seconds = tonumber(match["seconds"]) + ttl = ttl + seconds + end + return ttl, nil +end + +local function get_remediation_id(remediation) + for key, value in pairs(runtime.remediations) do + if value == remediation then + return tonumber(key) + end + end + return nil +end + +local function item_to_string(item, scope) + local ip, cidr, ip_version + if scope:lower() == "ip" then + ip = item + end + if scope:lower() == "range" then + ip, cidr = iputils.splitRange(item, scope) + end + + local ip_network_address, is_ipv4 = iputils.parseIPAddress(ip) + if ip_network_address == nil then + return nil + end + if is_ipv4 then + ip_version = "ipv4" + if cidr == nil then + cidr = 32 + end + else + ip_version = "ipv6" + ip_network_address = ip_network_address.uint32[3]..":"..ip_network_address.uint32[2]..":"..ip_network_address.uint32[1]..":"..ip_network_address.uint32[0] + if cidr == nil then + cidr = 128 + end + end + + if ip_version == nil then + return "normal_"..item + end + local ip_netmask = iputils.cidrToInt(cidr, ip_version) + return ip_version.."_"..ip_netmask.."_"..ip_network_address +end + +local function set_refreshing(value) + local succ, err, forcible = runtime.cache:set("refreshing", value) + if not succ then + error("Failed to set refreshing key in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end +end + +local function stream_query(premature) + -- As this function is running inside coroutine (with ngx.timer.at), + -- we need to raise error instead of returning them + + if runtime.conf["API_URL"] == "" then + return + end + + ngx.log(ngx.DEBUG, "running timers: " .. tostring(ngx.timer.running_count()) .. " | pending timers: " .. tostring(ngx.timer.pending_count())) + + if premature then + ngx.log(ngx.DEBUG, "premature run of the timer, returning") + return + end + + local refreshing = runtime.cache:get("refreshing") + + if refreshing == true then + ngx.log(ngx.DEBUG, "another worker is refreshing the data, returning") + local ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query) + if not ok then + error("Failed to create the timer: " .. (err or "unknown")) + end + return + end + + local last_refresh = runtime.cache:get("last_refresh") + if last_refresh ~= nil then + -- local last_refresh_time = tonumber(last_refresh) + local now = ngx.time() + if now - last_refresh < runtime.conf["UPDATE_FREQUENCY"] then + ngx.log(ngx.DEBUG, "last refresh was less than " .. runtime.conf["UPDATE_FREQUENCY"] .. " seconds ago, returning") + local ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query) + if not ok then + error("Failed to create the timer: " .. (err or "unknown")) + end + return + end + end + + set_refreshing(true) + + local is_startup = runtime.cache:get("startup") + ngx.log(ngx.DEBUG, "Stream Query from worker : " .. tostring(ngx.worker.id()) .. " with startup "..tostring(is_startup) .. " | premature: " .. tostring(premature)) + local link = runtime.conf["API_URL"] .. "/v1/decisions/stream?startup=" .. tostring(is_startup) + local res, err = get_remediation_http_request(link) + if not res then + local ok, err2 = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query) + if not ok then + set_refreshing(false) + error("Failed to create the timer: " .. (err2 or "unknown")) + end + set_refreshing(false) + error("request failed: ".. err) + end + + local succ, err, forcible = runtime.cache:set("last_refresh", ngx.time()) + if not succ then + error("Failed to set last_refresh key in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + + local status = res.status + local body = res.body + + ngx.log(ngx.DEBUG, "Response:" .. tostring(status) .. " | " .. tostring(body)) + + if status~=200 then + local ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query) + if not ok then + set_refreshing(false) + error("Failed to create the timer: " .. (err or "unknown")) + end + set_refreshing(false) + error("HTTP error while request to Local API '" .. status .. "' with message (" .. tostring(body) .. ")") + end + + local decisions = cjson.decode(body) + -- process deleted decisions + if type(decisions.deleted) == "table" then + for i, decision in pairs(decisions.deleted) do + if decision.type == "captcha" then + runtime.cache:delete("captcha_" .. decision.value) + end + local key = item_to_string(decision.value, decision.scope) + runtime.cache:delete(key) + ngx.log(ngx.DEBUG, "Deleting '" .. key .. "'") + end + end + + -- process new decisions + if type(decisions.new) == "table" then + for i, decision in pairs(decisions.new) do + if runtime.conf["BOUNCING_ON_TYPE"] == decision.type or runtime.conf["BOUNCING_ON_TYPE"] == "all" then + local ttl, err = parse_duration(decision.duration) + if err ~= nil then + ngx.log(ngx.ERR, "[Crowdsec] failed to parse ban duration '" .. decision.duration .. "' : " .. err) + end + local remediation_id = get_remediation_id(decision.type) + if remediation_id == nil then + remediation_id = get_remediation_id(runtime.fallback) + end + local key = item_to_string(decision.value, decision.scope) + local succ, err, forcible = runtime.cache:set(key, false, ttl, remediation_id) + if not succ then + ngx.log(ngx.ERR, "failed to add ".. decision.value .." : "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + ngx.log(ngx.DEBUG, "Adding '" .. key .. "' in cache for '" .. ttl .. "' seconds") + end + end + end + + -- not startup anymore after first callback + local succ, err, forcible = runtime.cache:set("startup", false) + if not succ then + ngx.log(ngx.ERR, "failed to set startup key in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + + + local ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query) + if not ok then + set_refreshing(false) + error("Failed to create the timer: " .. (err or "unknown")) + end + + set_refreshing(false) + ngx.log(ngx.DEBUG, "end of stream_query") + return nil +end + +local function live_query(ip) + if runtime.conf["API_URL"] == "" then + return true, nil, nil + end + local link = runtime.conf["API_URL"] .. "/v1/decisions?ip=" .. ip + local res, err = get_remediation_http_request(link) + if not res then + return true, nil, "request failed: ".. err + end + + local status = res.status + local body = res.body + if status~=200 then + return true, nil, "Http error " .. status .. " while talking to LAPI (" .. link .. ")" + end + if body == "null" then -- no result from API, no decision for this IP + -- set ip in cache and DON'T block it + local key = item_to_string(ip, "ip") + local succ, err, forcible = runtime.cache:set(key, true, runtime.conf["CACHE_EXPIRATION"], 1) + if not succ then + ngx.log(ngx.ERR, "failed to add ip '" .. ip .. "' in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + return true, nil, nil + end + local decision = cjson.decode(body)[1] + + if runtime.conf["BOUNCING_ON_TYPE"] == decision.type or runtime.conf["BOUNCING_ON_TYPE"] == "all" then + local remediation_id = get_remediation_id(decision.type) + if remediation_id == nil then + remediation_id = get_remediation_id(runtime.fallback) + end + local key = item_to_string(decision.value, decision.scope) + local succ, err, forcible = runtime.cache:set(key, false, runtime.conf["CACHE_EXPIRATION"], remediation_id) + if not succ then + ngx.log(ngx.ERR, "failed to add ".. decision.value .." : "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + ngx.log(ngx.DEBUG, "Adding '" .. key .. "' in cache for '" .. runtime.conf["CACHE_EXPIRATION"] .. "' seconds") + return false, decision.type, nil + else + return true, nil, nil + end +end + +local function get_body() + + -- the LUA module requires a content-length header to read a body for HTTP 2/3 requests, although it's not mandatory. + -- This means that we will likely miss body, but AFAIK, there's no workaround for this. + -- do not even try to read the body if there's no content-length as the LUA API will throw an error + if ngx.req.http_version() >= 2 and ngx.var.http_content_length == nil then + ngx.log(ngx.DEBUG, "No content-length header in request") + return nil + end + ngx.req.read_body() + local body = ngx.req.get_body_data() + if body == nil then + local bodyfile = ngx.req.get_body_file() + if bodyfile then + local fh, err = io.open(bodyfile, "r") + if fh then + body = fh:read("*a") + fh:close() + end + end + end + return body +end + +function csmod.GetCaptchaTemplate() + return captcha.GetTemplate() +end + +function csmod.GetCaptchaBackendKey() + return captcha.GetCaptchaBackendKey() +end + +function csmod.SetupStream() + -- if it stream mode and startup start timer + if runtime.conf["API_URL"] == "" then + return + end + ngx.log(ngx.DEBUG, "timer started: " .. tostring(runtime.timer_started) .. " in worker " .. tostring(ngx.worker.id())) + if runtime.timer_started == false and runtime.conf["MODE"] == "stream" then + local ok, err + ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query) + if not ok then + return true, nil, "Failed to create the timer: " .. (err or "unknown") + end + runtime.timer_started = true + ngx.log(ngx.DEBUG, "Timer launched") + end +end + +function csmod.allowIp(ip) + if runtime.conf == nil then + return true, nil, "Configuration is bad, cannot run properly" + end + + if runtime.conf["API_URL"] == "" then + return true, nil, nil + end + + csmod.SetupStream() + + local key = item_to_string(ip, "ip") + if key == nil then + return true, nil, "Check failed '" .. ip .. "' has no valid IP address" + end + local key_parts = {} + for i in key.gmatch(key, "([^_]+)") do + table.insert(key_parts, i) + end + + local key_type = key_parts[1] + if key_type == "normal" then + local in_cache, remediation_id = runtime.cache:get(key) + if in_cache ~= nil then -- we have it in cache + ngx.log(ngx.DEBUG, "'" .. key .. "' is in cache") + return in_cache, runtime.remediations[tostring(remediation_id)], nil + end + end + + local ip_network_address = key_parts[3] + local netmasks = iputils.netmasks_by_key_type[key_type] + for i, netmask in pairs(netmasks) do + local item + if key_type == "ipv4" then + item = key_type.."_"..netmask.."_"..iputils.ipv4_band(ip_network_address, netmask) + end + if key_type == "ipv6" then + item = key_type.."_"..table.concat(netmask, ":").."_"..iputils.ipv6_band(ip_network_address, netmask) + end + local in_cache, remediation_id = runtime.cache:get(item) + if in_cache ~= nil then -- we have it in cache + ngx.log(ngx.DEBUG, "'" .. key .. "' is in cache") + return in_cache, runtime.remediations[tostring(remediation_id)], nil + end + end + + -- if live mode, query lapi + if runtime.conf["MODE"] == "live" then + local ok, remediation, err = live_query(ip) + return ok, remediation, err + end + return true, nil, nil +end + + +function csmod.AppSecCheck(ip) + local httpc = http.new() + httpc:set_timeouts(runtime.conf["APPSEC_CONNECT_TIMEOUT"], runtime.conf["APPSEC_SEND_TIMEOUT"], runtime.conf["APPSEC_PROCESS_TIMEOUT"]) + + local uri = ngx.var.request_uri + local headers = ngx.req.get_headers() + + -- overwrite headers with crowdsec appsec require headers + headers[APPSEC_IP_HEADER] = ip + headers[APPSEC_HOST_HEADER] = ngx.var.http_host + headers[APPSEC_VERB_HEADER] = ngx.var.request_method + headers[APPSEC_URI_HEADER] = uri + headers[APPSEC_USER_AGENT_HEADER] = ngx.var.http_user_agent + headers[APPSEC_API_KEY_HEADER] = runtime.conf["API_KEY"] + + -- set CrowdSec APPSEC Host + headers["host"] = runtime.conf["APPSEC_HOST"] + + local ok, remediation, status_code = true, "allow", 200 + if runtime.conf["APPSEC_FAILURE_ACTION"] == DENY then + ok = false + remediation = runtime.conf["FALLBACK_REMEDIATION"] + end + + local method = "GET" + + local body = get_body() + if body ~= nil then + if #body > 0 then + method = "POST" + if headers["content-length"] == nil then + headers["content-length"] = tostring(#body) + end + end + else + headers["content-length"] = nil + end + + local res, err = httpc:request_uri(runtime.conf["APPSEC_URL"], { + method = method, + headers = headers, + body = body, + ssl_verify = runtime.conf["SSL_VERIFY"], + }) + httpc:close() + + if err ~= nil then + ngx.log(ngx.ERR, "Fallback because of err: " .. err) + return ok, remediation, status_code, err + end + + if res.status == 200 then + ok = true + remediation = "allow" + elseif res.status == 403 then + ok = false + ngx.log(ngx.DEBUG, "Appsec body response: " .. res.body) + local response = cjson.decode(res.body) + remediation = response.action + if response.http_status ~= nil then + ngx.log(ngx.DEBUG, "Got status code from APPSEC: " .. response.http_status) + status_code = response.http_status + else + status_code = ngx.HTTP_FORBIDDEN + end + elseif res.status == 401 then + ngx.log(ngx.ERR, "Unauthenticated request to APPSEC") + else + ngx.log(ngx.ERR, "Bad request to APPSEC (" .. res.status .. "): " .. res.body) + end + + return ok, remediation, status_code, err + +end + +function csmod.Allow(ip) + if runtime.conf["ENABLED"] == "false" then + ngx.exit(ngx.DECLINED) + end + + if runtime.conf["ENABLE_INTERNAL"] == "false" and ngx.req.is_internal() then + ngx.exit(ngx.DECLINED) + end + + local remediationSource = flag.BOUNCER_SOURCE + local ret_code = nil + + if utils.table_len(runtime.conf["EXCLUDE_LOCATION"]) > 0 then + for k, v in pairs(runtime.conf["EXCLUDE_LOCATION"]) do + if ngx.var.uri == v then + ngx.log(ngx.ERR, "whitelisted location: " .. v) + ngx.exit(ngx.DECLINED) + end + local uri_to_check = v + if utils.ends_with(uri_to_check, "/") == false then + uri_to_check = uri_to_check .. "/" + end + if utils.starts_with(ngx.var.uri, uri_to_check) then + ngx.log(ngx.ERR, "whitelisted location: " .. uri_to_check) + end + end + end + + local ok, remediation, err = csmod.allowIp(ip) + if err ~= nil then + ngx.log(ngx.ERR, "[Crowdsec] bouncer error: " .. err) + end + + -- if the ip is now allowed, try to delete its captcha state in cache + if ok == true then + ngx.shared.crowdsec_cache:delete("captcha_" .. ip) + end + + -- check with appSec if the remediation component doesn't have decisions for the IP + -- OR + -- that user configured the remediation component to always check on the appSec (even if there is a decision for the IP) + if ok == true or runtime.conf["ALWAYS_SEND_TO_APPSEC"] == true then + if runtime.conf["APPSEC_ENABLED"] == true and ngx.var.no_appsec ~= "1" then + local appsecOk, appsecRemediation, status_code, err = csmod.AppSecCheck(ip) + if err ~= nil then + ngx.log(ngx.ERR, "AppSec check: " .. err) + end + if appsecOk == false then + ok = false + remediationSource = flag.APPSEC_SOURCE + remediation = appsecRemediation + ret_code = status_code + end + end + end + + local captcha_ok = runtime.cache:get("captcha_ok") + + if runtime.fallback ~= "" then + -- if we can't use captcha, fallback + if remediation == "captcha" and captcha_ok == false then + remediation = runtime.fallback + end + + -- if remediation is not supported, fallback + if remediation ~= "captcha" and remediation ~= "ban" then + remediation = runtime.fallback + end + end + + if captcha_ok then -- if captcha can be use (configuration is valid) + -- we check if the IP need to validate its captcha before checking it against crowdsec local API + local previous_uri, flags = ngx.shared.crowdsec_cache:get("captcha_"..ip) + local source, state_id, err = flag.GetFlags(flags) + local body = get_body() + + -- nil body means it was likely not a post, abort here because the user hasn't provided a captcha solution + + if previous_uri ~= nil and state_id == flag.VERIFY_STATE and body ~= nil then + local captcha_res = ngx.req.get_post_args()[csmod.GetCaptchaBackendKey()] or 0 + if captcha_res ~= 0 then + local valid, err = csmod.validateCaptcha(captcha_res, ip) + if err ~= nil then + ngx.log(ngx.ERR, "Error while validating captcha: " .. err) + end + if valid == true then + -- if the captcha is valid and has been applied by the application security component + -- then we delete the state from the cache because from the bouncing part, if the user solve the captcha + -- we will not propose a captcha until the 'CAPTCHA_EXPIRATION'. + -- But for the Application security component, we serve the captcha each time the user trigger it. + if source == flag.APPSEC_SOURCE then + ngx.shared.crowdsec_cache:delete("captcha_"..ip) + else + local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ip, previous_uri, runtime.conf["CAPTCHA_EXPIRATION"], bit.bor(flag.VALIDATED_STATE, source) ) + if not succ then + ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ip .. "' in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + end + -- captcha is valid, we redirect the IP to its previous URI but in GET method + ngx.req.set_method(ngx.HTTP_GET) + if not ngx.var.query_string then + return ngx.redirect(previous_uri) + else + return ngx.redirect(previous_uri .. ngx.var.query_string) + end + else + ngx.log(ngx.ALERT, "Invalid captcha from " .. ip) + end + end + end + end + if not ok then + if remediation == "ban" then + ngx.log(ngx.ALERT, "[Crowdsec] denied '" .. ip .. "' with '"..remediation.."' (by " .. flag.Flags[remediationSource] .. ")") + ban.apply(ret_code) + return + end + -- if the remediation is a captcha and captcha is well configured + if remediation == "captcha" and captcha_ok and ngx.var.uri ~= "/favicon.ico" then + local previous_uri, flags = ngx.shared.crowdsec_cache:get("captcha_"..ip) + local source, state_id, err = flag.GetFlags(flags) + -- we check if the IP is already in cache for captcha and not yet validated + if previous_uri == nil or state_id ~= flag.VALIDATED_STATE or remediationSource == flag.APPSEC_SOURCE then + ngx.header.content_type = "text/html" + ngx.header.cache_control = "no-cache" + ngx.say(csmod.GetCaptchaTemplate()) + -- if not ngx.var.query_string then + local uri = ngx.var.uri + -- else + -- local uri = ngx.var.uri .. ngx.var.query_string + -- end + -- in case its not a GET request, we prefer to fallback on referer + if ngx.req.get_method() ~= "GET" then + local headers, err = ngx.req.get_headers() + for k, v in pairs(headers) do + if k == "referer" then + uri = v + end + end + end + local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ip, uri , 60, bit.bor(flag.VERIFY_STATE, remediationSource)) + if not succ then + ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ip .. "' in cache: "..err) + end + if forcible then + ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") + end + ngx.log(ngx.ALERT, "[Crowdsec] denied '" .. ip .. "' with '"..remediation.."'") + return + end + end + end + ngx.exit(ngx.DECLINED) +end + + +-- Use it if you are able to close at shuttime +function csmod.close() +end + +return csmod diff --git a/lua/plugins/crowdsec/plugins/crowdsec/ban.lua b/lua/plugins/crowdsec/plugins/crowdsec/ban.lua new file mode 100644 index 0000000..1dddf8b --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/ban.lua @@ -0,0 +1,77 @@ +local utils = require "plugins.crowdsec.utils" + + +local M = {_TYPE='module', _NAME='ban.funcs', _VERSION='1.0-0'} + +M.template_str = "" +M.redirect_location = "" +M.ret_code = ngx.HTTP_FORBIDDEN + + +function M.new(template_path, redirect_location, ret_code) + M.redirect_location = redirect_location + + ret_code_ok = false + if ret_code ~= nil and ret_code ~= 0 and ret_code ~= "" then + for k, v in pairs(utils.HTTP_CODE) do + if k == ret_code then + M.ret_code = utils.HTTP_CODE[ret_code] + ret_code_ok = true + break + end + end + if ret_code_ok == false then + ngx.log(ngx.ERR, "RET_CODE '" .. ret_code .. "' is not supported, using default HTTP code " .. M.ret_code) + end + end + + template_file_ok = false + if (template_path ~= nil and template_path ~= "" and utils.file_exist(template_path) == true) then + M.template_str = utils.read_file(template_path) + if M.template_str ~= nil then + template_file_ok = true + end + end + + if template_file_ok == false and (M.redirect_location == nil or M.redirect_location == "") then + ngx.log(ngx.ERR, "BAN_TEMPLATE_PATH and REDIRECT_LOCATION variable are empty, will return HTTP " .. M.ret_code .. " for ban decisions") + end + + return nil +end + + + +function M.apply(...) + local args = {...} + local ret_code = args[1] + + ngx.log(ngx.DEBUG, "args:" .. tostring(args[1])) + + local status = 0 + if ret_code ~= nil then + status = ret_code + else + status = M.ret_code + end + + ngx.log(ngx.DEBUG, "BAN: status=" .. status .. ", redirect_location=" .. M.redirect_location .. ", template_str=" .. M.template_str) + if M.redirect_location ~= "" then + ngx.redirect(M.redirect_location) + return + end + if M.template_str ~= "" then + ngx.header.content_type = "text/html" + ngx.header.cache_control = "no-cache" + ngx.status = status + ngx.say(M.template_str) + ngx.exit(status) + return + end + + ngx.exit(status) + + return +end + +return M \ No newline at end of file diff --git a/lua/plugins/crowdsec/plugins/crowdsec/bitop.lua b/lua/plugins/crowdsec/plugins/crowdsec/bitop.lua new file mode 100644 index 0000000..7adb111 --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/bitop.lua @@ -0,0 +1,344 @@ +-- +-- Library from https://github.com/AlberTajuelo/bitop-lua/ +-- + +local M = {_TYPE='module', _NAME='bitop.funcs', _VERSION='1.0-0'} + +local floor = math.floor + +local MOD = 2^32 +local MODM = MOD-1 + +local function memoize(f) + + local mt = {} + local t = setmetatable({}, mt) + + function mt:__index(k) + local v = f(k) + t[k] = v + return v + end + + return t +end + +local function make_bitop_uncached(t, m) + local function bitop(a, b) + local res,p = 0,1 + while a ~= 0 and b ~= 0 do + local am, bm = a%m, b%m + res = res + t[am][bm]*p + a = (a - am) / m + b = (b - bm) / m + p = p*m + end + res = res + (a+b) * p + return res + end + return bitop +end + +local function make_bitop(t) + local op1 = make_bitop_uncached(t, 2^1) + local op2 = memoize(function(a) + return memoize(function(b) + return op1(a, b) + end) + end) + return make_bitop_uncached(op2, 2^(t.n or 1)) +end + +-- ok? probably not if running on a 32-bit int Lua number type platform +function M.tobit(x) + return x % 2^32 +end + +M.bxor = make_bitop {[0]={[0]=0,[1]=1},[1]={[0]=1,[1]=0}, n=4} +local bxor = M.bxor + +function M.bnot(a) return MODM - a end +local bnot = M.bnot + +function M.band(a,b) return ((a+b) - bxor(a,b))/2 end +local band = M.band + +function M.bor(a,b) return MODM - band(MODM - a, MODM - b) end +local bor = M.bor + +local lshift, rshift -- forward declare + +function M.rshift(a,disp) -- Lua5.2 insipred + if disp < 0 then return lshift(a,-disp) end + return floor(a % 2^32 / 2^disp) +end +rshift = M.rshift + +function M.lshift(a,disp) -- Lua5.2 inspired + if disp < 0 then return rshift(a,-disp) end + return (a * 2^disp) % 2^32 +end +lshift = M.lshift + +function M.tohex(x, n) -- BitOp style + n = n or 8 + local up + if n <= 0 then + if n == 0 then return '' end + up = true + n = - n + end + x = band(x, 16^n-1) + return ('%0'..n..(up and 'X' or 'x')):format(x) +end +local tohex = M.tohex + +function M.extract(n, field, width) -- Lua5.2 inspired + width = width or 1 + return band(rshift(n, field), 2^width-1) +end +local extract = M.extract + +function M.replace(n, v, field, width) -- Lua5.2 inspired + width = width or 1 + local mask1 = 2^width-1 + v = band(v, mask1) -- required by spec? + local mask = bnot(lshift(mask1, field)) + return band(n, mask) + lshift(v, field) +end +local replace = M.replace + +function M.bswap(x) -- BitOp style + local a = band(x, 0xff); x = rshift(x, 8) + local b = band(x, 0xff); x = rshift(x, 8) + local c = band(x, 0xff); x = rshift(x, 8) + local d = band(x, 0xff) + return lshift(lshift(lshift(a, 8) + b, 8) + c, 8) + d +end +local bswap = M.bswap + +function M.rrotate(x, disp) -- Lua5.2 inspired + disp = disp % 32 + local low = band(x, 2^disp-1) + return rshift(x, disp) + lshift(low, 32-disp) +end +local rrotate = M.rrotate + +function M.lrotate(x, disp) -- Lua5.2 inspired + return rrotate(x, -disp) +end +local lrotate = M.lrotate + +M.rol = M.lrotate -- LuaOp inspired +M.ror = M.rrotate -- LuaOp insipred + + +function M.arshift(x, disp) -- Lua5.2 inspired + local z = rshift(x, disp) + if x >= 0x80000000 then z = z + lshift(2^disp-1, 32-disp) end + return z +end +local arshift = M.arshift + +function M.btest(x, y) -- Lua5.2 inspired + return band(x, y) ~= 0 +end + +-- +-- Start Lua 5.2 "bit32" compat section. +-- + +M.bit32 = {} -- Lua 5.2 'bit32' compatibility + + +local function bit32_bnot(x) + return (-1 - x) % MOD +end +M.bit32.bnot = bit32_bnot + +local function bit32_bxor(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = bxor(a, b) + if c then + z = bit32_bxor(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return 0 + end +end +M.bit32.bxor = bit32_bxor + +local function bit32_band(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = ((a+b) - bxor(a,b)) / 2 + if c then + z = bit32_band(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return MODM + end +end +M.bit32.band = bit32_band + +local function bit32_bor(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = MODM - band(MODM - a, MODM - b) + if c then + z = bit32_bor(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return 0 + end +end +M.bit32.bor = bit32_bor + +function M.bit32.btest(...) + return bit32_band(...) ~= 0 +end + +function M.bit32.lrotate(x, disp) + return lrotate(x % MOD, disp) +end + +function M.bit32.rrotate(x, disp) + return rrotate(x % MOD, disp) +end + +function M.bit32.lshift(x,disp) + if disp > 31 or disp < -31 then return 0 end + return lshift(x % MOD, disp) +end + +function M.bit32.rshift(x,disp) + if disp > 31 or disp < -31 then return 0 end + return rshift(x % MOD, disp) +end + +function M.bit32.arshift(x,disp) + x = x % MOD + if disp >= 0 then + if disp > 31 then + return (x >= 0x80000000) and MODM or 0 + else + local z = rshift(x, disp) + if x >= 0x80000000 then z = z + lshift(2^disp-1, 32-disp) end + return z + end + else + return lshift(x, -disp) + end +end + +function M.bit32.extract(x, field, ...) + local width = ... or 1 + if field < 0 or field > 31 or width < 0 or field+width > 32 then error 'out of range' end + x = x % MOD + return extract(x, field, ...) +end + +function M.bit32.replace(x, v, field, ...) + local width = ... or 1 + if field < 0 or field > 31 or width < 0 or field+width > 32 then error 'out of range' end + x = x % MOD + v = v % MOD + return replace(x, v, field, ...) +end + + +-- +-- Start LuaBitOp "bit" compat section. +-- + +M.bit = {} -- LuaBitOp "bit" compatibility + +function M.bit.tobit(x) + x = x % MOD + if x >= 0x80000000 then x = x - MOD end + return x +end +local bit_tobit = M.bit.tobit + +function M.bit.tohex(x, ...) + return tohex(x % MOD, ...) +end + +function M.bit.bnot(x) + return bit_tobit(bnot(x % MOD)) +end + +local function bit_bor(a, b, c, ...) + if c then + return bit_bor(bit_bor(a, b), c, ...) + elseif b then + return bit_tobit(bor(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.bor = bit_bor + +local function bit_band(a, b, c, ...) + if c then + return bit_band(bit_band(a, b), c, ...) + elseif b then + return bit_tobit(band(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.band = bit_band + +local function bit_bxor(a, b, c, ...) + if c then + return bit_bxor(bit_bxor(a, b), c, ...) + elseif b then + return bit_tobit(bxor(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.bxor = bit_bxor + +function M.bit.lshift(x, n) + return bit_tobit(lshift(x % MOD, n % 32)) +end + +function M.bit.rshift(x, n) + return bit_tobit(rshift(x % MOD, n % 32)) +end + +function M.bit.arshift(x, n) + return bit_tobit(arshift(x % MOD, n % 32)) +end + +function M.bit.rol(x, n) + return bit_tobit(lrotate(x % MOD, n % 32)) +end + +function M.bit.ror(x, n) + return bit_tobit(rrotate(x % MOD, n % 32)) +end + +function M.bit.bswap(x) + return bit_tobit(bswap(x % MOD)) +end + +return M \ No newline at end of file diff --git a/lua/plugins/crowdsec/plugins/crowdsec/captcha.lua b/lua/plugins/crowdsec/plugins/crowdsec/captcha.lua new file mode 100644 index 0000000..4530e6a --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/captcha.lua @@ -0,0 +1,308 @@ +-- local http = require "resty.http" +-- local cjson = require "cjson" +-- local template = require "plugins.crowdsec.template" +-- local utils = require "plugins.crowdsec.utils" +-- +-- local M = {_TYPE='module', _NAME='recaptcha.funcs', _VERSION='1.0-0'} +-- +-- local captcha_backend_url = {} +-- captcha_backend_url["recaptcha"] = "https://www.recaptcha.net/recaptcha/api/siteverify" +-- captcha_backend_url["hcaptcha"] = "https://hcaptcha.com/siteverify" +-- captcha_backend_url["turnstile"] = "https://challenges.cloudflare.com/turnstile/v0/siteverify" +-- captcha_backend_url["mcaptcha"] = "https://mcaptcha.nadeko.net/api/v1/pow/siteverify" +-- +-- local captcha_frontend_js = {} +-- captcha_frontend_js["recaptcha"] = "https://www.recaptcha.net/recaptcha/api.js" +-- captcha_frontend_js["hcaptcha"] = "https://js.hcaptcha.com/1/api.js" +-- captcha_frontend_js["turnstile"] = "https://challenges.cloudflare.com/turnstile/v0/api.js" +-- captcha_frontend_js["mcaptcha"] = "https://unpkg.com/@mcaptcha/vanilla-glue@0.1.0-rc2/dist/index.js" +-- +-- local captcha_frontend_key = {} +-- captcha_frontend_key["recaptcha"] = "g-recaptcha" +-- captcha_frontend_key["hcaptcha"] = "h-captcha" +-- captcha_frontend_key["turnstile"] = "cf-turnstile" +-- captcha_frontend_key["mcaptcha"] = "m-captcha" +-- +-- M.SecretKey = "" +-- M.SiteKey = "" +-- M.Template = "" +-- +-- function M.New(siteKey, secretKey, TemplateFilePath, captcha_provider) +-- +-- if siteKey == nil or siteKey == "" then +-- return "no recaptcha site key provided, can't use recaptcha" +-- end +-- M.SiteKey = siteKey +-- +-- if secretKey == nil or secretKey == "" then +-- return "no recaptcha secret key provided, can't use recaptcha" +-- end +-- +-- M.SecretKey = secretKey +-- +-- if TemplateFilePath == nil then +-- return "CAPTCHA_TEMPLATE_PATH variable is empty, will ban without template" +-- end +-- if utils.file_exist(TemplateFilePath) == false then +-- return "captcha template file doesn't exist, can't use recaptcha" +-- end +-- +-- local captcha_template = utils.read_file(TemplateFilePath) +-- if captcha_template == nil then +-- return "Template file " .. TemplateFilePath .. "not found." +-- end +-- +-- M.CaptchaProvider = captcha_provider +-- +-- local template_data = {} +-- template_data["captcha_site_key"] = M.SiteKey +-- template_data["captcha_frontend_js"] = captcha_frontend_js[M.CaptchaProvider] +-- template_data["captcha_frontend_key"] = captcha_frontend_key[M.CaptchaProvider] +-- local view = template.compile(captcha_template, template_data) +-- M.Template = view +-- +-- return nil +-- end +-- +-- +-- function M.GetTemplate() +-- return M.Template +-- end +-- +-- function M.GetCaptchaBackendKey() +-- -- return captcha_frontend_key[M.CaptchaProvider] .. "-response" +-- return "mcaptcha__token" +-- end +-- +-- function table_to_encoded_url(args) +-- local params = {} +-- for k, v in pairs(args) do table.insert(params, k .. '=' .. v) end +-- return table.concat(params, "&") +-- end +-- +-- function M.Validate(captcha_res, remote_ip) +-- local xd = "mcaptcha" +-- ngx.log(ngx.ERR, "xd: "..xd) +-- ngx.log(ngx.ERR, "MCaptchaProvider: "..M.CaptchaProvider) +-- -- if M.CaptchaProvider == xd then +-- ngx.log(ngx.ERR, "catpcha_res: "..captcha_res) +-- ngx.log(ngx.ERR, "M.SiteKey: "..M.SiteKey) +-- ngx.log(ngx.ERR, "M.SecretKey: "..M.SiteKey) +-- local body = { +-- token = captcha_res, +-- key = M.SiteKey, +-- secret = M.SecretKey +-- } +-- -- else +-- -- local body = { +-- -- secret = M.SecretKey, +-- -- response = captcha_res, +-- -- remoteip = remote_ip +-- -- } +-- -- end +-- +-- -- local data = table_to_encoded_url(body) +-- local data = cjson.encode(body) +-- -- ngx.log(ngx.ERR, "body: "..data) +-- local httpc = http.new() +-- httpc:set_timeout(2000) +-- -- if M.CaptchaProvider == xd then +-- local res, err = httpc:request_uri(captcha_backend_url[M.CaptchaProvider], { +-- method = "POST", +-- body = data, +-- headers = { +-- ["Content-Type"] = "application/json", +-- }, +-- }) +-- -- ngx.log(ngx.ERR, "response: "..res) +-- -- else +-- -- local res, err = httpc:request_uri(captcha_backend_url[M.CaptchaProvider], { +-- -- method = "POST", +-- -- body = data, +-- -- headers = { +-- -- ["Content-Type"] = "application/x-www-form-urlencoded", +-- -- }, +-- -- }) +-- -- end +-- -- ngx.log(ngx.ERR, res.body) +-- httpc:close() +-- if err ~= nil then +-- return true, err +-- end +-- +-- -- ngx.log(ngx.ALERT, M.CaptchaProvider) +-- local result = cjson.decode(res.body) +-- ngx.log(ngx.ALERT, result.valid) +-- +-- -- if result.success == false then +-- -- Disable this verification +-- -- if result.valid == false then +-- -- for k, v in pairs(result["error-codes"]) do +-- -- if v == "invalid-input-secret" then +-- -- ngx.log(ngx.ERR, "reCaptcha secret key is invalid") +-- -- return true, nil +-- -- end +-- -- end +-- -- end +-- +-- return result.valid, nil +-- end +-- +-- +-- return M + +local http = require "resty.http" +local cjson = require "cjson" +local template = require "plugins.crowdsec.template" +local utils = require "plugins.crowdsec.utils" + +local M = {_TYPE='module', _NAME='recaptcha.funcs', _VERSION='1.0-0'} + +local captcha_backend_url = {} +captcha_backend_url["recaptcha"] = "https://www.recaptcha.net/recaptcha/api/siteverify" +captcha_backend_url["hcaptcha"] = "https://hcaptcha.com/siteverify" +captcha_backend_url["turnstile"] = "https://challenges.cloudflare.com/turnstile/v0/siteverify" +captcha_backend_url["mcaptcha"] = "https://mcaptcha.nadeko.net/api/v1/pow/siteverify" + +local captcha_frontend_js = {} +captcha_frontend_js["recaptcha"] = "https://www.recaptcha.net/recaptcha/api.js" +captcha_frontend_js["hcaptcha"] = "https://js.hcaptcha.com/1/api.js" +captcha_frontend_js["turnstile"] = "https://challenges.cloudflare.com/turnstile/v0/api.js" +captcha_frontend_js["mcaptcha"] = "https://unpkg.com/@mcaptcha/vanilla-glue@0.1.0-rc2/dist/index.js" + +local captcha_frontend_key = {} +captcha_frontend_key["recaptcha"] = "g-recaptcha" +captcha_frontend_key["hcaptcha"] = "h-captcha" +captcha_frontend_key["turnstile"] = "cf-turnstile" +captcha_frontend_key["mcaptcha"] = "m-captcha" + +M.SecretKey = "" +M.SiteKey = "" +M.Template = "" + +function M.New(siteKey, secretKey, TemplateFilePath, captcha_provider) + + if siteKey == nil or siteKey == "" then + return "no recaptcha site key provided, can't use recaptcha" + end + M.SiteKey = siteKey + + if secretKey == nil or secretKey == "" then + return "no recaptcha secret key provided, can't use recaptcha" + end + + M.SecretKey = secretKey + + if TemplateFilePath == nil then + return "CAPTCHA_TEMPLATE_PATH variable is empty, will ban without template" + end + if utils.file_exist(TemplateFilePath) == false then + return "captcha template file doesn't exist, can't use recaptcha" + end + + local captcha_template = utils.read_file(TemplateFilePath) + if captcha_template == nil then + return "Template file " .. TemplateFilePath .. "not found." + end + + M.CaptchaProvider = captcha_provider + + local template_data = {} + template_data["captcha_site_key"] = M.SiteKey + template_data["captcha_frontend_js"] = captcha_frontend_js[M.CaptchaProvider] + template_data["captcha_frontend_key"] = captcha_frontend_key[M.CaptchaProvider] + local view = template.compile(captcha_template, template_data) + M.Template = view + + return nil +end + + +function M.GetTemplate() + return M.Template +end + +function M.GetCaptchaBackendKey() + -- return captcha_frontend_key[M.CaptchaProvider] .. "-response" + return "mcaptcha__token" +end + +function table_to_encoded_url(args) + local params = {} + for k, v in pairs(args) do table.insert(params, k .. '=' .. v) end + return table.concat(params, "&") +end + +function M.Validate(captcha_res, remote_ip) + local body = { + secret = M.SecretKey, + response = captcha_res, + remoteip = remote_ip + } + + local data = table_to_encoded_url(body) + local httpc = http.new() + httpc:set_timeout(2000) + local res, err = httpc:request_uri(captcha_backend_url[M.CaptchaProvider], { + method = "POST", + body = data, + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded", + }, + }) + httpc:close() + if err ~= nil then + return true, err + end + + local result = cjson.decode(res.body) + + if result.success == false then + for k, v in pairs(result["error-codes"]) do + if v == "invalid-input-secret" then + ngx.log(ngx.ERR, "reCaptcha secret key is invalid") + return true, nil + end + end + end + + return result.success, nil +end + +function M.ValidateMCaptcha(captcha_res, remote_ip) + local body = { + token = captcha_res, + key = M.SiteKey, + secret = M.SecretKey + } + + local data = cjson.encode(body) + local httpc = http.new() + httpc:set_timeout(2000) + local res, err = httpc:request_uri(captcha_backend_url[M.CaptchaProvider], { + method = "POST", + body = data, + headers = { + ["Content-Type"] = "application/json", + }, + }) + httpc:close() + if err ~= nil then + return true, err + end + + local result = cjson.decode(res.body) + + if result.error and result.error == "Account not found" then + ngx.log(ngx.ERR, "siteKey is not valid") + return true, nil + elseif result.error and result.error == "Wrong password" then + ngx.log(ngx.ERR, "secretKey is not valid") + return true, nil + end + + return result.valid, nil +end + +return M + diff --git a/lua/plugins/crowdsec/plugins/crowdsec/config.lua b/lua/plugins/crowdsec/plugins/crowdsec/config.lua new file mode 100644 index 0000000..11cb32f --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/config.lua @@ -0,0 +1,136 @@ +local config = {} + +function config.file_exists(file) + local f = io.open(file, "rb") + if f then + f:close() + end + return f ~= nil +end + + function split(s, delimiter) + result = {}; + for match in (s..delimiter):gmatch("(.-)"..delimiter.."(.-)") do + table.insert(result, match); + end + return result; +end + +local function has_value (tab, val) + for index, value in ipairs(tab) do + if value == val then + return true + end + end + + return false +end + +local function starts_with(str, start) + return str:sub(1, #start) == start +end + +local function trim(s) + return (string.gsub(s, "^%s*(.-)%s*$", "%1")) +end + +function config.loadConfig(file) + if not config.file_exists(file) then + return nil, "File ".. file .." doesn't exist" + end + local conf = {} + local valid_params = {'ENABLED','API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE', 'SECRET_KEY', 'SITE_KEY', 'BAN_TEMPLATE_PATH' ,'CAPTCHA_TEMPLATE_PATH', 'REDIRECT_LOCATION', 'RET_CODE', 'EXCLUDE_LOCATION', 'FALLBACK_REMEDIATION', 'CAPTCHA_PROVIDER', 'APPSEC_URL', 'APPSEC_FAILURE_ACTION', 'ALWAYS_SEND_TO_APPSEC', 'SSL_VERIFY'} + local valid_int_params = {'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY', 'CAPTCHA_EXPIRATION', 'APPSEC_CONNECT_TIMEOUT', 'APPSEC_SEND_TIMEOUT', 'APPSEC_PROCESS_TIMEOUT', 'STREAM_REQUEST_TIMEOUT'} + local valid_bouncing_on_type_values = {'ban', 'captcha', 'all'} + local valid_truefalse_values = {'false', 'true'} + local default_values = { + ['ENABLED'] = "true", + ['API_URL'] = "", + ['REQUEST_TIMEOUT'] = 500, + ['STREAM_REQUEST_TIMEOUT'] = 15000, + ['BOUNCING_ON_TYPE'] = "ban", + ['MODE'] = "stream", + ['UPDATE_FREQUENCY'] = 10, + ['CAPTCHA_EXPIRATION'] = 3600, + ['REDIRECT_LOCATION'] = "", + ['EXCLUDE_LOCATION'] = {}, + ['RET_CODE'] = 0, + ['CAPTCHA_PROVIDER'] = "recaptcha", + ['APPSEC_URL'] = "", + ['APPSEC_CONNECT_TIMEOUT'] = 100, + ['APPSEC_SEND_TIMEOUT'] = 100, + ['APPSEC_PROCESS_TIMEOUT'] = 500, + ['APPSEC_FAILURE_ACTION'] = "passthrough", + ['SSL_VERIFY'] = "true", + ['ALWAYS_SEND_TO_APPSEC'] = "false", + + } + for line in io.lines(file) do + local isOk = false + if starts_with(line, "#") then + isOk = true + end + if trim(line) == "" then + isOk = true + end + if not isOk then + local sep_pos = line:find("=") + if not sep_pos then + ngx.log(ngx.ERR, "invalid configuration line: " .. line) + break + end + local key = trim(line:sub(1, sep_pos - 1)) + local value = trim(line:sub(sep_pos + 1)) + if has_value(valid_params, key) then + if key == "ENABLED" then + if not has_value(valid_truefalse_values, value) then + ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'true' instead") + value = "true" + end + elseif key == "BOUNCING_ON_TYPE" then + if not has_value(valid_bouncing_on_type_values, value) then + ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'ban' instead") + value = "ban" + end + elseif key == "SSL_VERIFY" then + if not has_value(valid_truefalse_values, value) then + ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'true' instead") + value = "true" + end + elseif key == "MODE" then + if not has_value({'stream', 'live'}, value) then + ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'stream' instead") + value = "stream" + end + elseif key == "EXCLUDE_LOCATION" then + exclude_location = {} + if value ~= "" then + for match in (value..","):gmatch("(.-)"..",") do + table.insert(exclude_location, match) + end + end + value = exclude_location + elseif key == "FALLBACK_REMEDIATION" then + if not has_value({'captcha', 'ban'}, value) then + ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'ban' instead") + value = "ban" + end + end + + conf[key] = value + + elseif has_value(valid_int_params, key) then + conf[key] = tonumber(value) + else + ngx.log(ngx.ERR, "unsupported configuration '" .. key .. "'") + end + end + end + for k, v in pairs(default_values) do + if conf[k] == nil then + conf[k] = v + end + end + return conf, nil +end +return config \ No newline at end of file diff --git a/lua/plugins/crowdsec/plugins/crowdsec/flag.lua b/lua/plugins/crowdsec/plugins/crowdsec/flag.lua new file mode 100644 index 0000000..63ce8c9 --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/flag.lua @@ -0,0 +1,43 @@ +local bit +if _VERSION == "Lua 5.1" then bit = require "bit" else bit = require "bit32" end + +local M = {_TYPE='module', _NAME='flag.funcs', _VERSION='1.0-0'} + +M.BOUNCER_SOURCE = 0x1 +M.APPSEC_SOURCE = 0x2 +M.VERIFY_STATE = 0x4 +M.VALIDATED_STATE = 0x8 + +M.Flags = {} +M.Flags[0x0] = "" +M.Flags[0x1] = "bouncer" +M.Flags[0x2] = "appsec" +M.Flags[0x4] = "to_verify" +M.Flags[0x8] = "validated" + + +function M.GetFlags(flags) + local source = 0x0 + local err = "" + local state = 0x0 + + if flags == nil then + return source, state, err + end + + if bit.band(flags, M.BOUNCER_SOURCE) ~= 0 then + source = M.BOUNCER_SOURCE + elseif bit.band(flags, M.APPSEC_SOURCE) ~= 0 then + source = M.APPSEC_SOURCE + end + + if bit.band(flags, M.VERIFY_STATE) ~= 0 then + state = M.VERIFY_STATE + elseif bit.band(flags, M.VALIDATED_STATE) ~= 0 then + state = M.VALIDATED_STATE + end + return source, state, err + +end + +return M diff --git a/lua/plugins/crowdsec/plugins/crowdsec/iputils.lua b/lua/plugins/crowdsec/plugins/crowdsec/iputils.lua new file mode 100644 index 0000000..5ba6458 --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/iputils.lua @@ -0,0 +1,215 @@ +-- This code uses some functions from +-- https://github.com/libmoon/libmoon/blob/master/lua/utils.lua + +local _M = {} +local bit = require 'plugins.crowdsec.bitop' +local bor, band, rshift, lshift, bswap = bit.bor, bit.band, bit.rshift, bit.lshift, bit.bswap + +--- Byte swap for 16 bit integers +--- @param n 16 bit integer +--- @return Byte swapped integer +function bswap16(n) + return bor(rshift(n, 8), lshift(band(n, 0xFF), 8)) +end + +hton16 = bswap16 +ntoh16 = hton16 + +_G.bswap = bswap -- export bit.bswap to global namespace to be consistent with bswap16 +hton = bswap +ntoh = hton + +local ffi = require "ffi" + +local ipv4_netmasks = { + 4294967295, 4294967294, 4294967292, 4294967288, 4294967280, 4294967264, 4294967232, 4294967168, 4294967040, 4294966784, + 4294966272, 4294965248, 4294963200, 4294959104, 4294950912, 4294934528, 4294901760, 4294836224, 4294705152, 4294443008, + 4293918720, 4292870144, 4290772992, 4286578688, 4278190080, 4261412864, 4227858432, 4160749568, 4026531840, 3758096384, 3221225472, 2147483648, 0 +} + +local ipv6_netmasks = { + {4294967295,4294967295,4294967295,4294967295}, {4294967295,4294967295,4294967295,4294967294}, {4294967295,4294967295,4294967295,4294967292}, + {4294967295,4294967295,4294967295,4294967288}, {4294967295,4294967295,4294967295,4294967280}, {4294967295,4294967295,4294967295,4294967264}, + {4294967295,4294967295,4294967295,4294967232}, {4294967295,4294967295,4294967295,4294967168}, {4294967295,4294967295,4294967295,4294967040}, + {4294967295,4294967295,4294967295,4294966784}, {4294967295,4294967295,4294967295,4294966272}, {4294967295,4294967295,4294967295,4294965248}, + {4294967295,4294967295,4294967295,4294963200}, {4294967295,4294967295,4294967295,4294959104}, {4294967295,4294967295,4294967295,4294950912}, + {4294967295,4294967295,4294967295,4294934528}, {4294967295,4294967295,4294967295,4294901760}, {4294967295,4294967295,4294967295,4294836224}, + {4294967295,4294967295,4294967295,4294705152}, {4294967295,4294967295,4294967295,4294443008}, {4294967295,4294967295,4294967295,4293918720}, + {4294967295,4294967295,4294967295,4292870144}, {4294967295,4294967295,4294967295,4290772992}, {4294967295,4294967295,4294967295,4286578688}, + {4294967295,4294967295,4294967295,4278190080}, {4294967295,4294967295,4294967295,4261412864}, {4294967295,4294967295,4294967295,4227858432}, + {4294967295,4294967295,4294967295,4160749568}, {4294967295,4294967295,4294967295,4026531840}, {4294967295,4294967295,4294967295,3758096384}, + {4294967295,4294967295,4294967295,3221225472}, {4294967295,4294967295,4294967295,2147483648}, {4294967295,4294967295,4294967295,0}, + {4294967295,4294967295,4294967294,0}, {4294967295,4294967295,4294967292,0}, {4294967295,4294967295,4294967288,0}, + {4294967295,4294967295,4294967280,0}, {4294967295,4294967295,4294967264,0}, {4294967295,4294967295,4294967232,0}, + {4294967295,4294967295,4294967168,0}, {4294967295,4294967295,4294967040,0}, {4294967295,4294967295,4294966784,0}, + {4294967295,4294967295,4294966272,0}, {4294967295,4294967295,4294965248,0}, {4294967295,4294967295,4294963200,0}, + {4294967295,4294967295,4294959104,0}, {4294967295,4294967295,4294950912,0}, {4294967295,4294967295,4294934528,0}, + {4294967295,4294967295,4294901760,0}, {4294967295,4294967295,4294836224,0}, {4294967295,4294967295,4294705152,0}, + {4294967295,4294967295,4294443008,0}, {4294967295,4294967295,4293918720,0}, {4294967295,4294967295,4292870144,0}, + {4294967295,4294967295,4290772992,0}, {4294967295,4294967295,4286578688,0}, {4294967295,4294967295,4278190080,0}, + {4294967295,4294967295,4261412864,0}, {4294967295,4294967295,4227858432,0}, {4294967295,4294967295,4160749568,0}, + {4294967295,4294967295,4026531840,0}, {4294967295,4294967295,3758096384,0}, {4294967295,4294967295,3221225472,0}, + {4294967295,4294967295,2147483648,0}, + {4294967295,4294967295,0,0}, {4294967295,4294967294,0,0}, {4294967295,4294967292,0,0}, {4294967295,4294967288,0,0}, + {4294967295,4294967280,0,0}, {4294967295,4294967264,0,0}, {4294967295,4294967232,0,0}, {4294967295,4294967168,0,0}, + {4294967295,4294967040,0,0}, {4294967295,4294966784,0,0}, {4294967295,4294966272,0,0}, {4294967295,4294965248,0,0}, + {4294967295,4294963200,0,0}, {4294967295,4294959104,0,0}, {4294967295,4294950912,0,0}, {4294967295,4294934528,0,0}, + {4294967295,4294901760,0,0}, {4294967295,4294836224,0,0}, {4294967295,4294705152,0,0}, {4294967295,4294443008,0,0}, + {4294967295,4293918720,0,0}, {4294967295,4292870144,0,0}, {4294967295,4290772992,0,0}, {4294967295,4286578688,0,0}, + {4294967295,4278190080,0,0}, {4294967295,4261412864,0,0}, {4294967295,4227858432,0,0}, {4294967295,4160749568,0,0}, + {4294967295,4026531840,0,0}, {4294967295,3758096384,0,0}, {4294967295,3221225472,0,0}, {4294967295,2147483648,0,0}, + {4294967295,0,0,0},{4294967294,0,0,0},{4294967292,0,0,0}, {4294967288,0,0,0}, {4294967280,0,0,0},{4294967264,0,0,0}, + {4294967232,0,0,0},{4294967168,0,0,0},{4294967040,0,0,0}, {4294966784,0,0,0},{4294966272,0,0,0},{4294965248,0,0,0}, + {4294963200,0,0,0},{4294959104,0,0,0},{4294950912,0,0,0}, {4294934528,0,0,0},{4294901760,0,0,0},{4294836224,0,0,0}, + {4294705152,0,0,0},{4294443008,0,0,0},{4293918720,0,0,0}, {4292870144,0,0,0},{4290772992,0,0,0},{4286578688,0,0,0}, + {4278190080,0,0,0},{4261412864,0,0,0},{4227858432,0,0,0}, {4160749568,0,0,0},{4026531840,0,0,0},{3758096384,0,0,0}, + {3221225472,0,0,0},{2147483648,0,0,0},{0,0,0,0} +} + +local netmasks_by_key_type = {} +netmasks_by_key_type["ipv4"] = ipv4_netmasks +netmasks_by_key_type["ipv6"] = ipv6_netmasks + +_M.netmasks_by_key_type = netmasks_by_key_type + +function _M.ipToInt( str ) + local num = 0 + if str and type(str)=="string" then + local o1,o2,o3,o4 = str:match("(%d+)%.(%d+)%.(%d+)%.(%d+)" ) + num = 2^24*o1 + 2^16*o2 + 2^8*o3 + o4 + end + return num +end + +function _M.concatIPv6(ip) + if type(ip) == "table" and table.getn(ip) == 4 then + return ip[1]*2^96 + ip[2]*2^64 + ip[3]*2^32 + ip[4] + end + return nil +end + +function _M.ipv6_band(ip, netmask) + local res_table = {} + local nb = 1 + for item in ip.gmatch(ip, "([^:]+)") do + table.insert(res_table, bit.band(tonumber(item), netmask[nb])) + nb = nb + 1 + end + return table.concat(res_table, ":") +end + +function _M.ipv4_band(ip, netmask) + return bit.band(ip, netmask) +end + +function _M.splitRange(range) + if range and type(range) == "string" then + local ip_address, cidr = range:match("^([^/]+)/(%d+)") + return ip_address, tonumber(cidr) + end + return nil, nil +end + +function _M.cidrToInt(cidr, ip_version) + if cidr and type(cidr) ~= "number" and ip_version and type(ip_version) ~= "string" then + return nil + end + if ip_version == "ipv4" then + return tostring(ipv4_netmasks[32-cidr+1]) + end + if ip_version == "ipv6" then + return table.concat(ipv6_netmasks[128-cidr+1], ":") + end +end + +--- Parse a string to an IP address +--- @return address ip address in ip4_address or ip6_address format or nil if invalid address +--- @return boolean true if IPv4 address, false otherwise +function _M.parseIPAddress(ip) + ip = tostring(ip) + local address = parseIP4Address(ip) + if address == nil then + return parseIP6Address(ip), false + end + return address, true +end + +--- Parse a string to an IPv4 address +--- @param ip address in string format +--- @return address in uint32 format or nil if invalid address +function parseIP4Address(ip) + ip = tostring(ip) + local bytes = {string.match(ip, '(%d+)%.(%d+)%.(%d+)%.(%d+)')} + if bytes == nil then + return + end + for i = 1, 4 do + if bytes[i] == nil then + return + end + bytes[i] = tonumber(bytes[i]) + if bytes[i] < 0 or bytes[i] > 255 then + return + end + end + + -- build a uint32 + ip = bytes[1] + for i = 2, 4 do + ip = bor(lshift(ip, 8), bytes[i]) + end + return ip +end + +ffi.cdef[[ + union ip6_address { + uint8_t uint8[16]; + uint32_t uint32[4]; + uint64_t uint64[2]; + }; +]] + +ffi.cdef[[ +int inet_pton(int af, const char *src, void *dst); +]] + +--- Parse a string to an IPv6 address +--- @param ip address in string format +--- @return address in ip6_address format or nil if invalid address +function parseIP6Address(ip) + ip = tostring(ip) + local LINUX_AF_INET6 = 10 --preprocessor constant of Linux + local tmp_addr = ffi.new("union ip6_address") + local res = ffi.C.inet_pton(LINUX_AF_INET6, ip, tmp_addr) + if res == 0 then + return nil + end + + local addr = ffi.new("union ip6_address") + addr.uint32[0] = bswap(tmp_addr.uint32[3]) + addr.uint32[1] = bswap(tmp_addr.uint32[2]) + addr.uint32[2] = bswap(tmp_addr.uint32[1]) + addr.uint32[3] = bswap(tmp_addr.uint32[0]) + + return addr +end + +--- Merge tables. +--- @param args Arbitrary amount of tables to get merged. +function mergeTables(...) + local table = {} + if select("#", ...) > 0 then + table = select(1, ...) + for i = 2, select("#", ...) do + for k,v in pairs(select(i, ...)) do + table[k] = v + end + end + end + return table +end + +local band = bit.band +local sar = bit.arshift + +return _M \ No newline at end of file diff --git a/lua/plugins/crowdsec/plugins/crowdsec/template.lua b/lua/plugins/crowdsec/plugins/crowdsec/template.lua new file mode 100644 index 0000000..535abb2 --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/template.lua @@ -0,0 +1,13 @@ +local template = {} + +function template.compile(template_str, args) + + for k, v in pairs(args) do + local var = "{{" .. k .. "}}" + template_str = template_str:gsub(var, v) + end + + return template_str +end + +return template \ No newline at end of file diff --git a/lua/plugins/crowdsec/plugins/crowdsec/url.lua b/lua/plugins/crowdsec/plugins/crowdsec/url.lua new file mode 100644 index 0000000..ef66209 --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/url.lua @@ -0,0 +1,523 @@ +-- net/url.lua - a robust url parser and builder +-- +-- Bertrand Mansion, 2011-2021; License MIT +-- @module net.url +-- @alias M + +local M = {} +M.version = "1.1.0" + +--- url options +-- - `separator` is set to `&` by default but could be anything like `&amp;` or `;` +-- - `cumulative_parameters` is false by default. If true, query parameters with the same name will be stored in a table. +-- - `legal_in_path` is a table of characters that will not be url encoded in path components +-- - `legal_in_query` is a table of characters that will not be url encoded in query values. Query parameters only support a small set of legal characters (-_.). +-- - `query_plus_is_space` is true by default, so a plus sign in a query value will be converted to %20 (space), not %2B (plus) +-- @todo Add option to limit the size of the argument table +-- @todo Add option to limit the depth of the argument table +-- @todo Add option to process dots in parameter names, ie. `param.filter=1` +M.options = { + separator = '&', + cumulative_parameters = false, + legal_in_path = { + [":"] = true, ["-"] = true, ["_"] = true, ["."] = true, + ["!"] = true, ["~"] = true, ["*"] = true, ["'"] = true, + ["("] = true, [")"] = true, ["@"] = true, ["&"] = true, + ["="] = true, ["$"] = true, [","] = true, + [";"] = true + }, + legal_in_query = { + [":"] = true, ["-"] = true, ["_"] = true, ["."] = true, + [","] = true, ["!"] = true, ["~"] = true, ["*"] = true, + ["'"] = true, [";"] = true, ["("] = true, [")"] = true, + ["@"] = true, ["$"] = true, + }, + query_plus_is_space = true +} + +--- list of known and common scheme ports +-- as documented in IANA URI scheme list +M.services = { + acap = 674, + cap = 1026, + dict = 2628, + ftp = 21, + gopher = 70, + http = 80, + https = 443, + iax = 4569, + icap = 1344, + imap = 143, + ipp = 631, + ldap = 389, + mtqp = 1038, + mupdate = 3905, + news = 2009, + nfs = 2049, + nntp = 119, + rtsp = 554, + sip = 5060, + snmp = 161, + telnet = 23, + tftp = 69, + vemmi = 575, + afs = 1483, + jms = 5673, + rsync = 873, + prospero = 191, + videotex = 516 +} + +local function decode(str) + return (str:gsub("%%(%x%x)", function(c) + return string.char(tonumber(c, 16)) + end)) +end + +local function encode(str, legal) + return (str:gsub("([^%w])", function(v) + if legal[v] then + return v + end + return string.upper(string.format("%%%02x", string.byte(v))) + end)) +end + +-- for query values, + can mean space if configured as such +local function decodeValue(str) + if M.options.query_plus_is_space then + str = str:gsub('+', ' ') + end + return decode(str) +end + +local function concat(a, b) + if type(a) == 'table' then + return a:build() .. b + else + return a .. b:build() + end +end + +function M:addSegment(path) + if type(path) == 'string' then + self.path = self.path .. '/' .. encode(path:gsub("^/+", ""), M.options.legal_in_path) + end + return self +end + +--- builds the url +-- @return a string representing the built url +function M:build() + local url = '' + if self.path then + local path = self.path + url = url .. tostring(path) + end + if self.query then + local qstring = tostring(self.query) + if qstring ~= "" then + url = url .. '?' .. qstring + end + end + if self.host then + local authority = self.host + if self.port and self.scheme and M.services[self.scheme] ~= self.port then + authority = authority .. ':' .. self.port + end + local userinfo + if self.user and self.user ~= "" then + userinfo = self.user + if self.password then + userinfo = userinfo .. ':' .. self.password + end + end + if userinfo and userinfo ~= "" then + authority = userinfo .. '@' .. authority + end + if authority then + if url ~= "" then + url = '//' .. authority .. '/' .. url:gsub('^/+', '') + else + url = '//' .. authority + end + end + end + if self.scheme then + url = self.scheme .. ':' .. url + end + if self.fragment then + url = url .. '#' .. self.fragment + end + return url +end + +--- builds the querystring +-- @param tab The key/value parameters +-- @param sep The separator to use (optional) +-- @param key The parent key if the value is multi-dimensional (optional) +-- @return a string representing the built querystring +function M.buildQuery(tab, sep, key) + local query = {} + if not sep then + sep = M.options.separator or '&' + end + local keys = {} + for k in pairs(tab) do + keys[#keys+1] = k + end + table.sort(keys, function (a, b) + local function padnum(n, rest) return ("%03d"..rest):format(tonumber(n)) end + return tostring(a):gsub("(%d+)(%.)",padnum) < tostring(b):gsub("(%d+)(%.)",padnum) + end) + for _,name in ipairs(keys) do + local value = tab[name] + name = encode(tostring(name), {["-"] = true, ["_"] = true, ["."] = true}) + if key then + if M.options.cumulative_parameters and string.find(name, '^%d+$') then + name = tostring(key) + else + name = string.format('%s[%s]', tostring(key), tostring(name)) + end + end + if type(value) == 'table' then + query[#query+1] = M.buildQuery(value, sep, name) + else + local value = encode(tostring(value), M.options.legal_in_query) + if value ~= "" then + query[#query+1] = string.format('%s=%s', name, value) + else + query[#query+1] = name + end + end + end + return table.concat(query, sep) +end + +--- Parses the querystring to a table +-- This function can parse multidimensional pairs and is mostly compatible +-- with PHP usage of brackets in key names like ?param[key]=value +-- @param str The querystring to parse +-- @param sep The separator between key/value pairs, defaults to `&` +-- @todo limit the max number of parameters with M.options.max_parameters +-- @return a table representing the query key/value pairs +function M.parseQuery(str, sep) + if not sep then + sep = M.options.separator or '&' + end + + local values = {} + for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do + local key = decodeValue(key) + local keys = {} + key = key:gsub('%[([^%]]*)%]', function(v) + -- extract keys between balanced brackets + if string.find(v, "^-?%d+$") then + v = tonumber(v) + else + v = decodeValue(v) + end + table.insert(keys, v) + return "=" + end) + key = key:gsub('=+.*$', "") + key = key:gsub('%s', "_") -- remove spaces in parameter name + val = val:gsub('^=+', "") + + if not values[key] then + values[key] = {} + end + if #keys > 0 and type(values[key]) ~= 'table' then + values[key] = {} + elseif #keys == 0 and type(values[key]) == 'table' then + values[key] = decodeValue(val) + elseif M.options.cumulative_parameters + and type(values[key]) == 'string' then + values[key] = { values[key] } + table.insert(values[key], decodeValue(val)) + end + + local t = values[key] + for i,k in ipairs(keys) do + if type(t) ~= 'table' then + t = {} + end + if k == "" then + k = #t+1 + end + if not t[k] then + t[k] = {} + end + if i == #keys then + t[k] = val + end + t = t[k] + end + + end + setmetatable(values, { __tostring = M.buildQuery }) + return values +end + +--- set the url query +-- @param query Can be a string to parse or a table of key/value pairs +-- @return a table representing the query key/value pairs +function M:setQuery(query) + local query = query + if type(query) == 'table' then + query = M.buildQuery(query) + end + self.query = M.parseQuery(query) + return query +end + +--- set the authority part of the url +-- The authority is parsed to find the user, password, port and host if available. +-- @param authority The string representing the authority +-- @return a string with what remains after the authority was parsed +function M:setAuthority(authority) + self.authority = authority + self.port = nil + self.host = nil + self.userinfo = nil + self.user = nil + self.password = nil + + authority = authority:gsub('^([^@]*)@', function(v) + self.userinfo = v + return '' + end) + + authority = authority:gsub(':(%d+)$', function(v) + self.port = tonumber(v) + return '' + end) + + local function getIP(str) + -- ipv4 + local chunks = { str:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") } + if #chunks == 4 then + for _, v in pairs(chunks) do + if tonumber(v) > 255 then + return false + end + end + return str + end + -- ipv6 + local chunks = { str:match("^%["..(("([a-fA-F0-9]*):"):rep(8):gsub(":$","%%]$"))) } + if #chunks == 8 or #chunks < 8 and + str:match('::') and not str:gsub("::", "", 1):match('::') then + for _,v in pairs(chunks) do + if #v > 0 and tonumber(v, 16) > 65535 then + return false + end + end + return str + end + return nil + end + + local ip = getIP(authority) + if ip then + self.host = ip + elseif type(ip) == 'nil' then + -- domain + if authority ~= '' and not self.host then + local host = authority:lower() + if string.match(host, '^[%d%a%-%.]+$') ~= nil and + string.sub(host, 0, 1) ~= '.' and + string.sub(host, -1) ~= '.' and + string.find(host, '%.%.') == nil then + self.host = host + end + end + end + + if self.userinfo then + local userinfo = self.userinfo + userinfo = userinfo:gsub(':([^:]*)$', function(v) + self.password = v + return '' + end) + if string.find(userinfo, "^[%w%+%.]+$") then + self.user = userinfo + else + -- incorrect userinfo + self.userinfo = nil + self.user = nil + self.password = nil + end + end + + return authority +end + +--- Parse the url into the designated parts. +-- Depending on the url, the following parts can be available: +-- scheme, userinfo, user, password, authority, host, port, path, +-- query, fragment +-- @param url Url string +-- @return a table with the different parts and a few other functions +function M.parse(url) + local comp = {} + M.setAuthority(comp, "") + M.setQuery(comp, "") + + local url = tostring(url or '') + url = url:gsub('#(.*)$', function(v) + comp.fragment = v + return '' + end) + url =url:gsub('^([%w][%w%+%-%.]*)%:', function(v) + comp.scheme = v:lower() + return '' + end) + url = url:gsub('%?(.*)', function(v) + M.setQuery(comp, v) + return '' + end) + url = url:gsub('^//([^/]*)', function(v) + M.setAuthority(comp, v) + return '' + end) + + comp.path = url:gsub("([^/]+)", function (s) return encode(decode(s), M.options.legal_in_path) end) + + setmetatable(comp, { + __index = M, + __tostring = M.build, + __concat = concat, + __div = M.addSegment + }) + return comp +end + +--- removes dots and slashes in urls when possible +-- This function will also remove multiple slashes +-- @param path The string representing the path to clean +-- @return a string of the path without unnecessary dots and segments +function M.removeDotSegments(path) + local fields = {} + if string.len(path) == 0 then + return "" + end + local startslash = false + local endslash = false + if string.sub(path, 1, 1) == "/" then + startslash = true + end + if (string.len(path) > 1 or startslash == false) and string.sub(path, -1) == "/" then + endslash = true + end + + path:gsub('[^/]+', function(c) table.insert(fields, c) end) + + local new = {} + local j = 0 + + for i,c in ipairs(fields) do + if c == '..' then + if j > 0 then + j = j - 1 + end + elseif c ~= "." then + j = j + 1 + new[j] = c + end + end + local ret = "" + if #new > 0 and j > 0 then + ret = table.concat(new, '/', 1, j) + else + ret = "" + end + if startslash then + ret = '/'..ret + end + if endslash then + ret = ret..'/' + end + return ret +end + +local function reducePath(base_path, relative_path) + if string.sub(relative_path, 1, 1) == "/" then + return '/' .. string.gsub(relative_path, '^[%./]+', '') + end + local path = base_path + local startslash = string.sub(path, 1, 1) ~= "/"; + if relative_path ~= "" then + path = (startslash and '' or '/') .. path:gsub("[^/]*$", "") + end + path = path .. relative_path + path = path:gsub("([^/]*%./)", function (s) + if s ~= "./" then return s else return "" end + end) + path = string.gsub(path, "/%.$", "/") + local reduced + while reduced ~= path do + reduced = path + path = string.gsub(reduced, "([^/]*/%.%./)", function (s) + if s ~= "../../" then return "" else return s end + end) + end + path = string.gsub(path, "([^/]*/%.%.?)$", function (s) + if s ~= "../.." then return "" else return s end + end) + local reduced + while reduced ~= path do + reduced = path + path = string.gsub(reduced, '^/?%.%./', '') + end + return (startslash and '' or '/') .. path +end + +--- builds a new url by using the one given as parameter and resolving paths +-- @param other A string or a table representing a url +-- @return a new url table +function M:resolve(other) + if type(self) == "string" then + self = M.parse(self) + end + if type(other) == "string" then + other = M.parse(other) + end + if other.scheme then + return other + else + other.scheme = self.scheme + if not other.authority or other.authority == "" then + other:setAuthority(self.authority) + if not other.path or other.path == "" then + other.path = self.path + local query = other.query + if not query or not next(query) then + other.query = self.query + end + else + other.path = reducePath(self.path, other.path) + end + end + return other + end +end + +--- normalize a url path following some common normalization rules +-- described on The URL normalization page of Wikipedia +-- @return the normalized path +function M:normalize() + if type(self) == 'string' then + self = M.parse(self) + end + if self.path then + local path = self.path + path = reducePath(path, "") + -- normalize multiple slashes + path = string.gsub(path, "//+", "/") + self.path = path + end + return self +end + +return M \ No newline at end of file diff --git a/lua/plugins/crowdsec/plugins/crowdsec/utils.lua b/lua/plugins/crowdsec/plugins/crowdsec/utils.lua new file mode 100644 index 0000000..0665c4c --- /dev/null +++ b/lua/plugins/crowdsec/plugins/crowdsec/utils.lua @@ -0,0 +1,57 @@ +local M = {} + + +M.HTTP_CODE = {} +M.HTTP_CODE["200"] = ngx.HTTP_OK +M.HTTP_CODE["202"] = ngx.HTTP_ACCEPTED +M.HTTP_CODE["204"] = ngx.HTTP_NO_CONTENT +M.HTTP_CODE["301"] = ngx.HTTP_MOVED_PERMANENTLY +M.HTTP_CODE["302"] = ngx.HTTP_MOVED_TEMPORARILY +M.HTTP_CODE["400"] = ngx.HTTP_BAD_REQUEST +M.HTTP_CODE["401"] = ngx.HTTP_UNAUTHORIZED +M.HTTP_CODE["401"] = ngx.HTTP_UNAUTHORIZED +M.HTTP_CODE["403"] = ngx.HTTP_FORBIDDEN +M.HTTP_CODE["404"] = ngx.HTTP_NOT_FOUND +M.HTTP_CODE["405"] = ngx.HTTP_NOT_ALLOWED +M.HTTP_CODE["406"] = ngx.HTTP_NOT_ACCEPTABLE +M.HTTP_CODE["500"] = ngx.HTTP_INTERNAL_SERVER_ERROR + +function M.read_file(path) + local file = io.open(path, "r") -- r read mode and b binary mode + if not file then return nil end + io.input(file) + local content = io.read("*a") + io.close(file) + return content + end + +function M.file_exist(path) + if path == nil then + return nil + end + local f = io.open(path, "r") + if f ~= nil then + io.close(f) + return true + else + return false + end +end + +function M.starts_with(str, start) + return str:sub(1, #start) == start + end + + function M.ends_with(str, ending) + return ending == "" or str:sub(-#ending) == ending + end + +function M.table_len(table) + local count = 0 + for k, v in pairs(table) do + count = count + 1 + end + return count +end + +return M \ No newline at end of file diff --git a/lua/plugins/crowdsec/resty/http.lua b/lua/plugins/crowdsec/resty/http.lua new file mode 100644 index 0000000..a85f85a --- /dev/null +++ b/lua/plugins/crowdsec/resty/http.lua @@ -0,0 +1,1185 @@ +local http_headers = require "resty.http_headers" + +local ngx = ngx +local ngx_socket_tcp = ngx.socket.tcp +local ngx_req = ngx.req +local ngx_req_socket = ngx_req.socket +local ngx_req_get_headers = ngx_req.get_headers +local ngx_req_get_method = ngx_req.get_method +local str_lower = string.lower +local str_upper = string.upper +local str_find = string.find +local str_sub = string.sub +local tbl_concat = table.concat +local tbl_insert = table.insert +local ngx_encode_args = ngx.encode_args +local ngx_re_match = ngx.re.match +local ngx_re_gmatch = ngx.re.gmatch +local ngx_re_sub = ngx.re.sub +local ngx_re_gsub = ngx.re.gsub +local ngx_re_find = ngx.re.find +local ngx_log = ngx.log +local ngx_DEBUG = ngx.DEBUG +local ngx_ERR = ngx.ERR +local ngx_var = ngx.var +local ngx_print = ngx.print +local ngx_header = ngx.header +local co_yield = coroutine.yield +local co_create = coroutine.create +local co_status = coroutine.status +local co_resume = coroutine.resume +local setmetatable = setmetatable +local tonumber = tonumber +local tostring = tostring +local unpack = unpack +local rawget = rawget +local select = select +local ipairs = ipairs +local pairs = pairs +local pcall = pcall +local type = type + + +-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 +local HOP_BY_HOP_HEADERS = { + ["connection"] = true, + ["keep-alive"] = true, + ["proxy-authenticate"] = true, + ["proxy-authorization"] = true, + ["te"] = true, + ["trailers"] = true, + ["transfer-encoding"] = true, + ["upgrade"] = true, + ["content-length"] = true, -- Not strictly hop-by-hop, but Nginx will deal + -- with this (may send chunked for example). +} + + +local EXPECTING_BODY = { + POST = true, + PUT = true, + PATCH = true, +} + + +-- Reimplemented coroutine.wrap, returning "nil, err" if the coroutine cannot +-- be resumed. This protects user code from infinite loops when doing things like +-- repeat +-- local chunk, err = res.body_reader() +-- if chunk then -- <-- This could be a string msg in the core wrap function. +-- ... +-- end +-- until not chunk +local co_wrap = function(func) + local co = co_create(func) + if not co then + return nil, "could not create coroutine" + else + return function(...) + if co_status(co) == "suspended" then + return select(2, co_resume(co, ...)) + else + return nil, "can't resume a " .. co_status(co) .. " coroutine" + end + end + end +end + + +-- Returns a new table, recursively copied from the one given. +-- +-- @param table table to be copied +-- @return table +local function tbl_copy(orig) + local orig_type = type(orig) + local copy + if orig_type == "table" then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[tbl_copy(orig_key)] = tbl_copy(orig_value) + end + else -- number, string, boolean, etc + copy = orig + end + return copy +end + + +local _M = { + _VERSION = '0.17.2', +} +_M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version + +local mt = { __index = _M } + + +local HTTP = { + [1.0] = " HTTP/1.0\r\n", + [1.1] = " HTTP/1.1\r\n", +} + + +local DEFAULT_PARAMS = { + method = "GET", + path = "/", + version = 1.1, +} + + +local DEBUG = false + + +function _M.new(_) + local sock, err = ngx_socket_tcp() + if not sock then + return nil, err + end + return setmetatable({ sock = sock, keepalive = true }, mt) +end + + +function _M.debug(d) + DEBUG = (d == true) +end + + +function _M.set_timeout(self, timeout) + local sock = self.sock + if not sock then + return nil, "not initialized" + end + + return sock:settimeout(timeout) +end + + +function _M.set_timeouts(self, connect_timeout, send_timeout, read_timeout) + local sock = self.sock + if not sock then + return nil, "not initialized" + end + + return sock:settimeouts(connect_timeout, send_timeout, read_timeout) +end + +do + local aio_connect = require "resty.http_connect" + -- Function signatures to support: + -- ok, err, ssl_session = httpc:connect(options_table) + -- ok, err = httpc:connect(host, port, options_table?) + -- ok, err = httpc:connect("unix:/path/to/unix.sock", options_table?) + function _M.connect(self, options, ...) + if type(options) == "table" then + -- all-in-one interface + return aio_connect(self, options) + else + -- backward compatible + return self:tcp_only_connect(options, ...) + end + end +end + +function _M.tcp_only_connect(self, ...) + ngx_log(ngx_DEBUG, "Use of deprecated `connect` method signature") + + local sock = self.sock + if not sock then + return nil, "not initialized" + end + + self.host = select(1, ...) + self.port = select(2, ...) + + -- If port is not a number, this is likely a unix domain socket connection. + if type(self.port) ~= "number" then + self.port = nil + end + + self.keepalive = true + self.ssl = false + + return sock:connect(...) +end + + +function _M.set_keepalive(self, ...) + local sock = self.sock + if not sock then + return nil, "not initialized" + end + + if self.keepalive == true then + return sock:setkeepalive(...) + else + -- The server said we must close the connection, so we cannot setkeepalive. + -- If close() succeeds we return 2 instead of 1, to differentiate between + -- a normal setkeepalive() failure and an intentional close(). + local res, err = sock:close() + if res then + return 2, "connection must be closed" + else + return res, err + end + end +end + + +function _M.get_reused_times(self) + local sock = self.sock + if not sock then + return nil, "not initialized" + end + + return sock:getreusedtimes() +end + + +function _M.close(self) + local sock = self.sock + if not sock then + return nil, "not initialized" + end + + return sock:close() +end + + +local function _should_receive_body(method, code) + if method == "HEAD" then return nil end + if code == 204 or code == 304 then return nil end + if code >= 100 and code < 200 then return nil end + return true +end + + +function _M.parse_uri(_, uri, query_in_path) + if query_in_path == nil then query_in_path = true end + + local m, err = ngx_re_match( + uri, + [[^(?:(http[s]?):)?//((?:[^\[\]:/\?]+)|(?:\[.+\]))(?::(\d+))?([^\?]*)\??(.*)]], + "jo" + ) + + if not m then + if err then + return nil, "failed to match the uri: " .. uri .. ", " .. err + end + + return nil, "bad uri: " .. uri + else + -- If the URI is schemaless (i.e. //example.com) try to use our current + -- request scheme. + if not m[1] then + -- Schema-less URIs can occur in client side code, implying "inherit + -- the schema from the current request". We support it for a fairly + -- specific case; if for example you are using the ESI parser in + -- ledge (https://github.com/ledgetech/ledge) to perform in-flight + -- sub requests on the edge based on instructions found in markup, + -- those URIs may also be schemaless with the intention that the + -- subrequest would inherit the schema just like JavaScript would. + local scheme = ngx_var.scheme + if scheme == "http" or scheme == "https" then + m[1] = scheme + else + return nil, "schemaless URIs require a request context: " .. uri + end + end + + if m[3] then + m[3] = tonumber(m[3]) + else + if m[1] == "https" then + m[3] = 443 + else + m[3] = 80 + end + end + if not m[4] or "" == m[4] then m[4] = "/" end + + if query_in_path and m[5] and m[5] ~= "" then + m[4] = m[4] .. "?" .. m[5] + m[5] = nil + end + + return m, nil + end +end + + +local function _format_request(self, params) + local version = params.version + local headers = params.headers or {} + + local query = params.query or "" + if type(query) == "table" then + query = ngx_encode_args(query) + end + + if query ~= "" and str_sub(query, 1, 1) ~= "?" then + query = "?" .. query + end + + -- Initialize request + local req = { + str_upper(params.method), + " ", + self.path_prefix or "", + params.path, + query, + HTTP[version], + -- Pre-allocate slots for minimum headers and carriage return. + true, + true, + true, + } + local c = 7 -- req table index it's faster to do this inline vs table.insert + + -- Append headers + for key, values in pairs(headers) do + key = tostring(key) + + if type(values) == "table" then + for _, value in pairs(values) do + req[c] = key .. ": " .. tostring(value) .. "\r\n" + c = c + 1 + end + + else + req[c] = key .. ": " .. tostring(values) .. "\r\n" + c = c + 1 + end + end + + -- Close headers + req[c] = "\r\n" + + return tbl_concat(req) +end + + +local function _receive_status(sock) + local line, err = sock:receive("*l") + if not line then + return nil, nil, nil, err + end + + local version = tonumber(str_sub(line, 6, 8)) + if not version then + return nil, nil, nil, + "couldn't parse HTTP version from response status line: " .. line + end + + local status = tonumber(str_sub(line, 10, 12)) + if not status then + return nil, nil, nil, + "couldn't parse status code from response status line: " .. line + end + + local reason = str_sub(line, 14) + + return status, version, reason +end + + +local function _receive_headers(sock) + local headers = http_headers.new() + + repeat + local line, err = sock:receive("*l") + if not line then + return nil, err + end + + local m, err = ngx_re_match(line, "([^:\\s]+):\\s*(.*)", "jo") + if err then ngx_log(ngx_ERR, err) end + + if not m then + break + end + + local key = m[1] + local val = m[2] + if headers[key] then + if type(headers[key]) ~= "table" then + headers[key] = { headers[key] } + end + tbl_insert(headers[key], tostring(val)) + else + headers[key] = tostring(val) + end + until ngx_re_find(line, "^\\s*$", "jo") + + return headers, nil +end + + +local function transfer_encoding_is_chunked(headers) + local te = headers["Transfer-Encoding"] + if not te then + return false + end + + -- Handle duplicate headers + -- This shouldn't happen but can in the real world + if type(te) ~= "string" then + te = tbl_concat(te, ",") + end + + return str_find(str_lower(te), "chunked", 1, true) ~= nil +end +_M.transfer_encoding_is_chunked = transfer_encoding_is_chunked + + +local function _chunked_body_reader(sock, default_chunk_size) + return co_wrap(function(max_chunk_size) + local remaining = 0 + local length + max_chunk_size = max_chunk_size or default_chunk_size + + repeat + -- If we still have data on this chunk + if max_chunk_size and remaining > 0 then + + if remaining > max_chunk_size then + -- Consume up to max_chunk_size + length = max_chunk_size + remaining = remaining - max_chunk_size + else + -- Consume all remaining + length = remaining + remaining = 0 + end + else -- This is a fresh chunk + + -- Receive the chunk size + local str, err = sock:receive("*l") + if not str then + co_yield(nil, err) + end + + length = tonumber(str, 16) + + if not length then + co_yield(nil, "unable to read chunksize") + end + + if max_chunk_size and length > max_chunk_size then + -- Consume up to max_chunk_size + remaining = length - max_chunk_size + length = max_chunk_size + end + end + + if length > 0 then + local str, err = sock:receive(length) + if not str then + co_yield(nil, err) + end + + max_chunk_size = co_yield(str) or default_chunk_size + + -- If we're finished with this chunk, read the carriage return. + if remaining == 0 then + sock:receive(2) -- read \r\n + end + else + -- Read the last (zero length) chunk's carriage return + sock:receive(2) -- read \r\n + end + + until length == 0 + end) +end + + +local function _body_reader(sock, content_length, default_chunk_size) + return co_wrap(function(max_chunk_size) + max_chunk_size = max_chunk_size or default_chunk_size + + if not content_length and max_chunk_size then + -- We have no length, but wish to stream. + -- HTTP 1.0 with no length will close connection, so read chunks to the end. + repeat + local str, err, partial = sock:receive(max_chunk_size) + if not str and err == "closed" then + co_yield(partial, err) + end + + max_chunk_size = tonumber(co_yield(str) or default_chunk_size) + if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end + + if not max_chunk_size then + ngx_log(ngx_ERR, "Buffer size not specified, bailing") + break + end + until not str + + elseif not content_length then + -- We have no length but don't wish to stream. + -- HTTP 1.0 with no length will close connection, so read to the end. + co_yield(sock:receive("*a")) + + elseif not max_chunk_size then + -- We have a length and potentially keep-alive, but want everything. + co_yield(sock:receive(content_length)) + + else + -- We have a length and potentially a keep-alive, and wish to stream + -- the response. + local received = 0 + repeat + local length = max_chunk_size + if received + length > content_length then + length = content_length - received + end + + if length > 0 then + local str, err = sock:receive(length) + if not str then + co_yield(nil, err) + end + received = received + length + + max_chunk_size = tonumber(co_yield(str) or default_chunk_size) + if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end + + if not max_chunk_size then + ngx_log(ngx_ERR, "Buffer size not specified, bailing") + break + end + end + + until length == 0 + end + end) +end + + +local function _no_body_reader() + return nil +end + + +local function _read_body(res) + local reader = res.body_reader + + if not reader then + -- Most likely HEAD or 304 etc. + return nil, "no body to be read" + end + + local chunks = {} + local c = 1 + + local chunk, err + repeat + chunk, err = reader() + + if err then + return nil, err, tbl_concat(chunks) -- Return any data so far. + end + if chunk then + chunks[c] = chunk + c = c + 1 + end + until not chunk + + return tbl_concat(chunks) +end + + +local function _trailer_reader(sock) + return co_wrap(function() + co_yield(_receive_headers(sock)) + end) +end + + +local function _read_trailers(res) + local reader = res.trailer_reader + if not reader then + return nil, "no trailers" + end + + local trailers = reader() + setmetatable(res.headers, { __index = trailers }) +end + + +local function _send_body(sock, body) + if type(body) == "function" then + repeat + local chunk, err, partial = body() + + if chunk then + local ok, err = sock:send(chunk) + + if not ok then + return nil, err + end + elseif err ~= nil then + return nil, err, partial + end + + until chunk == nil + elseif body ~= nil then + local bytes, err = sock:send(body) + + if not bytes then + return nil, err + end + end + return true, nil +end + + +local function _handle_continue(sock, body) + local status, version, reason, err = _receive_status(sock) --luacheck: no unused + if not status then + return nil, nil, nil, err + end + + -- Only send body if we receive a 100 Continue + if status == 100 then + -- Read headers + local headers, err = _receive_headers(sock) + if not headers then + return nil, nil, nil, err + end + + local ok, err = _send_body(sock, body) + if not ok then + return nil, nil, nil, err + end + end + return status, version, reason, err +end + + +function _M.send_request(self, params) + -- Apply defaults + setmetatable(params, { __index = DEFAULT_PARAMS }) + + local sock = self.sock + local body = params.body + local headers = http_headers.new() + + -- We assign one-by-one so that the metatable can handle case insensitivity + -- for us. You can blame the spec for this inefficiency. + local params_headers = params.headers or {} + for k, v in pairs(params_headers) do + headers[k] = v + end + + if not headers["Proxy-Authorization"] then + -- TODO: next major, change this to always override the provided + -- header. Can't do that yet because it would be breaking. + -- The connect method uses self.http_proxy_auth in the poolname so + -- that should be leading. + headers["Proxy-Authorization"] = self.http_proxy_auth + end + + -- Ensure we have appropriate message length or encoding. + do + local is_chunked = transfer_encoding_is_chunked(headers) + + if is_chunked then + -- If we have both Transfer-Encoding and Content-Length we MUST + -- drop the Content-Length, to help prevent request smuggling. + -- https://tools.ietf.org/html/rfc7230#section-3.3.3 + headers["Content-Length"] = nil + + elseif not headers["Content-Length"] then + -- A length was not given, try to calculate one. + + local body_type = type(body) + + if body_type == "function" then + return nil, "Request body is a function but a length or chunked encoding is not specified" + + elseif body_type == "table" then + local length = 0 + for _, v in ipairs(body) do + length = length + #tostring(v) + end + headers["Content-Length"] = length + + elseif body == nil and EXPECTING_BODY[str_upper(params.method)] then + headers["Content-Length"] = 0 + + elseif body ~= nil then + headers["Content-Length"] = #tostring(body) + end + end + end + + if not headers["Host"] then + if (str_sub(self.host, 1, 5) == "unix:") then + return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one." + end + -- If we have a port (i.e. not connected to a unix domain socket), and this + -- port is non-standard, append it to the Host header. + if self.port then + if self.ssl and self.port ~= 443 then + headers["Host"] = self.host .. ":" .. self.port + elseif not self.ssl and self.port ~= 80 then + headers["Host"] = self.host .. ":" .. self.port + else + headers["Host"] = self.host + end + else + headers["Host"] = self.host + end + end + if not headers["User-Agent"] then + headers["User-Agent"] = _M._USER_AGENT + end + if params.version == 1.0 and not headers["Connection"] then + headers["Connection"] = "Keep-Alive" + end + + params.headers = headers + + -- Format and send request + local req = _format_request(self, params) + if DEBUG then ngx_log(ngx_DEBUG, "\n", req) end + local bytes, err = sock:send(req) + + if not bytes then + return nil, err + end + + -- Send the request body, unless we expect: continue, in which case + -- we handle this as part of reading the response. + if headers["Expect"] ~= "100-continue" then + local ok, err, partial = _send_body(sock, body) + if not ok then + return nil, err, partial + end + end + + return true +end + + +function _M.read_response(self, params) + local sock = self.sock + + local status, version, reason, err + + -- If we expect: continue, we need to handle this, sending the body if allowed. + -- If we don't get 100 back, then status is the actual status. + if params.headers["Expect"] == "100-continue" then + local _status, _version, _reason, _err = _handle_continue(sock, params.body) + if not _status then + return nil, _err + elseif _status ~= 100 then + status, version, reason, err = _status, _version, _reason, _err -- luacheck: no unused + end + end + + -- Just read the status as normal. + if not status then + status, version, reason, err = _receive_status(sock) + if not status then + return nil, err + end + end + + + local res_headers, err = _receive_headers(sock) + if not res_headers then + return nil, err + end + + -- keepalive is true by default. Determine if this is correct or not. + local ok, connection = pcall(str_lower, res_headers["Connection"]) + if ok then + if (version == 1.1 and str_find(connection, "close", 1, true)) or + (version == 1.0 and not str_find(connection, "keep-alive", 1, true)) then + self.keepalive = false + end + else + -- no connection header + if version == 1.0 then + self.keepalive = false + end + end + + local body_reader = _no_body_reader + local trailer_reader, err + local has_body = false + + -- Receive the body_reader + if _should_receive_body(params.method, status) then + has_body = true + + if version == 1.1 and transfer_encoding_is_chunked(res_headers) then + body_reader, err = _chunked_body_reader(sock) + else + local ok, length = pcall(tonumber, res_headers["Content-Length"]) + if not ok then + -- No content-length header, read until connection is closed by server + length = nil + end + + body_reader, err = _body_reader(sock, length) + end + end + + if res_headers["Trailer"] then + trailer_reader, err = _trailer_reader(sock) + end + + if err then + return nil, err + else + return { + status = status, + reason = reason, + headers = res_headers, + has_body = has_body, + body_reader = body_reader, + read_body = _read_body, + trailer_reader = trailer_reader, + read_trailers = _read_trailers, + } + end +end + + +function _M.request(self, params) + params = tbl_copy(params) -- Take by value + local res, err = self:send_request(params) + if not res then + return res, err + else + return self:read_response(params) + end +end + + +function _M.request_pipeline(self, requests) + requests = tbl_copy(requests) -- Take by value + + for _, params in ipairs(requests) do + if params.headers and params.headers["Expect"] == "100-continue" then + return nil, "Cannot pipeline request specifying Expect: 100-continue" + end + + local res, err = self:send_request(params) + if not res then + return res, err + end + end + + local responses = {} + for i, params in ipairs(requests) do + responses[i] = setmetatable({ + params = params, + response_read = false, + }, { + -- Read each actual response lazily, at the point the user tries + -- to access any of the fields. + __index = function(t, k) + local res, err + if t.response_read == false then + res, err = _M.read_response(self, t.params) + t.response_read = true + + if not res then + ngx_log(ngx_ERR, err) + else + for rk, rv in pairs(res) do + t[rk] = rv + end + end + end + return rawget(t, k) + end, + }) + end + return responses +end + + +function _M.request_uri(self, uri, params) + params = tbl_copy(params or {}) -- Take by value + if self.proxy_opts then + params.proxy_opts = tbl_copy(self.proxy_opts or {}) + end + + do + local parsed_uri, err = self:parse_uri(uri, false) + if not parsed_uri then + return nil, err + end + + local path, query + params.scheme, params.host, params.port, path, query = unpack(parsed_uri) + params.path = params.path or path + params.query = params.query or query + params.ssl_server_name = params.ssl_server_name or params.host + end + + do + local proxy_auth = (params.headers or {})["Proxy-Authorization"] + if proxy_auth and params.proxy_opts then + params.proxy_opts.https_proxy_authorization = proxy_auth + params.proxy_opts.http_proxy_authorization = proxy_auth + end + end + + local ok, err = self:connect(params) + if not ok then + return nil, err + end + + local res, err = self:request(params) + if not res then + self:close() + return nil, err + end + + local body, err = res:read_body() + if not body then + self:close() + return nil, err + end + + res.body = body + + if params.keepalive == false then + local ok, err = self:close() + if not ok then + ngx_log(ngx_ERR, err) + end + + else + local ok, err = self:set_keepalive(params.keepalive_timeout, params.keepalive_pool) + if not ok then + ngx_log(ngx_ERR, err) + end + + end + + return res, nil +end + + +function _M.get_client_body_reader(_, chunksize, sock) + chunksize = chunksize or 65536 + + if not sock then + local ok, err + ok, sock, err = pcall(ngx_req_socket) + + if not ok then + return nil, sock -- pcall err + end + + if not sock then + if err == "no body" then + return nil + else + return nil, err + end + end + end + + local headers = ngx_req_get_headers() + local length = headers.content_length + if length then + return _body_reader(sock, tonumber(length), chunksize) + elseif transfer_encoding_is_chunked(headers) then + -- Not yet supported by ngx_lua but should just work... + return _chunked_body_reader(sock, chunksize) + else + return nil + end +end + + +function _M.set_proxy_options(self, opts) + -- TODO: parse and cache these options, instead of parsing them + -- on each request over and over again (lru-cache on module level) + self.proxy_opts = tbl_copy(opts) -- Take by value +end + + +function _M.get_proxy_uri(self, scheme, host) + if not self.proxy_opts then + return nil + end + + -- Check if the no_proxy option matches this host. Implementation adapted + -- from lua-http library (https://github.com/daurnimator/lua-http) + if self.proxy_opts.no_proxy then + if self.proxy_opts.no_proxy == "*" then + -- all hosts are excluded + return nil + end + + local no_proxy_set = {} + -- wget allows domains in no_proxy list to be prefixed by "." + -- e.g. no_proxy=.mit.edu + for host_suffix in ngx_re_gmatch(self.proxy_opts.no_proxy, "\\.?([^,]+)", "jo") do + no_proxy_set[host_suffix[1]] = true + end + + -- From curl docs: + -- matched as either a domain which contains the hostname, or the + -- hostname itself. For example local.com would match local.com, + -- local.com:80, and www.local.com, but not www.notlocal.com. + -- + -- Therefore, we keep stripping subdomains from the host, compare + -- them to the ones in the no_proxy list and continue until we find + -- a match or until there's only the TLD left + repeat + if no_proxy_set[host] then + return nil + end + + -- Strip the next level from the domain and check if that one + -- is on the list + host = ngx_re_sub(host, "^[^.]+\\.", "", "jo") + until not ngx_re_find(host, "\\.", "jo") + end + + if scheme == "http" and self.proxy_opts.http_proxy then + return self.proxy_opts.http_proxy + end + + if scheme == "https" and self.proxy_opts.https_proxy then + return self.proxy_opts.https_proxy + end + + return nil +end + + +-- ---------------------------------------------------------------------------- +-- The following functions are considered DEPRECATED and may be REMOVED in +-- future releases. Please see the notes in `README.md`. +-- ---------------------------------------------------------------------------- + +function _M.ssl_handshake(self, ...) + ngx_log(ngx_DEBUG, "Use of deprecated function `ssl_handshake`") + + local sock = self.sock + if not sock then + return nil, "not initialized" + end + + self.ssl = true + + return sock:sslhandshake(...) +end + + +function _M.connect_proxy(self, proxy_uri, scheme, host, port, proxy_authorization) + ngx_log(ngx_DEBUG, "Use of deprecated function `connect_proxy`") + + -- Parse the provided proxy URI + local parsed_proxy_uri, err = self:parse_uri(proxy_uri, false) + if not parsed_proxy_uri then + return nil, err + end + + -- Check that the scheme is http (https is not supported for + -- connections between the client and the proxy) + local proxy_scheme = parsed_proxy_uri[1] + if proxy_scheme ~= "http" then + return nil, "protocol " .. proxy_scheme .. " not supported for proxy connections" + end + + -- Make the connection to the given proxy + local proxy_host, proxy_port = parsed_proxy_uri[2], parsed_proxy_uri[3] + local c, err = self:tcp_only_connect(proxy_host, proxy_port) + if not c then + return nil, err + end + + if scheme == "https" then + -- Make a CONNECT request to create a tunnel to the destination through + -- the proxy. The request-target and the Host header must be in the + -- authority-form of RFC 7230 Section 5.3.3. See also RFC 7231 Section + -- 4.3.6 for more details about the CONNECT request + local destination = host .. ":" .. port + local res, err = self:request({ + method = "CONNECT", + path = destination, + headers = { + ["Host"] = destination, + ["Proxy-Authorization"] = proxy_authorization, + } + }) + + if not res then + return nil, err + end + + if res.status < 200 or res.status > 299 then + return nil, "failed to establish a tunnel through a proxy: " .. res.status + end + end + + return c, nil +end + + +function _M.proxy_request(self, chunksize) + ngx_log(ngx_DEBUG, "Use of deprecated function `proxy_request`") + + return self:request({ + method = ngx_req_get_method(), + path = ngx_re_gsub(ngx_var.uri, "\\s", "%20", "jo") .. ngx_var.is_args .. (ngx_var.query_string or ""), + body = self:get_client_body_reader(chunksize), + headers = ngx_req_get_headers(), + }) +end + + +function _M.proxy_response(_, response, chunksize) + ngx_log(ngx_DEBUG, "Use of deprecated function `proxy_response`") + + if not response then + ngx_log(ngx_ERR, "no response provided") + return + end + + ngx.status = response.status + + -- Filter out hop-by-hop headeres + for k, v in pairs(response.headers) do + if not HOP_BY_HOP_HEADERS[str_lower(k)] then + ngx_header[k] = v + end + end + + local reader = response.body_reader + + repeat + local chunk, ok, read_err, print_err + + chunk, read_err = reader(chunksize) + if read_err then + ngx_log(ngx_ERR, read_err) + end + + if chunk then + ok, print_err = ngx_print(chunk) + if not ok then + ngx_log(ngx_ERR, print_err) + end + end + + if read_err or print_err then + break + end + until not chunk +end + + +return _M diff --git a/lua/plugins/crowdsec/resty/http_connect.lua b/lua/plugins/crowdsec/resty/http_connect.lua new file mode 100644 index 0000000..83b6d2f --- /dev/null +++ b/lua/plugins/crowdsec/resty/http_connect.lua @@ -0,0 +1,341 @@ +local ffi = require "ffi" +local ngx_re_gmatch = ngx.re.gmatch +local ngx_re_sub = ngx.re.sub +local ngx_re_find = ngx.re.find +local ngx_log = ngx.log +local ngx_WARN = ngx.WARN +local ngx_DEBUG = ngx.DEBUG +local to_hex = require("resty.string").to_hex +local ffi_gc = ffi.gc +local ffi_cast = ffi.cast +local type = type + +local lib_chain, lib_x509, lib_pkey +local openssl_available, res = xpcall(function() + lib_chain = require("resty.openssl.x509.chain") + lib_x509 = require("resty.openssl.x509") + lib_pkey = require("resty.openssl.pkey") +end, debug.traceback) + +if not openssl_available then + ngx_log(ngx_WARN, "failed to load module `resty.openssl.*`, \z + mTLS isn't supported without lua-resty-openssl:\n", res) +end + +--[[ +A connection function that incorporates: + - tcp connect + - ssl handshake + - http proxy +Due to this it will be better at setting up a socket pool where connections can +be kept alive. + + +Call it with a single options table as follows: + +client:connect { + scheme = "https" -- scheme to use, or nil for unix domain socket + host = "myhost.com", -- target machine, or a unix domain socket + port = nil, -- port on target machine, will default to 80/443 based on scheme + pool = nil, -- connection pool name, leave blank! this function knows best! + pool_size = nil, -- options as per: https://github.com/openresty/lua-nginx-module#tcpsockconnect + backlog = nil, + + -- ssl options as per: https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake + ssl_reused_session = nil + ssl_server_name = nil, + ssl_send_status_req = nil, + ssl_verify = true, -- NOTE: defaults to true + ctx = nil, -- NOTE: not supported + + -- mTLS options: These require support for mTLS in cosockets, which first + -- appeared in `ngx_http_lua_module` v0.10.23. + ssl_client_cert = nil, + ssl_client_priv_key = nil, + + proxy_opts, -- proxy opts, defaults to global proxy options +} +]] +local function connect(self, options) + local sock = self.sock + if not sock then + return nil, "not initialized" + end + + local ok, err + + local request_scheme = options.scheme + local request_host = options.host + local request_port = options.port + + local poolname = options.pool + local pool_size = options.pool_size + local backlog = options.backlog + + if request_scheme and not request_port then + request_port = (request_scheme == "https" and 443 or 80) + elseif request_port and not request_scheme then + return nil, "'scheme' is required when providing a port" + end + + -- ssl settings + local ssl, ssl_reused_session, ssl_server_name + local ssl_verify, ssl_send_status_req, ssl_client_cert, ssl_client_priv_key + if request_scheme == "https" then + ssl = true + ssl_reused_session = options.ssl_reused_session + ssl_server_name = options.ssl_server_name + ssl_send_status_req = options.ssl_send_status_req + ssl_verify = true -- default + if options.ssl_verify == false then + ssl_verify = false + end + ssl_client_cert = options.ssl_client_cert + ssl_client_priv_key = options.ssl_client_priv_key + end + + -- proxy related settings + local proxy, proxy_uri, proxy_authorization, proxy_host, proxy_port, path_prefix + proxy = options.proxy_opts or self.proxy_opts + + if proxy then + if request_scheme == "https" then + proxy_uri = proxy.https_proxy + proxy_authorization = proxy.https_proxy_authorization + else + proxy_uri = proxy.http_proxy + proxy_authorization = proxy.http_proxy_authorization + -- When a proxy is used, the target URI must be in absolute-form + -- (RFC 7230, Section 5.3.2.). That is, it must be an absolute URI + -- to the remote resource with the scheme, host and an optional port + -- in place. + -- + -- Since _format_request() constructs the request line by concatenating + -- params.path and params.query together, we need to modify the path + -- to also include the scheme, host and port so that the final form + -- in conformant to RFC 7230. + path_prefix = "http://" .. request_host .. (request_port == 80 and "" or (":" .. request_port)) + end + if not proxy_uri then + proxy = nil + proxy_authorization = nil + path_prefix = nil + end + end + + if proxy and proxy.no_proxy then + -- Check if the no_proxy option matches this host. Implementation adapted + -- from lua-http library (https://github.com/daurnimator/lua-http) + if proxy.no_proxy == "*" then + -- all hosts are excluded + proxy = nil + + else + local host = request_host + local no_proxy_set = {} + -- wget allows domains in no_proxy list to be prefixed by "." + -- e.g. no_proxy=.mit.edu + for host_suffix in ngx_re_gmatch(proxy.no_proxy, "\\.?([^,]+)") do + no_proxy_set[host_suffix[1]] = true + end + + -- From curl docs: + -- matched as either a domain which contains the hostname, or the + -- hostname itself. For example local.com would match local.com, + -- local.com:80, and www.local.com, but not www.notlocal.com. + -- + -- Therefore, we keep stripping subdomains from the host, compare + -- them to the ones in the no_proxy list and continue until we find + -- a match or until there's only the TLD left + repeat + if no_proxy_set[host] then + proxy = nil + proxy_uri = nil + proxy_authorization = nil + break + end + + -- Strip the next level from the domain and check if that one + -- is on the list + host = ngx_re_sub(host, "^[^.]+\\.", "") + until not ngx_re_find(host, "\\.") + end + end + + if proxy then + local proxy_uri_t + proxy_uri_t, err = self:parse_uri(proxy_uri) + if not proxy_uri_t then + return nil, "uri parse error: " .. err + end + + local proxy_scheme = proxy_uri_t[1] + if proxy_scheme ~= "http" then + return nil, "protocol " .. tostring(proxy_scheme) .. + " not supported for proxy connections" + end + proxy_host = proxy_uri_t[2] + proxy_port = proxy_uri_t[3] + end + + local cert_hash + if ssl and ssl_client_cert and ssl_client_priv_key then + local cert_type = type(ssl_client_cert) + local key_type = type(ssl_client_priv_key) + + if cert_type ~= "cdata" then + return nil, "bad ssl_client_cert: cdata expected, got " .. cert_type + end + + if key_type ~= "cdata" then + return nil, "bad ssl_client_priv_key: cdata expected, got " .. key_type + end + + if not openssl_available then + return nil, "module `resty.openssl.*` not available, mTLS isn't supported without lua-resty-openssl" + end + + -- convert from `void*` to `OPENSSL_STACK*` + local cert_chain, err = lib_chain.dup(ffi_cast("OPENSSL_STACK*", ssl_client_cert)) + if not cert_chain then + return nil, "failed to dup the ssl_client_cert: " .. err + end + + if #cert_chain < 1 then + return nil, "no cert in ssl_client_cert" + end + + local cert, err = lib_x509.dup(cert_chain[1].ctx) + if not cert then + return nil, "failed to dup the x509: " .. err + end + + -- convert from `void*` to `EVP_PKEY*` + local key, err = lib_pkey.new(ffi_cast("EVP_PKEY*", ssl_client_priv_key)) + if not key then + return nil, "failed to new the pkey: " .. err + end + + -- should not free the cdata passed in + ffi_gc(key.ctx, nil) + + -- check the private key in order to make sure the caller is indeed the holder of the cert + ok, err = cert:check_private_key(key) + if not ok then + return nil, "the private key doesn't match the cert: " .. err + end + + cert_hash, err = cert:digest("sha256") + if not cert_hash then + return nil, "failed to calculate the digest of the cert: " .. err + end + + cert_hash = to_hex(cert_hash) -- convert to hex so that it's printable + end + + -- construct a poolname unique within proxy and ssl info + if not poolname then + poolname = (request_scheme or "") + .. ":" .. request_host + .. ":" .. tostring(request_port) + .. ":" .. tostring(ssl) + .. ":" .. (ssl_server_name or "") + .. ":" .. tostring(ssl_verify) + .. ":" .. (proxy_uri or "") + .. ":" .. (request_scheme == "https" and proxy_authorization or "") + .. ":" .. (cert_hash or "") + -- in the above we only add the 'proxy_authorization' as part of the poolname + -- when the request is https. Because in that case the CONNECT request (which + -- carries the authorization header) is part of the connect procedure, whereas + -- with a plain http request the authorization is part of the actual request. + end + + ngx_log(ngx_DEBUG, "poolname: ", poolname) + + -- do TCP level connection + local tcp_opts = { pool = poolname, pool_size = pool_size, backlog = backlog } + if proxy then + -- proxy based connection + ok, err = sock:connect(proxy_host, proxy_port, tcp_opts) + if not ok then + return nil, "failed to connect to: " .. (proxy_host or "") .. + ":" .. (proxy_port or "") .. + ": " .. err + end + + if ssl and sock:getreusedtimes() == 0 then + -- Make a CONNECT request to create a tunnel to the destination through + -- the proxy. The request-target and the Host header must be in the + -- authority-form of RFC 7230 Section 5.3.3. See also RFC 7231 Section + -- 4.3.6 for more details about the CONNECT request + local destination = request_host .. ":" .. request_port + local res + res, err = self:request({ + method = "CONNECT", + path = destination, + headers = { + ["Host"] = destination, + ["Proxy-Authorization"] = proxy_authorization, + } + }) + + if not res then + return nil, "failed to issue CONNECT to proxy: " .. err + end + + if res.status < 200 or res.status > 299 then + return nil, "failed to establish a tunnel through a proxy: " .. res.status + end + end + + elseif not request_port then + -- non-proxy, without port -> unix domain socket + ok, err = sock:connect(request_host, tcp_opts) + if not ok then + return nil, err + end + + else + -- non-proxy, regular network tcp + ok, err = sock:connect(request_host, request_port, tcp_opts) + if not ok then + return nil, err + end + end + + local ssl_session + -- Now do the ssl handshake + if ssl and sock:getreusedtimes() == 0 then + + -- Experimental mTLS support + if ssl_client_cert and ssl_client_priv_key then + if type(sock.setclientcert) ~= "function" then + return nil, "cannot use SSL client cert and key without mTLS support" + + else + ok, err = sock:setclientcert(ssl_client_cert, ssl_client_priv_key) + if not ok then + return nil, "could not set client certificate: " .. err + end + end + end + + ssl_session, err = sock:sslhandshake(ssl_reused_session, ssl_server_name, ssl_verify, ssl_send_status_req) + if not ssl_session then + self:close() + return nil, err + end + end + + self.host = request_host + self.port = request_port + self.keepalive = true + self.ssl = ssl + -- set only for http, https has already been handled + self.http_proxy_auth = request_scheme ~= "https" and proxy_authorization or nil + self.path_prefix = path_prefix + + return true, nil, ssl_session +end + +return connect diff --git a/lua/plugins/crowdsec/resty/http_headers.lua b/lua/plugins/crowdsec/resty/http_headers.lua new file mode 100644 index 0000000..6394e61 --- /dev/null +++ b/lua/plugins/crowdsec/resty/http_headers.lua @@ -0,0 +1,44 @@ +local rawget, rawset, setmetatable = + rawget, rawset, setmetatable + +local str_lower = string.lower + +local _M = { + _VERSION = '0.17.2', +} + + +-- Returns an empty headers table with internalised case normalisation. +function _M.new() + local mt = { + normalised = {}, + } + + mt.__index = function(t, k) + return rawget(t, mt.normalised[str_lower(k)]) + end + + mt.__newindex = function(t, k, v) + local k_normalised = str_lower(k) + + -- First time seeing this header field? + if not mt.normalised[k_normalised] then + -- Create a lowercased entry in the metatable proxy, with the value + -- of the given field case + mt.normalised[k_normalised] = k + + -- Set the header using the given field case + rawset(t, k, v) + else + -- We're being updated just with a different field case. Use the + -- normalised metatable proxy to give us the original key case, and + -- perorm a rawset() to update the value. + rawset(t, mt.normalised[k_normalised], v) + end + end + + return setmetatable({}, mt) +end + + +return _M diff --git a/lua/plugins/crowdsec/templates/crowdsec/ban.html b/lua/plugins/crowdsec/templates/crowdsec/ban.html new file mode 100644 index 0000000..16e9fed --- /dev/null +++ b/lua/plugins/crowdsec/templates/crowdsec/ban.html @@ -0,0 +1,99 @@ + + + + + nadeko.net Protection + + + + + + + +
+

Your IP has been banned from the server.

+

Tú IP ha sido baneada del servidor.

+
+
+

Beta feature to prevent bots, false positives can happen.

+

Funcionalidad beta para evitar bots, pueden producirse falsos positivos.

+
+ + + diff --git a/lua/plugins/crowdsec/templates/crowdsec/ban2.html b/lua/plugins/crowdsec/templates/crowdsec/ban2.html new file mode 100644 index 0000000..168fae5 --- /dev/null +++ b/lua/plugins/crowdsec/templates/crowdsec/ban2.html @@ -0,0 +1,66 @@ + + + + + nadeko.net Protection + + + + + + + +
+

Estas baneado del servidor

+

You are banned from the server

+
+ + diff --git a/lua/plugins/crowdsec/templates/crowdsec/captcha.html b/lua/plugins/crowdsec/templates/crowdsec/captcha.html new file mode 100644 index 0000000..5d1d4ec --- /dev/null +++ b/lua/plugins/crowdsec/templates/crowdsec/captcha.html @@ -0,0 +1,136 @@ + + + + + nadeko.net Protection + + + + + + + +
+

Verifying that you are not a bot/abuser

+

Verificando que no seas un bot/abusador

+
+ +
+ + +
+ +

(This will not leave any cookies in your browser)

+

(Esto no dejará ninguna cookie en tu navegador)

+
+ +
+
+

Beta feature to prevent bots, false positives can happen.

+

Funcionalidad beta para evitar bots, pueden producirse falsos positivos.

+
+ + + diff --git a/lua/plugins/crowdsec/templates/crowdsec/captcha2.html b/lua/plugins/crowdsec/templates/crowdsec/captcha2.html new file mode 100644 index 0000000..956b278 --- /dev/null +++ b/lua/plugins/crowdsec/templates/crowdsec/captcha2.html @@ -0,0 +1,132 @@ + + + + + nadeko.net Protection + + + + + + + +
+

Verifying that you are not a bot/abuser

+

Verificando que no seas un bot/abusador

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/lua/plugins/crowdsec/templates/crowdsec/captcha3.html b/lua/plugins/crowdsec/templates/crowdsec/captcha3.html new file mode 100644 index 0000000..75e3bb6 --- /dev/null +++ b/lua/plugins/crowdsec/templates/crowdsec/captcha3.html @@ -0,0 +1,150 @@ + + + + + nadeko.net Protection + + + + + + + +
+

Verifying that you are not a bot/abuser

+

Verificando que no seas un bot/abusador

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+ + + + diff --git a/lua/rewrite-invidious/check-backend.lua b/lua/rewrite-invidious/check-backend.lua new file mode 100644 index 0000000..fc9d38f --- /dev/null +++ b/lua/rewrite-invidious/check-backend.lua @@ -0,0 +1,15 @@ +local _M = {} + +function _M.check_backend_args() + local args = ngx.req.get_uri_args() + for key, val in pairs(args) do + if key == "backend" then + val = tonumber(val) + if val == nil then + ngx.say("error: Wrong backend. Backends only supports numbers!") + end + end + end +end + +return _M diff --git a/lua/rewrite-invidious/init.lua b/lua/rewrite-invidious/init.lua new file mode 100644 index 0000000..ea3611e --- /dev/null +++ b/lua/rewrite-invidious/init.lua @@ -0,0 +1,6 @@ +local checkbackend = require "rewrite-invidious/check-backend" + +-- Functions +if ngx.req.get_headers()["Host"] == "inv.nadeko.net" then + checkbackend.check_backend_args() +end diff --git a/lua/sticky-http3.lua b/lua/sticky-http3.lua new file mode 100644 index 0000000..8665ba6 --- /dev/null +++ b/lua/sticky-http3.lua @@ -0,0 +1,100 @@ +-- Based on https://github.com/Klaessen/openresty-loadbalancers/blob/main/sticky-balancer.lua + +local _M = {} + +local balancer = require "ngx.balancer" +local cookie_name = "INVIDIOUS_SERVER_ID" + +-- Define backend server based on their capabilities (IP, port, weight, number of retries before timeout, duration of timeout, current number of fails, fail timestamp) +local servers = { + { "unix:/tmp/http3-ytproxy.sock", 10061, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10071, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10081, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 20101, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +} + +-- Generate a weighted server list based on weights +local function generate_weighted_server_list(servers) + local weighted_servers = {} + for _, server in ipairs(servers) do + for i = 1, server.weight do + table.insert(weighted_servers, server) + end + end + return weighted_servers +end + +local weighted_servers = generate_weighted_server_list(servers) + +-- Hash function to select server +local function hash(key, num_buckets) + local hash = ngx.crc32_long(key) + return (hash % num_buckets) + 1 +end + +-- Check if a server is available based on max_fails and fail_timeout +local function is_server_available(server) + if server.fail_count >= server.max_fails then + if (ngx.now() - server.last_fail_time) < server.fail_timeout then + return false + else + server.fail_count = 0 + server.last_fail_time = 0 + end + end + return true +end + +-- Select server based on cookie or assign a new one +local function select_server() + local cookie = ngx.var["cookie_" .. cookie_name] + local host = "" + local server_index + + math.randomseed(os.time()) + + if cookie then + server_index = tonumber(cookie) + ngx.header["X-Server-Id"] = server_index + else + -- server_index = hash(ngx.var.remote_addr, #weighted_servers) + server_index = math.random(#servers) + ngx.header["Set-Cookie"] = cookie_name .. "=" .. server_index .. "; domain=.nadeko.net; Path=/; HttpOnly; SameSite=None; Secure; Partitioned" + end + + local server = weighted_servers[server_index] + if is_server_available(server) then + return server + else + -- If the selected server is not available, find another one + for _, s in ipairs(weighted_servers) do + if is_server_available(s) then + return s + end + end + end + ngx.log(ngx.ERR, "No available servers") + return nil +end + +local server = select_server() +if not server then + ngx.exit(502) + return +end + +local ok, err + +if string.match(server[1], 'unix:') then + ok, err = balancer.set_current_peer(server[1]) +else + ok, err = balancer.set_current_peer(server[1], server[2]) +end + +-- local ok, err = balancer.set_current_peer(server[1], server[2]) +if not ok then + ngx.log(ngx.ERR, "Failed to set the current peer: ", err) + server.fail_count = server.fail_count + 1 + server.last_fail_time = ngx.now() + return ngx.exit(500) +end diff --git a/lua/switchbackend.lua b/lua/switchbackend.lua new file mode 100644 index 0000000..8d54071 --- /dev/null +++ b/lua/switchbackend.lua @@ -0,0 +1,22 @@ +-- TODO: Support Clear, TOR and I2P +local args = ngx.req.get_uri_args() +local referer = ngx.req.get_headers()["Referer"] +local host = ngx.req.get_headers()["Host"] +local cookie_name = "INVIDIOUS_SERVER_ID" +local domain = ".nadeko.net" + +-- TOR Support +if string.match(host, ".onion") then + domain = host +end + +for key, server_index in pairs(args) do + if key == "backend_id" then + ngx.header["Set-Cookie"] = cookie_name .. "=" .. server_index .. "; domain=" .. domain .. "; Path=/; HttpOnly; SameSite=None; Secure; Partitioned" + end +end + +if referer == nil then + return ngx.redirect("/", 302) +end +return ngx.redirect(referer, 302) diff --git a/lua/unused/image-cache.lua b/lua/unused/image-cache.lua new file mode 100644 index 0000000..36b0a2d --- /dev/null +++ b/lua/unused/image-cache.lua @@ -0,0 +1,20 @@ +local _M = {} + +-- alternatively: local lrucache = require "resty.lrucache.pureffi" +local lrucache = require "resty.lrucache" + +-- we need to initialize the cache on the lua module level so that +-- it can be shared by all the requests served by each nginx worker process: +local c, err = lrucache.new(1024) -- allow up to 200 items in the cache +if not c then + error("failed to create the cache: " .. (err or "unknown")) +end + +function _M.go() + ngx.say("cat: ", c:get("cat")) + c:set("dog", { age = 10 }, 0.1) -- expire in 0.1 sec +end + + + +return _M diff --git a/lua/unused/invidious-healthcheck.lua b/lua/unused/invidious-healthcheck.lua new file mode 100644 index 0000000..2b5c02c --- /dev/null +++ b/lua/unused/invidious-healthcheck.lua @@ -0,0 +1,14 @@ +local http = require "resty.http" + +local httpc = http.new() +httpc:set_timeout(250) +local res, err = httpc:request_uri("", { + method = "HEAD", +}) + +httpc:close() +if err ~= nil then + return true, err +end + +ngx.say(res.status) diff --git a/lua/unused/invidious-sticky-i2p.lua b/lua/unused/invidious-sticky-i2p.lua new file mode 100644 index 0000000..edea7c9 --- /dev/null +++ b/lua/unused/invidious-sticky-i2p.lua @@ -0,0 +1,94 @@ +-- Based on https://github.com/Klaessen/openresty-loadbalancers/blob/main/sticky-balancer.lua + +local _M = {} + +local balancer = require "ngx.balancer" +local cookie_name = "SERVER_ID" + +-- Define backend server based on their capabilities (IP, port, weight, number of retries before timeout, duration of timeout, current number of fails, fail timestamp) +local servers = { + { "127.0.0.1", 10063, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10073, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10083, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +} + +-- Put the ammount of servers in the dictionary +local s = ngx.shared.servers +s:set("invidious-servers", #servers) + +-- Generate a weighted server list based on weights +local function generate_weighted_server_list(servers) + local weighted_servers = {} + for _, server in ipairs(servers) do + for i = 1, server.weight do + table.insert(weighted_servers, server) + end + end + return weighted_servers +end + +local weighted_servers = generate_weighted_server_list(servers) + +-- Hash function to select server +local function hash(key, num_buckets) + local hash = ngx.crc32_long(key) + return (hash % num_buckets) + 1 +end + +-- Check if a server is available based on max_fails and fail_timeout +local function is_server_available(server) + if server.fail_count >= server.max_fails then + if (ngx.now() - server.last_fail_time) < server.fail_timeout then + return false + else + server.fail_count = 0 + server.last_fail_time = 0 + end + end + return true +end + +-- Select server based on cookie or assign a new one +local function select_server() + local cookie = ngx.var["cookie_" .. cookie_name] + local host = ngx.req.get_headers()["Host"] + local server_index + + math.randomseed(os.time()) + + if cookie then + server_index = tonumber(cookie) + ngx.header["X-Server-Id"] = server_index + else + -- server_index = hash(ngx.var.remote_addr, #weighted_servers) + server_index = math.random(#servers) + ngx.header["Set-Cookie"] = cookie_name .. "=" .. server_index .. "; domain=" .. host .. "; Path=/; HttpOnly; SameSite=Lax" + end + + local server = weighted_servers[server_index] + if is_server_available(server) then + return server + else + -- If the selected server is not available, find another one + for _, s in ipairs(weighted_servers) do + if is_server_available(s) then + return s + end + end + end + ngx.log(ngx.ERR, "No available servers") + return nil +end + +local server = select_server() +if not server then + ngx.exit(502) + return +end +local ok, err = balancer.set_current_peer(server[1], server[2]) +if not ok then + ngx.log(ngx.ERR, "Failed to set the current peer: ", err) + server.fail_count = server.fail_count + 1 + server.last_fail_time = ngx.now() + return ngx.exit(500) +end diff --git a/lua/unused/invidious-sticky-tor.lua b/lua/unused/invidious-sticky-tor.lua new file mode 100644 index 0000000..d71f664 --- /dev/null +++ b/lua/unused/invidious-sticky-tor.lua @@ -0,0 +1,93 @@ +-- Based on https://github.com/Klaessen/openresty-loadbalancers/blob/main/sticky-balancer.lua + +local _M = {} + +local balancer = require "ngx.balancer" +local cookie_name = "SERVER_ID" + +-- Define backend server based on their capabilities (IP, port, weight, number of retries before timeout, duration of timeout, current number of fails, fail timestamp) +local servers = { + { "127.0.0.1", 10062, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10072, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10082, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +} + +-- Put the ammount of servers in the dictionary +local s = ngx.shared.servers +s:set("invidious-servers", #servers) + +-- Generate a weighted server list based on weights +local function generate_weighted_server_list(servers) + local weighted_servers = {} + for _, server in ipairs(servers) do + for i = 1, server.weight do + table.insert(weighted_servers, server) + end + end + return weighted_servers +end + +local weighted_servers = generate_weighted_server_list(servers) + +-- Hash function to select server +local function hash(key, num_buckets) + local hash = ngx.crc32_long(key) + return (hash % num_buckets) + 1 +end + +-- Check if a server is available based on max_fails and fail_timeout +local function is_server_available(server) + if server.fail_count >= server.max_fails then + if (ngx.now() - server.last_fail_time) < server.fail_timeout then + return false + else + server.fail_count = 0 + server.last_fail_time = 0 + end + end + return true +end + +-- Select server based on cookie or assign a new one +local function select_server() + local cookie = ngx.var["cookie_" .. cookie_name] + local server_index + + math.randomseed(os.time()) + + if cookie then + server_index = tonumber(cookie) + ngx.header["X-Server-Id"] = server_index + else + -- server_index = hash(ngx.var.remote_addr, #weighted_servers) + server_index = math.random(#servers) + ngx.header["Set-Cookie"] = cookie_name .. "=" .. server_index .. "; domain=inv.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion; Path=/; HttpOnly; SameSite=Lax" + end + + local server = weighted_servers[server_index] + if is_server_available(server) then + return server + else + -- If the selected server is not available, find another one + for _, s in ipairs(weighted_servers) do + if is_server_available(s) then + return s + end + end + end + ngx.log(ngx.ERR, "No available servers") + return nil +end + +local server = select_server() +if not server then + ngx.exit(502) + return +end +local ok, err = balancer.set_current_peer(server[1], server[2]) +if not ok then + ngx.log(ngx.ERR, "Failed to set the current peer: ", err) + server.fail_count = server.fail_count + 1 + server.last_fail_time = ngx.now() + return ngx.exit(500) +end diff --git a/lua/unused/invidious-sticky.lua b/lua/unused/invidious-sticky.lua new file mode 100644 index 0000000..e6d4c72 --- /dev/null +++ b/lua/unused/invidious-sticky.lua @@ -0,0 +1,96 @@ +-- Based on https://github.com/Klaessen/openresty-loadbalancers/blob/main/sticky-balancer.lua + +local _M = {} + +local balancer = require "ngx.balancer" +local cookie_name = "SERVER_ID" + +-- Define backend server based on their capabilities (IP, port, weight, number of retries before timeout, duration of timeout, current number of fails, fail timestamp) +local servers = { + { "127.0.0.1", 10060, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10070, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 10080, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, + { "127.0.0.1", 20100, weight = 1, max_fails = 3, fail_timeout = 30, fail_count = 0, last_fail_time = 0 }, +} + +-- Put the ammount of servers in the dictionary +local s = ngx.shared.servers +s:set("invidious-servers", #servers) + +-- Generate a weighted server list based on weights +local function generate_weighted_server_list(servers) + local weighted_servers = {} + for _, server in ipairs(servers) do + for i = 1, server.weight do + table.insert(weighted_servers, server) + end + end + return weighted_servers +end + +local weighted_servers = generate_weighted_server_list(servers) + +-- Hash function to select server +local function hash(key, num_buckets) + local hash = ngx.crc32_long(key) + return (hash % num_buckets) + 1 +end + +-- Check if a server is available based on max_fails and fail_timeout +local function is_server_available(server) + if server.fail_count >= server.max_fails then + if (ngx.now() - server.last_fail_time) < server.fail_timeout then + return false + else + server.fail_count = 0 + server.last_fail_time = 0 + end + end + return true +end + +-- Select server based on cookie or assign a new one +local function select_server() + local cookie = ngx.var["cookie_" .. cookie_name] + local server_index + local host = ngx.req.get_headers()["Host"] + + math.randomseed(os.time()) + + if cookie then + server_index = tonumber(cookie) + ngx.header["X-Server-Id"] = server_index + else + -- server_index = hash(ngx.var.remote_addr, #weighted_servers) + server_index = math.random(#servers) + -- FIX I2P. Secure doesn't work on i2p IIRC + ngx.header["Set-Cookie"] = cookie_name .. "=" .. server_index .. "; domain=" .. host .. "; Path=/; SameSite=None; Secure" + end + + local server = weighted_servers[server_index] + if is_server_available(server) then + return server + else + -- If the selected server is not available, find another one + for _, s in ipairs(weighted_servers) do + if is_server_available(s) then + return s + end + end + end + ngx.log(ngx.ERR, "No available servers") + return nil +end + +local server = select_server() +if not server then + ngx.exit(502) + return +end +local ok, err = balancer.set_current_peer(server[1], server[2]) +if not ok then + ngx.log(ngx.ERR, "Failed to set the current peer: ", err) + server.fail_count = server.fail_count + 1 + server.last_fail_time = ngx.now() + return ngx.exit(500) +end diff --git a/lua/unused/youtube-image-proxy.lua b/lua/unused/youtube-image-proxy.lua new file mode 100644 index 0000000..0fc4bb3 --- /dev/null +++ b/lua/unused/youtube-image-proxy.lua @@ -0,0 +1,114 @@ +local req = require("reqwest") +local http = require "resty.http" +-- local res, err = req.request("https://i.ytimg.com/" .. path, { headers = { ["User-Agent"] = "" }, version = 2 }) +-- if err ~= nil then +-- ngx.status = 500 +-- ngx.say(err) +-- end +-- +-- ngx.print(res.body) +-- your_script.lua + +-- local function blocking_io_operation() +-- local path = ngx.var.uri +-- local res, err = req.request("https://i.ytimg.com/" .. path, { headers = { ["User-Agent"] = "" }, version = 2 }) +-- if err ~= nil then +-- ngx.status = 500 +-- ngx.say(err) +-- end +-- return res +-- end +-- +local function blocking_io_operation() + local httpc = http.new() + local path = ngx.var.uri + -- local res, err = req.request("https://i.ytimg.com/" .. path, { headers = { ["User-Agent"] = "" }, version = 2 }) + -- if err ~= nil then + -- ngx.status = 500 + -- ngx.say(err) + -- end + -- return res + + local res, err = httpc:request_uri("https://i.ytimg.com/" .. path, { + method = "GET", + headers = { + ["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0", + } + }) + -- httpc:close() + if err ~= nil then + ngx.status = 500 + ngx.say(err) + end + ngx.print(res.body) + -- return res.body + +end +-- +-- local function main() +-- -- Spawn a new thread to handle the blocking operation +-- local co = ngx.thread.spawn(blocking_io_operation) +-- +-- -- Do other processing here if needed +-- +-- -- Wait for the thread to finish and retrieve the result +-- local ok, result = ngx.thread.wait(co) +-- +-- if not ok then +-- ngx.say("Error: ", result) +-- return +-- end +-- +-- ngx.print(result.body) +-- end + +-- main() +blocking_io_operation() + +-- +-- local req = require("reqwest") +-- +-- local path = ngx.var.uri +-- +-- -- Function to perform the request +-- local function perform_request() + -- local res, err = req.request("https://i.ytimg.com/" .. path, { headers = { ["User-Agent"] = "reqwest" }, version = 2 }) + -- if err then + -- ngx.log(ngx.ERR, "12313" .. err) + -- return nil, err + -- end + -- return res.body + -- end + -- + -- -- Thread function + -- local function thread_func(premature) + -- if premature then + -- return + -- end + -- + -- -- Perform the request in the thread + -- local body, err = perform_request() + -- + -- if err then + -- ngx.log(ngx.ERR, "Request failed: " .. err) + -- -- Here you can handle the error (e.g., log it) + -- else + -- -- Here you can store or process the response as needed + -- ngx.shared.my_shared_dict:set("response_body", body) + -- ngx.say(body) + -- -- ngx.log(ngx.ERR, "Requesasdasd") + -- end + -- end + -- + -- -- Start the thread + -- local ok, err = ngx.thread.spawn(thread_func) + -- if not ok then + -- + -- ngx.log(ngx.ERR, "xd") + -- ngx.status = 500 + -- ngx.say("Failed to spawn thread: " .. err) + -- return + -- end + -- + -- -- Respond immediately, while the request is processed in the background + -- -- ngx.say("Request is being processed. You will receive the response shortly.") diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..72be1a2 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,104 @@ +worker_processes auto; +worker_rlimit_nofile 65535; +pid /run/openresty.pid; +error_log /var/log/nginx/error.log debug; + +# include modules/*.conf; +#load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so; # for compressing responses on-the-fly +#load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so; # for serving pre-compressed files +#load_module /usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so; # To add headers to any location without the nginx bullshit +#load_module /usr/lib/nginx/modules/ngx_http_zstd_filter_module.so; # for compressing responses on-the-fly +#load_module /usr/lib/nginx/modules/ngx_http_zstd_static_module.so; # for serving pre-compressed files +#load_module /usr/lib/nginx/modules/ngx_http_lua_module.so; # ngx_lua +#load_module /usr/lib/nginx/modules/ngx_http_lua_module.so + +quic_bpf on; + +events { + worker_connections 4096; + multi_accept on; + use epoll; +} + +#resolver 127.0.0.1; + +http { + map $server_addr $unix { + default 0; + "~unix:" 1; + } + + include configs/cache.conf; + + lua_package_path '/etc/openresty/lua/plugins/crowdsec/?.lua;/etc/openresty/lua/?.lua;/etc/openresty/lua/?/?.lua;;'; + lua_package_cpath '/usr/lib/lua/5.1/?.so;;'; + lua_shared_dict crowdsec_cache 128m; + lua_socket_pool_size 1024; + resolver 127.0.0.1 ipv6=off; + lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + init_by_lua_file "conf/lua/init.lua"; + + include configs/crowdsec.conf; + + log_format limited '$remote_addr - $remote_user [$time_local] ' + '"$request_method /bogus $server_protocol" $status $body_bytes_sent ' + '"-" "Bogus/66.6" - "$http_host"'; + + access_log off; + error_log /dev/null; + #error_log /var/log/nginx/error.log debug; + + # Basic Settings + charset utf-8; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + server_tokens off; + log_not_found off; + types_hash_max_size 1024; + types_hash_bucket_size 128; + server_names_hash_bucket_size 128; + + # MIME + include mime.types; + + # SSL + include configs/ssl.conf; + + # reset timed out connections freeing ram + reset_timedout_connection on; + # maximum time between packets the client can pause when sending nginx any data + client_body_timeout 10s; + # maximum time the client has to send the entire header to nginx + client_header_timeout 10s; + # timeout which a single keep-alive client connection will stay open + keepalive_timeout 60s; + # maximum time between packets nginx is allowed to pause when sending the client data + send_timeout 10s; + + client_body_buffer_size 32k; + client_max_body_size 2m; + + # open_file_cache max=1024 inactive=10s; + # open_file_cache_valid 60s; + # open_file_cache_min_uses 2; + # open_file_cache_errors on; + + # PERFORMANCE / ASYNC I/O + #aio threads=default; + #aio_write on; + #directio 2m; + + # QUIC settings + # https://nginx.org/en/docs/http/ngx_http_v3_module.html + quic_gso on; + + # Maps + include snippets/maps.conf; + #include snippets/poop.conf; + + include configs/general.conf; + include configs/upstreams.conf; + include configs/limits.conf; + include http.d/*.conf; +} diff --git a/snippets/maps.conf b/snippets/maps.conf new file mode 100644 index 0000000..382051b --- /dev/null +++ b/snippets/maps.conf @@ -0,0 +1,35 @@ +# Connection header for WebSocket reverse proxy + +map $http_upgrade $connection_upgrade { + default upgrade; + "" close; +} + +map $remote_addr $proxy_forwarded_elem { + + # IPv4 addresses can be sent as-is + ~^[0-9.]+$ "for=$remote_addr"; + + # IPv6 addresses need to be bracketed and quoted + ~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\""; + + # Unix domain socket names cannot be represented in RFC 7239 syntax + default "for=unknown"; +} + +map $http_forwarded $proxy_add_forwarded { + + # If the incoming Forwarded header is syntactically valid, append to it + "~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem"; + + # Otherwise, replace it + default "$proxy_forwarded_elem"; +} + +map $http_user_agent $blocked_agent { + default 0; + ~*meta-externalagent 1; + ~*SemrushBot 1; + ~*MJ12bot 1; + ~*amazonbot 1; +}