├── .github ├── release-drafter.yml └── workflows │ ├── build_package.yaml │ └── release-drafter.yml ├── LICENSE ├── Makefile ├── README.md ├── community_blocklist.map ├── crowdsec-haproxy-bouncer.conf ├── debian ├── changelog ├── compat ├── control ├── files ├── postinst ├── postrm ├── prerm └── rules ├── docs └── assets │ └── crowdsec_haproxy.svg ├── example ├── README.md ├── conf │ ├── acquis.yaml │ ├── crowdsec-haproxy-bouncer.conf │ ├── haproxy_local.cfg │ └── nginx.conf └── docker-compose.yaml ├── haproxy.cfg ├── install.sh ├── lib ├── crowdsec.lua ├── json.lua └── plugins │ └── crowdsec │ ├── ban.lua │ ├── captcha.lua │ ├── config.lua │ ├── template.lua │ └── utils.lua ├── rpm └── SPECS │ └── crowdsec-haproxy-bouncer.spec ├── templates ├── ban.html └── captcha.html ├── uninstall.sh └── upgrade.sh /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/build_package.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/build-docker-image.yml 2 | name: release-package 3 | 4 | on: 5 | release: 6 | types: prereleased 7 | 8 | jobs: 9 | release-package: 10 | name: Upload release package 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: make the package 15 | run: make release 16 | - name: Upload to release 17 | uses: JasonEtco/upload-to-release@master 18 | with: 19 | args: crowdsec-haproxy-bouncer.tgz application/x-gzip 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | with: 16 | config-name: release-drafter.yml 17 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 18 | # config-name: my-config.yml 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Crowdsec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_VERSION?="$(shell git for-each-ref --sort=-v:refname --count=1 --format '%(refname)' | cut -d '/' -f3)" 2 | OUTDIR="crowdsec-haproxy-bouncer-${BUILD_VERSION}/" 3 | LUA_MOD_DIR="${OUTDIR}lua-mod" 4 | CONFIG_DIR="${OUTDIR}config" 5 | OUT_ARCHIVE="crowdsec-haproxy-bouncer.tgz" 6 | LUA_BOUNCER_BRANCH?=main 7 | default: release 8 | release: 9 | mkdir -p ${LUA_MOD_DIR}/lib 10 | cp -r lib/* "${LUA_MOD_DIR}"/lib 11 | mkdir -p ${LUA_MOD_DIR}/templates 12 | cp -r templates/* "${LUA_MOD_DIR}"/templates 13 | 14 | cp community_blocklist.map ${LUA_MOD_DIR} 15 | cp crowdsec-haproxy-bouncer.conf ${LUA_MOD_DIR} 16 | 17 | cp install.sh ${OUTDIR} 18 | chmod +x ${OUTDIR}install.sh 19 | 20 | cp uninstall.sh ${OUTDIR} 21 | chmod +x ${OUTDIR}uninstall.sh 22 | 23 | cp upgrade.sh ${OUTDIR} 24 | chmod +x ${OUTDIR}upgrade.sh 25 | 26 | tar cvzf ${OUT_ARCHIVE} ${OUTDIR} 27 | rm -rf ${OUTDIR} 28 | 29 | clean: 30 | rm -rf "${OUTDIR}" 31 | rm -rf "${OUT_ARCHIVE}" 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | CrowdSec 3 |

4 |

5 | 6 | 7 |

8 |

9 | 📚 Documentation 10 | 💠 Hub 11 | 💬 Discourse 12 |

13 | 14 | 15 | 16 | # CrowdSec HAProxy Bouncer 17 | 18 | A lua bouncer for haproxy. 19 | 20 | ## How does it work ? 21 | 22 | This bouncer leverages haproxy lua's API. 23 | 24 | New/unknown IP are checked against crowdsec API, and if request should be blocked, a **403** is returned to the user, and put in cache. 25 | 26 | # Installation 27 | 28 | Please follow the [official documentation](https://doc.crowdsec.net/docs/next/bouncers/haproxy). 29 | -------------------------------------------------------------------------------- /community_blocklist.map: -------------------------------------------------------------------------------- 1 | # ip or range remediation -------------------------------------------------------------------------------- /crowdsec-haproxy-bouncer.conf: -------------------------------------------------------------------------------- 1 | ENABLED=true 2 | API_KEY=${API_KEY} 3 | # haproxy 4 | # path to community_blocklist.map 5 | MAP_PATH=/var/lib/crowdsec/lua/haproxy/community_blocklist.map 6 | # bounce for all type of remediation that the bouncer can receive from the local API 7 | BOUNCING_ON_TYPE=all 8 | FALLBACK_REMEDIATION=ban 9 | REQUEST_TIMEOUT=3000 10 | UPDATE_FREQUENCY=10 11 | # live or stream 12 | MODE=stream 13 | # exclude the bouncing on those location 14 | EXCLUDE_LOCATION= 15 | #those apply for "ban" action 16 | # /!\ REDIRECT_LOCATION and RET_CODE can't be used together. REDIRECT_LOCATION take priority over RET_CODE 17 | # path to ban template 18 | BAN_TEMPLATE_PATH= 19 | REDIRECT_LOCATION= 20 | RET_CODE= 21 | #those apply for "captcha" action 22 | # Captcha Secret Key 23 | SECRET_KEY= 24 | # captcha Site key 25 | SITE_KEY= 26 | # path to captcha template 27 | CAPTCHA_TEMPLATE_PATH=/var/lib/crowdsec/lua/haproxy/templates/captcha.html 28 | CAPTCHA_EXPIRATION=3600 29 | 30 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | crowdsec-haproxy-bouncer (1.0.0) UNRELEASED; urgency=medium 2 | 3 | * debian package 4 | 5 | -- Crowdsec Team Mon, 18 Jul 2022 09:38:06 +0100 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: crowdsec-haproxy-bouncer 2 | Maintainer: Crowdsec Team 3 | Build-Depends: debhelper, bash 4 | 5 | Package: crowdsec-haproxy-bouncer 6 | Provides: crowdsec-haproxy-bouncer 7 | Description: lua-based haproxy bouncer for Crowdsec 8 | Architecture: all 9 | 10 | -------------------------------------------------------------------------------- /debian/files: -------------------------------------------------------------------------------- 1 | crowdsec-haproxy-bouncer_1.0.0_all.buildinfo - - 2 | crowdsec-haproxy-bouncer_1.0.0_all.deb - - 3 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | 2 | systemctl daemon-reload 3 | 4 | 5 | START=0 6 | 7 | if [ "$1" = "configure" ]; then 8 | 9 | type cscli > /dev/null 10 | 11 | if [ "$?" -eq "0" ] ; then 12 | START=1 13 | echo "cscli/crowdsec is present, generating API key" 14 | unique=`date +%s` 15 | API_KEY=`cscli -oraw bouncers add haproxy-${unique}` 16 | if [ $? -eq 1 ] ; then 17 | echo "failed to create API token, service won't be started." 18 | START=0 19 | API_KEY="" 20 | else 21 | echo "API Key : ${API_KEY}" 22 | fi 23 | 24 | TMP=`mktemp -p /tmp/` 25 | cp /etc/crowdsec/bouncers/crowdsec-haproxy-bouncer.conf ${TMP} 26 | API_KEY=${API_KEY} envsubst < ${TMP} > /etc/crowdsec/bouncers/crowdsec-haproxy-bouncer.conf 27 | rm ${TMP} 28 | fi 29 | 30 | else 31 | START=1 32 | fi 33 | 34 | 35 | if [ ${START} -eq 0 ] ; then 36 | echo "no api key was generated" 37 | fi 38 | 39 | echo "Configure and restart haproxy to enable the crowdsec bouncer, follow official documentation : " 40 | echo "https://docs.crowdsec.net/docs/bouncers/haproxy#haproxy-configuration" 41 | echo "" 42 | echo "If you want to setup captcha remediation, follow official documentation : " 43 | echo "https://docs.crowdsec.net/docs/bouncers/haproxy#setup-captcha" 44 | -------------------------------------------------------------------------------- /debian/postrm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowdsecurity/cs-haproxy-bouncer/031e1e37fea48a97d9e102f660b47027510c5a9f/debian/postrm -------------------------------------------------------------------------------- /debian/prerm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowdsecurity/cs-haproxy-bouncer/031e1e37fea48a97d9e102f660b47027510c5a9f/debian/prerm -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export DEB_VERSION=$(shell dpkg-parsechangelog | egrep '^Version:' | cut -f 2 -d ' ') 4 | export BUILD_VERSION=v${DEB_VERSION}-debian-pragmatic 5 | 6 | 7 | %: 8 | dh $@ 9 | 10 | override_dh_systemd_start: 11 | echo "Not running dh_systemd_start" 12 | override_dh_auto_test: 13 | override_dh_auto_build: 14 | override_dh_auto_install: 15 | mkdir -p debian/crowdsec-haproxy-bouncer/usr/share/crowdsec-haproxy-bouncer/ 16 | 17 | mkdir -p debian/crowdsec-haproxy-bouncer/usr/lib/crowdsec/lua/haproxy/ 18 | mkdir -p debian/crowdsec-haproxy-bouncer/var/lib/crowdsec/lua/haproxy/templates/ 19 | 20 | cp -r lib/* debian/crowdsec-haproxy-bouncer/usr/lib/crowdsec/lua/haproxy/ 21 | cp -r templates/* debian/crowdsec-haproxy-bouncer/var/lib/crowdsec/lua/haproxy/templates/ 22 | 23 | cp community_blocklist.map debian/crowdsec-haproxy-bouncer/var/lib/crowdsec/lua/haproxy/community_blocklist.map 24 | 25 | mkdir -p debian/crowdsec-haproxy-bouncer/etc/crowdsec/bouncers/ 26 | cp crowdsec-haproxy-bouncer.conf debian/crowdsec-haproxy-bouncer/etc/crowdsec/bouncers/crowdsec-haproxy-bouncer.conf 27 | 28 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | This all-in-one docker-compose file allows you to run a simple environment for development. 4 | 5 | It can also help to show how you can deploy the bouncer using the haproxy docker image. 6 | 7 | The docker-compose contains : 8 | 9 | * haproxy 10 | * nginx-server 11 | * crowdsec 12 | 13 | Host share docker unix with crowdsec container so it can read from nginx container stdout. 14 | haproxy is configured to use haproxy-bouncer to query crowdsec. 15 | 16 | ## How to use 17 | 18 | It's already setup, you need to run 19 | ``` 20 | docker-compose up -d 21 | ``` 22 | 23 | Then you have the containers up and running. -------------------------------------------------------------------------------- /example/conf/acquis.yaml: -------------------------------------------------------------------------------- 1 | source: docker 2 | container_name: 3 | - example-nginx-server-1 4 | labels: 5 | type: nginx -------------------------------------------------------------------------------- /example/conf/crowdsec-haproxy-bouncer.conf: -------------------------------------------------------------------------------- 1 | ENABLED=true 2 | API_KEY=6b71a77194327e3bf00bcef884d2688c 3 | # haproxy 4 | # path to community_blocklist.map 5 | MAP_PATH=/var/lib/crowdsec/lua/haproxy/community_blocklist.map 6 | # bounce for all type of remediation that the bouncer can receive from the local API 7 | BOUNCING_ON_TYPE=all 8 | FALLBACK_REMEDIATION=ban 9 | REQUEST_TIMEOUT=3000 10 | UPDATE_FREQUENCY=10 11 | # live or stream 12 | MODE=stream 13 | # exclude the bouncing on those location 14 | EXCLUDE_LOCATION= 15 | #those apply for "ban" action 16 | # /!\ REDIRECT_LOCATION and RET_CODE can't be used together. REDIRECT_LOCATION take priority over RET_CODE 17 | # path to ban template 18 | BAN_TEMPLATE_PATH= 19 | REDIRECT_LOCATION= 20 | RET_CODE= 21 | #those apply for "captcha" action 22 | # ReCaptcha Secret Key 23 | SECRET_KEY= 24 | # Recaptcha Site key 25 | SITE_KEY= 26 | # path to captcha template 27 | CAPTCHA_TEMPLATE_PATH=/var/lib/crowdsec/lua/haproxy/templates/captcha.html 28 | CAPTCHA_EXPIRATION=3600 29 | 30 | -------------------------------------------------------------------------------- /example/conf/haproxy_local.cfg: -------------------------------------------------------------------------------- 1 | #HA Proxy Config 2 | global 3 | daemon 4 | maxconn 256 5 | 6 | # Crowdsec bouncer >>> 7 | lua-prepend-path /usr/local/crowdsec/lua/haproxy/?.lua 8 | lua-load /usr/local/crowdsec/lua/haproxy/crowdsec.lua # path to crowdsec.lua 9 | setenv CROWDSEC_CONFIG /usr/local/crowdsec/crowdsec-haproxy-bouncer.conf # path to crowdsec bouncer configuration file 10 | # Crowdsec bouncer <<< 11 | 12 | defaults 13 | mode http 14 | timeout connect 5000ms 15 | timeout client 50000ms 16 | timeout server 50000ms 17 | option forwardfor 18 | 19 | frontend http-in 20 | bind *:80 21 | option forwardfor header X-Real-IP 22 | http-request set-header X-Real-IP %[src] 23 | 24 | # Crowdsec bouncer >>> 25 | stick-table type ip size 10k expire 30m # declare a stick table to cache captcha verifications 26 | http-request lua.crowdsec_allow # action to identify crowdsec remediation 27 | http-request track-sc0 src if { var(req.remediation) -m str "captcha-allow" } # cache captcha allow decision 28 | http-request redirect location %[var(req.redirect_uri)] if { var(req.remediation) -m str "captcha-allow" } # redirect to initial url 29 | http-request use-service lua.reply_captcha if { var(req.remediation) -m str "captcha" } # serve captcha template if remediation is captcha 30 | http-request use-service lua.reply_ban if { var(req.remediation) -m str "ban" } # serve ban template if remediation is ban 31 | # Crowdsec bouncer <<< 32 | 33 | default_backend myAppBackEnd 34 | 35 | backend myAppBackEnd 36 | balance roundrobin 37 | server nginx-server nginx-server:80 check 38 | 39 | # Crowdsec bouncer >>> 40 | # define a backend for google to allow DNS resolution if using reCAPTCHA 41 | backend captcha_verifier 42 | server captcha_verifier www.recaptcha.net:443 check 43 | 44 | # define a backend for crowdsec to allow DNS resolution 45 | backend crowdsec 46 | server crowdsec crowdsec:8080 check 47 | # Crowdsec bouncer <<< 48 | -------------------------------------------------------------------------------- /example/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | events { } 2 | http { 3 | set_real_ip_from 172.0.0.0/8; 4 | real_ip_header X-Forwarded-For; 5 | server { 6 | listen 80; 7 | root /usr/share/nginx/html; 8 | index index.html index.htm; 9 | 10 | location / { 11 | try_files $uri $uri/ /index.html; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /example/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # docker compose file defininf simple echo server with haproxy in front 2 | # haproxy is listening on port 80 and 443 3 | 4 | version: '3.7' 5 | services: 6 | crowdsec: 7 | image: crowdsecurity/crowdsec:latest 8 | #ports: 9 | # - "8080:8080" 10 | volumes: 11 | - ./conf/acquis.yaml:/etc/crowdsec/acquis.yaml 12 | - /var/run/docker.sock:/var/run/docker.sock 13 | environment: 14 | DISABLE_PARSERS: "crowdsecurity/whitelists" 15 | COLLECTIONS: crowdsecurity/nginx 16 | DISABLE_ONLINE_API: "true" 17 | BOUNCER_KEY_haproxy: 6b71a77194327e3bf00bcef884d2688c 18 | depends_on: 19 | - nginx-server 20 | nginx-server: 21 | image: nginx:latest 22 | #ports: 23 | # - "8081:80" 24 | volumes: 25 | - ./conf/nginx.conf:/etc/nginx/nginx.conf 26 | haproxy: 27 | depends_on: 28 | - nginx-server 29 | - crowdsec 30 | image: haproxy:latest 31 | ports: 32 | - 80:80 33 | volumes: 34 | - ./conf/haproxy_local.cfg:/usr/local/etc/haproxy/haproxy.cfg 35 | - ../lib:/usr/local/crowdsec/lua/haproxy/ 36 | - ../templates:/var/lib/crowdsec/lua/haproxy 37 | - ../community_blocklist.map:/var/lib/crowdsec/lua/haproxy/community_blocklist.map 38 | - ./conf/crowdsec-haproxy-bouncer.conf:/usr/local/crowdsec/crowdsec-haproxy-bouncer.conf -------------------------------------------------------------------------------- /haproxy.cfg: -------------------------------------------------------------------------------- 1 | #HA Proxy Config 2 | global 3 | daemon 4 | maxconn 256 5 | 6 | # Crowdsec bouncer >>> 7 | ## On some systems (we only identified the issue with a custom build on centos 6), haproxy cannot validate the certificate of the captcha service. 8 | ## If you see an unexplained 503 error in haproxy logs, uncomment this line. 9 | #httpclient.ssl.verify none 10 | httpclient.resolvers.id captcha_dns_resolver #Tell the lua httpclient to use this DNS resolver. Replace with your own resolver if you already have one. 11 | lua-prepend-path /usr/lib/crowdsec/lua/haproxy/?.lua 12 | lua-load /usr/lib/crowdsec/lua/haproxy/crowdsec.lua # path to crowdsec.lua 13 | setenv CROWDSEC_CONFIG /etc/crowdsec/bouncers/crowdsec-haproxy-bouncer.conf # path to crowdsec bouncer configuration file 14 | # Crowdsec bouncer <<< 15 | 16 | defaults 17 | mode http 18 | timeout connect 5000ms 19 | timeout client 50000ms 20 | timeout server 50000ms 21 | 22 | frontend myApp 23 | bind *:80 24 | 25 | # Crowdsec bouncer >>> 26 | stick-table type ip size 10k expire 30m # declare a stick table to cache captcha verifications 27 | http-request lua.crowdsec_allow # action to identify crowdsec remediation 28 | http-request track-sc0 src if { var(req.remediation) -m str "captcha-allow" } # cache captcha allow decision 29 | http-request redirect location %[var(req.redirect_uri)] if { var(req.remediation) -m str "captcha-allow" } # redirect to initial url 30 | http-request use-service lua.reply_captcha if { var(req.remediation) -m str "captcha" } # serve captcha template if remediation is captcha 31 | http-request use-service lua.reply_ban if { var(req.remediation) -m str "ban" } # serve ban template if remediation is ban 32 | # Crowdsec bouncer <<< 33 | 34 | default_backend myAppBackEnd 35 | 36 | backend myAppBackEnd 37 | balance roundrobin 38 | server myAppServer1 nginx:80 check 39 | 40 | # Crowdsec bouncer >>> 41 | # define a backend for google to allow DNS resolution if using reCAPTCHA 42 | backend captcha_verifier 43 | server captcha_verifier www.recaptcha.net:443 check 44 | #server hcaptcha_verifier hcaptcha.com:443 check 45 | #server turnstile_verifier challenges.cloudflare.com:443 check 46 | 47 | # define a backend for crowdsec to allow DNS resolution 48 | backend crowdsec 49 | server crowdsec localhost:8080 check 50 | # Crowdsec bouncer <<< 51 | 52 | #This is required to allow the lua code to perform DNS resolution 53 | resolvers captcha_dns_resolver 54 | nameserver ns1 1.1.1.1:53 #You can change this to your own DNS resolver or another server 55 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LUA_MOD_DIR="./lua-mod" 4 | LIB_PATH="/usr/lib/crowdsec/lua/haproxy/" 5 | CONFIG_PATH="/etc/crowdsec/bouncers/" 6 | DATA_PATH="/var/lib/crowdsec/lua/haproxy/" 7 | SILENT="false" 8 | 9 | usage() { 10 | echo "Usage:" 11 | echo " ./install.sh -h Display this help message." 12 | echo " ./install.sh Install the bouncer in interactive mode" 13 | echo " ./install.sh -y Install the bouncer and accept everything" 14 | exit 0 15 | } 16 | 17 | 18 | #Accept cmdline arguments to overwrite options. 19 | while [[ $# -gt 0 ]] 20 | do 21 | case $1 in 22 | -y|--yes) 23 | SILENT="true" 24 | shift 25 | ;; 26 | -h|--help) 27 | usage 28 | ;; 29 | esac 30 | shift 31 | done 32 | 33 | 34 | gen_apikey() { 35 | 36 | type cscli > /dev/null 37 | 38 | if [ "$?" -eq "0" ] ; then 39 | SUFFIX=`tr -dc A-Za-z0-9 /dev/null 47 | } 48 | 49 | 50 | install() { 51 | sudo mkdir -p ${LIB_PATH}/plugins/crowdsec/ 52 | sudo mkdir -p ${DATA_PATH}/templates/ 53 | 54 | sudo cp -r ${LUA_MOD_DIR}/lib/* ${LIB_PATH}/ 55 | sudo cp -r ${LUA_MOD_DIR}/templates/* ${DATA_PATH}/templates/ 56 | sudo cp ${LUA_MOD_DIR}/community_blocklist.map ${DATA_PATH} 57 | } 58 | 59 | 60 | gen_apikey 61 | install 62 | 63 | 64 | echo "crowdsec-haproxy-bouncer installed successfully" -------------------------------------------------------------------------------- /lib/crowdsec.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path .. ";./?.lua" 2 | 3 | local json = require "json" 4 | local config = require "plugins.crowdsec.config" 5 | local captcha = require "plugins.crowdsec.captcha" 6 | local ban = require "plugins.crowdsec.ban" 7 | local utils = require "plugins.crowdsec.utils" 8 | 9 | local runtime = {} 10 | 11 | -- Called after the configuration is parsed. 12 | -- Loads the configuration 13 | local function init() 14 | configFile = os.getenv("CROWDSEC_CONFIG") 15 | local conf, err = config.loadConfig(configFile) 16 | if conf == nil then 17 | core.Alert(err) 18 | return nil 19 | end 20 | runtime.conf = conf 21 | runtime.fallback = runtime.conf["FALLBACK_REMEDIATION"] or "ban" 22 | 23 | if core.backends["crowdsec"] == nil then 24 | error("no crowdsec backend provided: crowdsec connection must be provided as backend named crowdsec") 25 | end 26 | if core.backends["crowdsec"].servers["crowdsec"] == nil then 27 | error("no crowdsec backend provided: crowdsec connection must be provided as backend named crowdsec having a server named crowdsec") 28 | end 29 | 30 | runtime.captcha_ok = true 31 | local err = captcha.New(runtime.conf["SITE_KEY"], runtime.conf["SECRET_KEY"], runtime.conf["CAPTCHA_TEMPLATE_PATH"]) 32 | if err ~= nil then 33 | core.Alert("error loading captcha plugin: " .. err) 34 | runtime.captcha_ok = false 35 | end 36 | 37 | if runtime.conf["REDIRECT_LOCATION"] ~= "" then 38 | table.insert(runtime.conf["EXCLUDE_LOCATION"], runtime.conf["REDIRECT_LOCATION"]) 39 | end 40 | local err = ban.New(runtime.conf["BAN_TEMPLATE_PATH"], runtime.conf["REDIRECT_LOCATION"], runtime.conf["RET_CODE"]) 41 | if err ~= nil then 42 | core.Alert("error loading ban plugin: " .. err) 43 | end 44 | 45 | runtime.map = Map.new(conf["MAP_PATH"], Map._ip) 46 | end 47 | 48 | local function urldecode(str) 49 | str = string.gsub (str, "+", " ") 50 | str = string.gsub (str, "%%(%x%x)", function(h) return string.char(tonumber(h,16)) end) 51 | return str 52 | end 53 | 54 | local function remediate_allow(txn) 55 | txn:set_var("req.remediation", nil) 56 | return nil 57 | end 58 | 59 | local function remediate_fallback(txn) 60 | txn:set_var("req.remediation", runtime.fallback) 61 | return nil 62 | end 63 | 64 | -- Called in live mode 65 | -- interrogate Crowdsec in realtime to get a decision 66 | local function get_live_remediation(txn, source_ip) 67 | -- check in cache 68 | remediation_exp = runtime.map:lookup(source_ip) 69 | time_now = os.time() 70 | if remediation_exp ~= nil then 71 | -- extract expiration 72 | for remediation, expiration in string.gmatch(remediation_exp, "(%w+),(%d+)") do 73 | if expiration ~= nil and tonumber(expiration) >= time_now then 74 | if remediation == "null" then 75 | return nil 76 | end 77 | return remediation 78 | end 79 | end 80 | end 81 | 82 | local link = "http://" .. core.backends["crowdsec"].servers["crowdsec"]:get_addr() .. "/v1/decisions?ip=" .. source_ip 83 | 84 | core.Debug("Fetching decision for ip="..source_ip) 85 | local response = core.httpclient():get{ 86 | url=link, 87 | headers={ 88 | ["X-Api-Key"]={runtime.conf["API_KEY"]}, 89 | ["Connection"]={"keep-alive"}, 90 | ["User-Agent"]={"crowdsec-haproxy-bouncer/v1.0.0"} 91 | }, 92 | timeout=2*60*1000 93 | } 94 | core.Debug("Response: "..tostring(response.status).." ("..response.body..")") 95 | if response == nil then 96 | core.Alert("Got error fetching decisions from Crowdsec (unknown)") 97 | return nil 98 | end 99 | if response.status ~= 200 then 100 | core.Alert("Got error fetching decisions from Crowdsec: "..tostring(response.status).." ("..response.body..")") 101 | return nil 102 | end 103 | local body = response.body 104 | core.Debug("Decision fetched ip="..source_ip.."="..tostring(body)) 105 | 106 | if body == "null" then 107 | -- ip unknown 108 | core.set_map(runtime.conf["MAP_PATH"], source_ip, string.format("null,%d", time_now+runtime.conf["CACHE_EXPIRATION"])) 109 | return nil 110 | end 111 | 112 | local decisions = json.decode(body) 113 | 114 | -- add to cache 115 | core.set_map(runtime.conf["MAP_PATH"], source_ip, string.format("%s,%d", decisions[1].type, time_now+runtime.conf["CACHE_EXPIRATION"])) 116 | 117 | return decisions[1].type 118 | end 119 | 120 | -- Called for each request 121 | -- check the blocklists and decide of the remediation 122 | local function allow(txn) 123 | if runtime.conf["ENABLED"] == "false" then 124 | return remediate_allow(txn) 125 | end 126 | 127 | local source_ip = txn.f:src() 128 | 129 | core.Debug("Request from "..source_ip) 130 | 131 | local remediation = nil 132 | if runtime.conf["MODE"] == "stream" then 133 | remediation = runtime.map:lookup(source_ip) 134 | else 135 | remediation = get_live_remediation(txn, source_ip) 136 | end 137 | 138 | if remediation == nil then 139 | return remediate_allow(txn) 140 | end 141 | 142 | core.Debug("Active decision "..tostring(remediation).." for "..source_ip) 143 | 144 | -- whitelists 145 | if utils.table_len(runtime.conf["EXCLUDE_LOCATION"]) > 0 then 146 | for k, v in pairs(runtime.conf["EXCLUDE_LOCATION"]) do 147 | if txn.sf:path() == v then 148 | return remediate_allow(txn) 149 | end 150 | local uri_to_check = v 151 | if utils.ends_with(uri_to_check, "/") == false then 152 | uri_to_check = uri_to_check .. "/" 153 | end 154 | if utils.starts_with(txn.sf:path(), uri_to_check) then 155 | return remediate_allow(txn) 156 | end 157 | end 158 | end 159 | 160 | -- captcha 161 | if remediation == "captcha" then 162 | if runtime.captcha_ok == false then 163 | return remediate_fallback(txn) 164 | end 165 | local stk = core.frontends[txn.f:fe_name()].stktable 166 | if stk == nil then 167 | core.Alert("Stick table not defined in frontend "..txn.f:fe_name()..". Cannot cache captcha verifications") 168 | return remediate_fallback(txn) 169 | end 170 | if stk:lookup(source_ip) ~= nil then 171 | return remediate_allow(txn) 172 | end 173 | 174 | if txn.sf:method() == "POST" then 175 | local count = 0 176 | while tonumber(txn.sf:req_body_len()) == 0 and count < 10 do 177 | core.msleep(50) 178 | count = count + 1 179 | end 180 | end 181 | 182 | -- captcha response ? 183 | local captcha_resp = txn.sf:req_body_param(captcha.GetCaptchaBackendKey()) 184 | if captcha_resp ~= "" then 185 | valid, err = captcha.Validate(captcha_resp, source_ip) 186 | if err then 187 | core.Alert("error validating captcha: "..err.."; validator: "..core.backends["captcha_verifier"].servers[captcha.CaptchaServerName]:get_addr()) 188 | end 189 | if valid then 190 | -- valid, redirect to redirectUri 191 | txn:set_var("req.redirect_uri", urldecode(txn.sf:req_body_param("redirect_uri"))) 192 | remediation = "captcha-allow" 193 | end 194 | end 195 | end 196 | 197 | txn:set_var("req.remediation", remediation) 198 | end 199 | 200 | -- Called from task 201 | -- load decisions from LAPI 202 | local function refresh_decisions(is_startup) 203 | core.Debug("Stream Query with startup "..tostring(is_startup)) 204 | -- TODO: get protocol from config 205 | local link = "http://" .. core.backends["crowdsec"].servers["crowdsec"]:get_addr() .. "/v1/decisions/stream?startup=" .. tostring(is_startup) 206 | 207 | core.Debug("Start fetching decisions: startup="..tostring(is_startup)) 208 | local response = core.httpclient():get{ 209 | url=link, 210 | headers={ 211 | ["X-Api-Key"]={runtime.conf["API_KEY"]}, 212 | ["Connection"]={"keep-alive"}, 213 | ["User-Agent"]={"crowdsec-haproxy-bouncer/v1.0.0"} 214 | }, 215 | timeout=2*60*1000 216 | } 217 | if response == nil then 218 | core.Alert("Got error fetching decisions from Crowdsec (unknown)") 219 | return false 220 | end 221 | if response.status ~= 200 then 222 | core.Alert("Got error fetching decisions from Crowdsec: "..response.status.." ("..response.body..")") 223 | return false 224 | end 225 | local body = response.body 226 | core.Debug("Decisions fetched: startup="..tostring(is_startup)) 227 | 228 | local decisions = json.decode(body) 229 | 230 | if decisions.deleted == nil and decisions.new == nil then 231 | return true 232 | end 233 | 234 | -- process deleted decisions 235 | if type(decisions.deleted) == "table" then 236 | if not is_startup then 237 | for i, decision in pairs(decisions.deleted) do 238 | core.Debug("Delete decision "..decision.value) 239 | core.del_map(runtime.conf["MAP_PATH"], decision.value) 240 | -- This is very important: set_map takes an internal lock when called 241 | -- If the bouncers gets a lot of IPs to delete, we will block the whole process 242 | -- preventing it from answering any request, and the process will restart 243 | -- after 30s. 244 | core.msleep(1) 245 | end 246 | end 247 | end 248 | 249 | -- process new decisions 250 | if type(decisions.new) == "table" then 251 | for i, decision in pairs(decisions.new) do 252 | if runtime.conf["BOUNCING_ON_TYPE"] == decision.type or runtime.conf["BOUNCING_ON_TYPE"] == "all" then 253 | core.Debug("Add decision "..decision.value) 254 | core.set_map(runtime.conf["MAP_PATH"], decision.value, decision.type) 255 | -- This is very important: set_map takes an internal lock when called 256 | -- If the bouncers gets a lot of IPs, we will block the whole process 257 | -- preventing it from answering any request, and the process will restart 258 | -- after 30s. 259 | core.msleep(1) 260 | end 261 | end 262 | end 263 | 264 | return true 265 | end 266 | 267 | -- Task 268 | -- refresh decisions periodically 269 | local function refresh_decisions_task() 270 | if runtime.conf["ENABLED"] == "false" then 271 | return 272 | end 273 | 274 | if runtime.conf["MODE"] ~= "stream" then 275 | return 276 | end 277 | 278 | local is_first_fetch = true 279 | while true do 280 | local succes = refresh_decisions(is_first_fetch) 281 | if succes then 282 | is_first_fetch = false 283 | end 284 | core.sleep(runtime.conf["UPDATE_FREQUENCY"]) 285 | end 286 | end 287 | 288 | -- Registers 289 | core.register_init(init) 290 | core.register_action("crowdsec_allow", { 'tcp-req', 'tcp-res', 'http-req', 'http-res' }, allow, 0) 291 | core.register_service("reply_captcha", "http", captcha.ReplyCaptcha) 292 | core.register_service("reply_ban", "http", ban.ReplyBan) 293 | core.register_task(refresh_decisions_task) 294 | -------------------------------------------------------------------------------- /lib/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2020 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.2" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\", 35 | [ "\"" ] = "\"", 36 | [ "\b" ] = "b", 37 | [ "\f" ] = "f", 38 | [ "\n" ] = "n", 39 | [ "\r" ] = "r", 40 | [ "\t" ] = "t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if rawget(val, 1) ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(1, 4), 16 ) 208 | local n2 = tonumber( s:sub(7, 10), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local res = "" 220 | local j = i + 1 221 | local k = j 222 | 223 | while j <= #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | 229 | elseif x == 92 then -- `\`: Escape 230 | res = res .. str:sub(k, j - 1) 231 | j = j + 1 232 | local c = str:sub(j, j) 233 | if c == "u" then 234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 235 | or str:match("^%x%x%x%x", j + 1) 236 | or decode_error(str, j - 1, "invalid unicode escape in string") 237 | res = res .. parse_unicode_escape(hex) 238 | j = j + #hex 239 | else 240 | if not escape_chars[c] then 241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 242 | end 243 | res = res .. escape_char_map_inv[c] 244 | end 245 | k = j + 1 246 | 247 | elseif x == 34 then -- `"`: End of string 248 | res = res .. str:sub(k, j - 1) 249 | return res, j + 1 250 | end 251 | 252 | j = j + 1 253 | end 254 | 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | -- Read ':' delimiter 324 | i = next_char(str, i, space_chars, true) 325 | if str:sub(i, i) ~= ":" then 326 | decode_error(str, i, "expected ':' after key") 327 | end 328 | i = next_char(str, i + 1, space_chars, true) 329 | -- Read value 330 | val, i = parse(str, i) 331 | -- Set 332 | res[key] = val 333 | -- Next token 334 | i = next_char(str, i, space_chars, true) 335 | local chr = str:sub(i, i) 336 | i = i + 1 337 | if chr == "}" then break end 338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 339 | end 340 | return res, i 341 | end 342 | 343 | 344 | local char_func_map = { 345 | [ '"' ] = parse_string, 346 | [ "0" ] = parse_number, 347 | [ "1" ] = parse_number, 348 | [ "2" ] = parse_number, 349 | [ "3" ] = parse_number, 350 | [ "4" ] = parse_number, 351 | [ "5" ] = parse_number, 352 | [ "6" ] = parse_number, 353 | [ "7" ] = parse_number, 354 | [ "8" ] = parse_number, 355 | [ "9" ] = parse_number, 356 | [ "-" ] = parse_number, 357 | [ "t" ] = parse_literal, 358 | [ "f" ] = parse_literal, 359 | [ "n" ] = parse_literal, 360 | [ "[" ] = parse_array, 361 | [ "{" ] = parse_object, 362 | } 363 | 364 | 365 | parse = function(str, idx) 366 | local chr = str:sub(idx, idx) 367 | local f = char_func_map[chr] 368 | if f then 369 | return f(str, idx) 370 | end 371 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 372 | end 373 | 374 | 375 | function json.decode(str) 376 | if type(str) ~= "string" then 377 | error("expected argument of type string, got " .. type(str)) 378 | end 379 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 380 | idx = next_char(str, idx, space_chars, true) 381 | if idx <= #str then 382 | decode_error(str, idx, "trailing garbage") 383 | end 384 | return res 385 | end 386 | 387 | 388 | return json -------------------------------------------------------------------------------- /lib/plugins/crowdsec/ban.lua: -------------------------------------------------------------------------------- 1 | local utils = require "plugins.crowdsec.utils" 2 | 3 | 4 | local M = {_TYPE='module', _NAME='ban.funcs', _VERSION='1.0-0'} 5 | 6 | M.template_str = "" 7 | M.redirect_location = "" 8 | M.ret_code = 403 9 | 10 | 11 | function M.New(template_path, redirect_location, ret_code) 12 | M.redirect_location = redirect_location 13 | 14 | ret_code_ok = false 15 | if ret_code ~= nil and ret_code ~= 0 and ret_code ~= "" then 16 | ret_code_ok = true 17 | M.ret_code = ret_code 18 | end 19 | 20 | template_file_ok = false 21 | if (template_path ~= nil and template_path ~= "" and utils.file_exist(template_path) == true) then 22 | M.template_str = utils.read_file(template_path) 23 | if M.template_str ~= nil then 24 | template_file_ok = true 25 | end 26 | end 27 | 28 | if template_file_ok == false and (M.redirect_location == nil or M.redirect_location == "") then 29 | core.Alert("BAN_TEMPLATE_PATH and REDIRECT_LOCATION variable are empty, will return HTTP " .. M.ret_code .. " for ban decisions") 30 | end 31 | 32 | return nil 33 | end 34 | 35 | 36 | 37 | function M.ReplyBan(applet) 38 | if M.redirect_location ~= "" then 39 | applet:set_status(302) 40 | applet:add_header("location", M.redirect_location) 41 | applet:start_response() 42 | applet:send() 43 | elseif M.template_str ~= "" and utils.accept_html(applet) == true then 44 | local response = M.template_str 45 | applet:set_status(200) 46 | applet:add_header("content-length", string.len(response)) 47 | applet:add_header("content-type", "text/html") 48 | applet:start_response() 49 | applet:send(response) 50 | else 51 | applet:set_status(M.ret_code) 52 | applet:start_response() 53 | applet:send("Access forbidden") 54 | end 55 | end 56 | 57 | return M -------------------------------------------------------------------------------- /lib/plugins/crowdsec/captcha.lua: -------------------------------------------------------------------------------- 1 | local json = require "json" 2 | local template = require "plugins.crowdsec.template" 3 | local utils = require "plugins.crowdsec.utils" 4 | 5 | 6 | local M = {_TYPE='module', _NAME='recaptcha.funcs', _VERSION='1.0-0'} 7 | 8 | local captcha_backend_url = {} 9 | captcha_backend_url["recaptcha"] = "/recaptcha/api/siteverify" 10 | captcha_backend_url["captcha"] = "/recaptcha/api/siteverify" 11 | captcha_backend_url["hcaptcha"] = "/siteverify" 12 | captcha_backend_url["turnstile"] = "/turnstile/v0/siteverify" 13 | 14 | local captcha_backend_host = {} 15 | captcha_backend_host["recaptcha"] = "www.recaptcha.net" 16 | captcha_backend_host["captcha"] = "www.recaptcha.net" 17 | captcha_backend_host["hcaptcha"] = "hcaptcha.com" 18 | captcha_backend_host["turnstile"] = "challenges.cloudflare.com" 19 | 20 | local captcha_frontend_js = {} 21 | captcha_frontend_js["recaptcha"] = "https://www.recaptcha.net/recaptcha/api.js" 22 | captcha_frontend_js["captcha"] = "https://www.recaptcha.net/recaptcha/api.js" 23 | captcha_frontend_js["hcaptcha"] = "https://js.hcaptcha.com/1/api.js" 24 | captcha_frontend_js["turnstile"] = "https://challenges.cloudflare.com/turnstile/v0/api.js" 25 | 26 | local captcha_frontend_key = {} 27 | captcha_frontend_key["recaptcha"] = "g-recaptcha" 28 | captcha_frontend_key["captcha"] = "g-recaptcha" 29 | captcha_frontend_key["hcaptcha"] = "h-captcha" 30 | captcha_frontend_key["turnstile"] = "cf-turnstile" 31 | 32 | M._VERIFY_STATE = "to_verify" 33 | M._VALIDATED_STATE = "validated" 34 | 35 | 36 | M.State = {} 37 | M.State["1"] = M._VERIFY_STATE 38 | M.State["2"] = M._VALIDATED_STATE 39 | 40 | M.SecretKey = "" 41 | M.SiteKey = "" 42 | M.Template = "" 43 | 44 | 45 | function M.GetStateID(state) 46 | for k, v in pairs(M.State) do 47 | if v == state then 48 | return tonumber(k) 49 | end 50 | end 51 | return nil 52 | end 53 | 54 | function M.New(siteKey, secretKey, TemplateFilePath) 55 | if siteKey == nil or siteKey == "" then 56 | return "no recaptcha site key provided, can't use captcha" 57 | end 58 | 59 | M.SiteKey = siteKey 60 | 61 | if secretKey == nil or secretKey == "" then 62 | return "no recaptcha secret key provided, can't use captcha" 63 | end 64 | 65 | M.SecretKey = secretKey 66 | 67 | -- for loop over core.backends 68 | 69 | if core.backends["captcha_verifier"] == nil then 70 | return "no verifier backend provided, can't use captcha" 71 | end 72 | for k, v in pairs(core.backends["captcha_verifier"].servers) do 73 | M.CaptchaProvider = utils.split(v.name, "_")[1] 74 | M.CaptchaServerName = v.name 75 | end 76 | if M.CaptchaProvider == nil then 77 | return "no verifier backend provided, can't use captcha" 78 | end 79 | if TemplateFilePath == nil then 80 | return "CAPTCHA_TEMPLATE_PATH variable is empty, will ban without template" 81 | end 82 | if utils.file_exist(TemplateFilePath) == false then 83 | return "captcha template file doesn't exist, can't use captcha" 84 | end 85 | 86 | local captcha_template = utils.read_file(TemplateFilePath) 87 | if captcha_template == nil then 88 | return "Template file " .. TemplateFilePath .. "not found." 89 | end 90 | 91 | M.Template = captcha_template 92 | 93 | return nil 94 | end 95 | 96 | function M.GetTemplate(template_data) 97 | template_data["captcha_site_key"] = M.SiteKey 98 | template_data["captcha_frontend_js"] = captcha_frontend_js[M.CaptchaProvider] 99 | template_data["captcha_frontend_key"] = captcha_frontend_key[M.CaptchaProvider] 100 | return template.compile(M.Template, template_data) 101 | end 102 | 103 | function M.GetCaptchaBackendKey() 104 | return captcha_frontend_key[M.CaptchaProvider] .. "-response" 105 | end 106 | 107 | local function table_to_encoded_url(args) 108 | local params = {} 109 | for k, v in pairs(args) do table.insert(params, k .. '=' .. v) end 110 | return table.concat(params, "&") 111 | end 112 | 113 | function M.Validate(captcha_res, remote_ip) 114 | local body = { 115 | secret = M.SecretKey, 116 | response = captcha_res, 117 | remoteip = remote_ip 118 | } 119 | 120 | local verifier_ip = core.backends["captcha_verifier"].servers[M.CaptchaServerName]:get_addr() 121 | local data = table_to_encoded_url(body) 122 | local status, res = pcall(function() 123 | return core.httpclient():post{ 124 | url= "https://" .. captcha_backend_host[M.CaptchaProvider] .. captcha_backend_url[M.CaptchaProvider], 125 | body=data, 126 | headers={ 127 | ["Content-Type"] = {"application/x-www-form-urlencoded"}, 128 | ["Host"] = {captcha_backend_host[M.CaptchaProvider]} 129 | }, 130 | timeout=2000 131 | } 132 | end) 133 | if status == false then 134 | core.Alert("error verifying captcha: "..res.."; verifier: "..verifier_ip) 135 | return false, res 136 | end 137 | 138 | if res.status ~= 200 then 139 | core.Alert("error verifying captcha: "..res.status..","..res.body.."; verifier: "..verifier_ip) 140 | return false, res.body 141 | end 142 | if res.body:sub(1,1) ~= "{" then 143 | core.Alert("error recieved non json response: "..res.body) 144 | return false, res.body 145 | end 146 | local result = json.decode(res.body) 147 | 148 | if result.success == false then 149 | for k, v in pairs(result["error-codes"]) do 150 | if v == "invalid-input-secret" then 151 | core.Alert("reCaptcha secret key is invalid") 152 | return true, nil 153 | end 154 | end 155 | end 156 | 157 | return result.success, nil 158 | end 159 | 160 | -- Service implementation 161 | -- respond with captcha template 162 | function M.ReplyCaptcha(applet) 163 | -- block if accept is not text/html to avoid serving html when the client expect image or json 164 | if utils.accept_html(applet) == false then 165 | applet:set_status(403) 166 | applet:start_response() 167 | applet:send("Access forbidden") 168 | return 169 | end 170 | 171 | local redirect_uri = applet.path 172 | if applet.method:lower() ~= "get" then 173 | redirect_uri = "/" 174 | end 175 | local response = M.GetTemplate({["redirect_uri"]=redirect_uri}) 176 | applet:set_status(200) 177 | applet:add_header("content-length", string.len(response)) 178 | applet:add_header("content-type", "text/html") 179 | applet:start_response() 180 | applet:send(response) 181 | end 182 | 183 | return M -------------------------------------------------------------------------------- /lib/plugins/crowdsec/config.lua: -------------------------------------------------------------------------------- 1 | local config = {} 2 | 3 | function config.file_exists(file) 4 | local f = io.open(file, "rb") 5 | if f then 6 | f:close() 7 | end 8 | return f ~= nil 9 | end 10 | 11 | function split(s, delimiter) 12 | result = {}; 13 | for match in (s..delimiter):gmatch("(.-)"..delimiter.."(.-)") do 14 | table.insert(result, match); 15 | end 16 | return result; 17 | end 18 | 19 | local function has_value (tab, val) 20 | for index, value in ipairs(tab) do 21 | if value == val then 22 | return true 23 | end 24 | end 25 | 26 | return false 27 | end 28 | 29 | local function starts_with(str, start) 30 | return str:sub(1, #start) == start 31 | end 32 | 33 | local function trim(s) 34 | return (string.gsub(s, "^%s*(.-)%s*$", "%1")) 35 | end 36 | 37 | 38 | function config.loadConfig(file) 39 | if not config.file_exists(file) then 40 | return nil, "File ".. file .." doesn't exist" 41 | end 42 | local conf = {} 43 | local valid_params = {'ENABLED', 'API_URL', 'API_KEY', 'MAP_PATH', 'BOUNCING_ON_TYPE', 'MODE', 'SECRET_KEY', 'SITE_KEY', 'BAN_TEMPLATE_PATH' ,'CAPTCHA_TEMPLATE_PATH', 'REDIRECT_LOCATION', 'RET_CODE', 'EXCLUDE_LOCATION', 'FALLBACK_REMEDIATION'} 44 | local valid_int_params = {'HAPROXY_ADMIN_PORT', 'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY', 'CAPTCHA_EXPIRATION'} 45 | local valid_bouncing_on_type_values = {'ban', 'captcha', 'all'} 46 | local valid_truefalse_values = {'false', 'true'} 47 | local default_values = { 48 | ['ENABLED'] = "true", 49 | ['REQUEST_TIMEOUT'] = 0.2, 50 | ['BOUNCING_ON_TYPE'] = "ban", 51 | ['MODE'] = "stream", 52 | ['UPDATE_FREQUENCY'] = 10, 53 | ['CAPTCHA_EXPIRATION'] = 3600, 54 | ['CACHE_EXPIRATION'] = 1, 55 | ['REDIRECT_LOCATION'] = "", 56 | ['EXCLUDE_LOCATION'] = {}, 57 | ['RET_CODE'] = 0 58 | } 59 | for line in io.lines(file) do 60 | local isOk = false 61 | if starts_with(line, "#") then 62 | isOk = true 63 | end 64 | if trim(line) == "" then 65 | isOk = true 66 | end 67 | if not isOk then 68 | local s = split(line, "=") 69 | for k, v in pairs(s) do 70 | if has_value(valid_params, v) then 71 | if v == "ENABLED" then 72 | local value = s[2] 73 | if not has_value(valid_truefalse_values, s[2]) then 74 | core.Alert("unsupported value '" .. s[2] .. "' for variable '" .. v .. "'. Using default value 'true' instead") 75 | break 76 | end 77 | end 78 | if v == "BOUNCING_ON_TYPE" then 79 | local value = s[2] 80 | if not has_value(valid_bouncing_on_type_values, s[2]) then 81 | core.Alert("unsupported value '" .. s[2] .. "' for variable '" .. v .. "'. Using default value 'ban' instead") 82 | break 83 | end 84 | end 85 | if v == "MODE" then 86 | local value = s[2] 87 | if not has_value({'stream', 'live'}, s[2]) then 88 | core.Alert("unsupported value '" .. s[2] .. "' for variable '" .. v .. "'. Using default value 'stream' instead") 89 | break 90 | end 91 | end 92 | if v == "EXCLUDE_LOCATION" then 93 | local value = s[2] 94 | exclude_location = {} 95 | if value ~= "" then 96 | for match in (value..","):gmatch("(.-)"..",") do 97 | table.insert(exclude_location, match) 98 | end 99 | end 100 | local n = next(s, k) 101 | conf[v] = exclude_location 102 | break 103 | end 104 | if v == "FALLBACK_REMEDIATION" then 105 | local value = s[2] 106 | if not has_value({'captcha', 'ban'}, s[2]) then 107 | core.Alert("unsupported value '" .. s[2] .. "' for variable '" .. v .. "'. Using default value 'ban' instead") 108 | local n = next(s, k) 109 | conf[v] = "ban" 110 | break 111 | end 112 | end 113 | local n = next(s, k) 114 | conf[v] = s[n] 115 | break 116 | elseif has_value(valid_int_params, v) then 117 | local n = next(s, k) 118 | conf[v] = tonumber(s[n]) 119 | break 120 | else 121 | core.Alert("unsupported configuration '" .. v .. "'") 122 | break 123 | end 124 | end 125 | end 126 | end 127 | for k, v in pairs(default_values) do 128 | if conf[k] == nil then 129 | conf[k] = v 130 | end 131 | end 132 | return conf, nil 133 | end 134 | return config 135 | -------------------------------------------------------------------------------- /lib/plugins/crowdsec/template.lua: -------------------------------------------------------------------------------- 1 | local template = {} 2 | 3 | function template.compile(template_str, args) 4 | 5 | for k, v in pairs(args) do 6 | local var = "{{" .. k .. "}}" 7 | template_str = template_str:gsub(var, v) 8 | end 9 | 10 | return template_str 11 | end 12 | 13 | return template -------------------------------------------------------------------------------- /lib/plugins/crowdsec/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | 4 | function M.read_file(path) 5 | local file = io.open(path, "r") -- r read mode and b binary mode 6 | if not file then return nil end 7 | io.input(file) 8 | content = io.read("*a") 9 | io.close(file) 10 | return content 11 | end 12 | 13 | function M.file_exist(path) 14 | if path == nil then 15 | return nil 16 | end 17 | local f = io.open(path, "r") 18 | if f ~= nil then 19 | io.close(f) 20 | return true 21 | else 22 | return false 23 | end 24 | end 25 | 26 | function M.starts_with(str, start) 27 | return str:sub(1, #start) == start 28 | end 29 | 30 | function M.ends_with(str, ending) 31 | return ending == "" or str:sub(-#ending) == ending 32 | end 33 | 34 | function M.table_len(table) 35 | local count = 0 36 | for k, v in pairs(table) do 37 | count = count + 1 38 | end 39 | return count 40 | end 41 | 42 | function M.accept_html(applet) 43 | if applet.headers["accept"] == nil then 44 | return true 45 | end 46 | for _, accept in pairs(applet.headers["accept"]) do 47 | for _, value in pairs({"*/*", "text", "html"}) do 48 | local found_min, found_max = string.find(accept, value) 49 | if found_min ~= nil then 50 | return true 51 | end 52 | end 53 | end 54 | return false 55 | end 56 | 57 | function M.split(str, sep) 58 | local sep, fields = sep or ":", {} 59 | local pattern = string.format("([^%s]+)", sep) 60 | str:gsub(pattern, function(c) fields[#fields+1] = c end) 61 | return fields 62 | end 63 | 64 | return M 65 | -------------------------------------------------------------------------------- /rpm/SPECS/crowdsec-haproxy-bouncer.spec: -------------------------------------------------------------------------------- 1 | Name: crowdsec-haproxy-bouncer 2 | Version: %(echo $VERSION) 3 | Release: %(echo $PACKAGE_NUMBER)%{?dist} 4 | Summary: Haproxy bouncer for Crowdsec 5 | 6 | License: MIT 7 | URL: https://crowdsec.net 8 | Source0: https://github.com/crowdsecurity/%{name}/archive/v%(echo $VERSION).tar.gz 9 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 10 | 11 | BuildRequires: git 12 | BuildRequires: make 13 | Requires: haproxy 14 | Requires: lua 15 | %{?fc33:BuildRequires: systemd-rpm-macros} 16 | 17 | %define debug_package %{nil} 18 | 19 | %description 20 | 21 | %define version_number %(echo $VERSION) 22 | %define releasever %(echo $RELEASEVER) 23 | %global local_version v%{version_number}-%{releasever}-rpm 24 | %global name crowdsec-haproxy-bouncer 25 | %global __mangle_shebangs_exclude_from /usr/bin/env 26 | 27 | %prep 28 | %setup -n crowdsec-haproxy-bouncer-%{version} 29 | 30 | %install 31 | rm -rf %{buildroot} 32 | mkdir -p %{buildroot}%{_sysconfdir}/crowdsec/bouncers 33 | mkdir -p %{buildroot}%{_sharedstatedir}/crowdsec/lua/haproxy/templates/ 34 | mkdir -p %{buildroot}%{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec 35 | 36 | install -m 600 -D %{name}.conf %{buildroot}%{_sysconfdir}/crowdsec/bouncers/%{name}.conf 37 | 38 | install -m 644 lib/crowdsec.lua %{buildroot}%{_libdir}/crowdsec/lua/haproxy 39 | install -m 644 lib/json.lua %{buildroot}%{_libdir}/crowdsec/lua/haproxy 40 | install -m 644 lib/plugins/crowdsec/recaptcha.lua %{buildroot}%{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec 41 | install -m 644 lib/plugins/crowdsec/template.lua %{buildroot}%{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec 42 | install -m 644 lib/plugins/crowdsec/config.lua %{buildroot}%{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec 43 | install -m 644 lib/plugins/crowdsec/ban.lua %{buildroot}%{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec 44 | install -m 644 lib/plugins/crowdsec/utils.lua %{buildroot}%{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec 45 | 46 | install -m 644 templates/captcha.html %{buildroot}%{_sharedstatedir}/crowdsec/lua/haproxy/templates/ 47 | install -m 644 templates/ban.html %{buildroot}%{_sharedstatedir}/crowdsec/lua/haproxy/templates/ 48 | install -m 644 community_blocklist.map %{buildroot}%{_sharedstatedir}/crowdsec/lua/haproxy 49 | %clean 50 | rm -rf %{buildroot} 51 | 52 | %files 53 | %{_libdir}/crowdsec/lua/haproxy/crowdsec.lua 54 | %{_libdir}/crowdsec/lua/haproxy/json.lua 55 | %{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec/recaptcha.lua 56 | %{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec/template.lua 57 | %{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec/config.lua 58 | %{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec/ban.lua 59 | %{_libdir}/crowdsec/lua/haproxy/plugins/crowdsec/utils.lua 60 | %{_sharedstatedir}/crowdsec/lua/haproxy/templates/captcha.html 61 | %{_sharedstatedir}/crowdsec/lua/haproxy/templates/ban.html 62 | %{_sharedstatedir}/crowdsec/lua/haproxy/community_blocklist.map 63 | 64 | %config(noreplace) %{_sysconfdir}/crowdsec/bouncers/%{name}.conf 65 | 66 | 67 | %post -p /bin/bash 68 | systemctl daemon-reload 69 | 70 | 71 | START=0 72 | 73 | systemctl is-active --quiet crowdsec 74 | 75 | if [ "$?" -eq "0" ] ; then 76 | START=1 77 | echo "cscli/crowdsec is present, generating API key" 78 | unique=`date +%s` 79 | API_KEY=`sudo cscli -oraw bouncers add HaproxyBouncer-${unique}` 80 | if [ $? -eq 1 ] ; then 81 | echo "failed to create API token, service won't be started." 82 | START=0 83 | API_KEY="" 84 | else 85 | echo "API Key : ${API_KEY}" 86 | fi 87 | fi 88 | 89 | TMP=$(mktemp -p /tmp) 90 | cp /etc/crowdsec/bouncers/crowdsec-haproxy-bouncer.conf ${TMP} 91 | API_KEY=${API_KEY} envsubst < ${TMP} > /etc/crowdsec/bouncers/crowdsec-haproxy-bouncer.conf 92 | rm ${TMP} 93 | 94 | if [ ${START} -eq 0 ] ; then 95 | echo "no api key was generated, won't start service" 96 | fi 97 | 98 | echo "Please configure '/etc/crowdsec/bouncers/crowdsec-haproxy-bouncer.conf' as you see fit" 99 | 100 | 101 | 102 | %changelog 103 | * Wed Sep 29 2022 Manuel Sabban 104 | - First initial packaging 105 | 106 | -------------------------------------------------------------------------------- /templates/ban.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CrowdSec Ban 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 18 |

CrowdSec Access Forbidden

19 |

You are unable to visit the website.

20 |
21 | 65 |
66 |
67 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /templates/captcha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CrowdSec Captcha 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 18 |

CrowdSec Captcha

19 |
20 |
21 | 22 |
23 |
24 | 68 |
69 |
70 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LIB_PATH="/usr/local/lua/crowdsec/haproxy/" 4 | DATA_PATH="/var/lib/crowdsec/lua/haproxy/" 5 | SILENT="false" 6 | 7 | usage() { 8 | echo "Usage:" 9 | echo " ./uninstall.sh -h Display this help message." 10 | echo " ./uninstall.sh Uninstall the bouncer in interactive mode" 11 | echo " ./uninstall.sh -y Uninstall the bouncer and accept everything" 12 | exit 0 13 | } 14 | 15 | #Accept cmdline arguments to overwrite options. 16 | while [[ $# -gt 0 ]] 17 | do 18 | case $1 in 19 | -y|--yes) 20 | SILENT="true" 21 | shift 22 | ;; 23 | -h|--help) 24 | usage 25 | ;; 26 | esac 27 | shift 28 | done 29 | 30 | 31 | uninstall() { 32 | rm -rf ${DATA_PATH} 33 | rm -rf ${LIB_PATH} 34 | } 35 | 36 | if ! [ $(id -u) = 0 ]; then 37 | log_err "Please run the uninstall script as root or with sudo" 38 | exit 1 39 | fi 40 | uninstall 41 | echo "crowdsec-haproxy-bouncer uninstalled successfully" -------------------------------------------------------------------------------- /upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LUA_MOD_DIR="./lua-mod" 4 | LIB_PATH="/usr/local/lua/crowdsec/haproxy/" 5 | CONFIG_PATH="/etc/crowdsec/bouncers/" 6 | CONFIG_FILE="${CONFIG_PATH}crowdsec-haproxy-bouncer.conf" 7 | OLD_CONFIG_FILE="/etc/crowdsec/crowdsec-haproxy-bouncer.conf" 8 | DATA_PATH="/var/lib/crowdsec/lua/haproxy/" 9 | 10 | install() { 11 | mkdir -p ${LIB_PATH}/plugins/crowdsec/ 12 | mkdir -p ${DATA_PATH}/templates/ 13 | 14 | cp -r ${LUA_MOD_DIR}/lib/* ${LIB_PATH}/ 15 | cp -r ${LUA_MOD_DIR}/templates/* ${DATA_PATH}/templates/ 16 | 17 | if [ ! -f ${LUA_MOD_DIR}/community_blocklist.map ]; then 18 | cp ${LUA_MOD_DIR}/community_blocklist.map ${DATA_PATH} 19 | fi 20 | } 21 | 22 | migrate_conf() { 23 | if [ -f "$CONFIG_FILE" ]; then 24 | return 25 | fi 26 | if [ ! -f "$OLD_CONFIG_FILE" ]; then 27 | return 28 | fi 29 | echo "Found $OLD_CONFIG_FILE, but no $CONFIG_FILE. Migrating it." 30 | mv "$OLD_CONFIG_FILE" "$CONFIG_FILE" 31 | } 32 | 33 | if ! [ $(id -u) = 0 ]; then 34 | echo "Please run the upgrade script as root or with sudo" 35 | exit 1 36 | fi 37 | 38 | if [ ! -d "${CONFIG_PATH}" ]; then 39 | echo "crowdsec-haproxy-bouncer is not installed, please run the ./install.sh script" 40 | exit 1 41 | fi 42 | 43 | install 44 | migrate_conf 45 | echo "crowdsec-haproxy-bouncer upgraded successfully" --------------------------------------------------------------------------------