├── .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 |
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 |
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"
--------------------------------------------------------------------------------