diff --git a/Dockerfile b/Dockerfile index 68c8bec..2e9e19d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,12 @@ FROM openresty/openresty:alpine-fat +RUN apk add --no-cache git RUN luarocks install lua-resty-http -RUN mkdir -p /usr/local/openresty/lualib/plugins/ -COPY ./lua /usr/local/openresty/lualib/plugins/crowdsec -COPY ./config/template.conf /etc/crowdsec/bouncers/crowdsec-openresty-bouncer.conf +RUN git clone https://github.com/crowdsecurity/lua-cs-bouncer.git +RUN mkdir -p /usr/local/openresty/lualib/plugins/crowdsec/ /etc/crowdsec/bouncers/ +RUN cp lua-cs-bouncer/nginx/*.lua /usr/local/openresty/lualib/plugins/crowdsec/ +RUN cp lua-cs-bouncer/nginx/template.conf /etc/crowdsec/bouncers/crowdsec-openresty-bouncer.conf +RUN rm -rf ./lua-cs-bouncer/ COPY ./openresty /etc/nginx/conf.d COPY ./docker/docker_start.sh / diff --git a/Dockerfile.lua-bouncer-plugin b/Dockerfile.lua-bouncer-plugin index fc16411..787b245 100644 --- a/Dockerfile.lua-bouncer-plugin +++ b/Dockerfile.lua-bouncer-plugin @@ -1,8 +1,11 @@ -FROM busybox:latest +FROM alpine:latest -COPY ./lua /crowdsec +RUN apk add --no-cache git +RUN git clone https://github.com/crowdsecurity/lua-cs-bouncer.git +RUN mkdir -p /crowdsec +RUN cp lua-cs-bouncer/nginx/*.lua /crowdsec +RUN cp lua-cs-bouncer/nginx/template.conf /crowdsec/crowdsec-bouncer.conf COPY ./ingress-nginx /crowdsec -COPY ./config/template.conf /crowdsec/crowdsec-bouncer.conf COPY ./docker/docker_start.sh / ENV IS_LUALIB_IMAGE=true diff --git a/Makefile b/Makefile index ec13606..ce4d7d3 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,25 @@ BUILD_VERSION?="$(shell git for-each-ref --sort=-v:refname --count=1 --format '%(refname)' | cut -d '/' -f3)" OUTDIR="crowdsec-openresty-bouncer-${BUILD_VERSION}/" +LUA_DIR="${OUTDIR}lua" +CONFIG_DIR="${OUTDIR}config" OUT_ARCHIVE="crowdsec-openresty-bouncer.tgz" +LUA_BOUNCER_BRANCH?=main default: release release: - mkdir "${OUTDIR}" - cp -r ./lua/ "${OUTDIR}" - cp -r ./config/ ${OUTDIR} + git clone -b ${LUA_BOUNCER_BRANCH} https://github.com/crowdsecurity/lua-cs-bouncer.git + mkdir -p "${OUTDIR}" + mkdir -p "${LUA_DIR}" + mkdir -p "${CONFIG_DIR}" + cp -r lua-cs-bouncer/nginx/*.lua "${LUA_DIR}" + cp -r lua-cs-bouncer/nginx/template.conf ${CONFIG_DIR} cp -r ./openresty/ ${OUTDIR} cp install.sh ${OUTDIR} cp uninstall.sh ${OUTDIR} + chmod +x ${OUTDIR}install.sh + chmod +x ${OUTDIR}uninstall.sh tar cvzf ${OUT_ARCHIVE} ${OUTDIR} rm -rf ${OUTDIR} + rm -rf "lua-cs-bouncer/" clean: rm -rf "${OUTDIR}" rm -rf "${OUT_ARCHIVE}" \ No newline at end of file diff --git a/config/template.conf b/config/template.conf deleted file mode 100644 index 38358bc..0000000 --- a/config/template.conf +++ /dev/null @@ -1,8 +0,0 @@ -API_URL=${CROWDSEC_LAPI_URL} -API_KEY=${API_KEY} -CACHE_EXPIRATION=1 -CACHE_SIZE=10000 -BOUNCING_ON_TYPE=ban -REQUEST_TIMEOUT=0.2 -UPDATE_FREQUENCY=10 -MODE=stream \ No newline at end of file diff --git a/debian/files b/debian/files index 8d92ed4..482f315 100644 --- a/debian/files +++ b/debian/files @@ -1 +1,2 @@ -crowdsec-openresty-bouncer_0.1.0_source.buildinfo - - +crowdsec-openresty-bouncer_0.1.0_amd64.buildinfo - - +crowdsec-openresty-bouncer_0.1.0_amd64.deb - - diff --git a/debian/rules b/debian/rules index d5df60d..55afa0a 100755 --- a/debian/rules +++ b/debian/rules @@ -10,16 +10,18 @@ export BUILD_VERSION=v${DEB_VERSION}-debian-pragmatic override_dh_systemd_start: echo "Not running dh_systemd_start" override_dh_auto_clean: + rm -rf lua-cs-bouncer override_dh_auto_test: override_dh_auto_build: override_dh_auto_install: mkdir -p debian/crowdsec-openresty-bouncer/usr/local/openresty/nginx/conf/conf.d/ cp openresty/crowdsec_openresty.conf debian/crowdsec-openresty-bouncer/usr/local/openresty/nginx/conf/conf.d/ + git clone https://github.com/crowdsecurity/lua-cs-bouncer.git mkdir -p debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ - cp lua/config.lua debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ - cp lua/crowdsec.lua debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ - cp lua/access.lua debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ - cp lua/recaptcha.lua debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ + cp lua-cs-bouncer/nginx/config.lua debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ + cp lua-cs-bouncer/nginx/crowdsec.lua debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ + cp lua-cs-bouncer/nginx/access.lua debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ + cp lua-cs-bouncer/nginx/recaptcha.lua debian/crowdsec-openresty-bouncer/usr/local/openresty/lualib/plugins/crowdsec/ mkdir -p debian/crowdsec-openresty-bouncer/etc/crowdsec/bouncers/ - cp config/template.conf debian/crowdsec-openresty-bouncer/etc/crowdsec/bouncers/crowdsec-openresty-bouncer.conf + cp lua-cs-bouncer/nginx/template.conf debian/crowdsec-openresty-bouncer/etc/crowdsec/bouncers/crowdsec-openresty-bouncer.conf override_dh_usrlocal: \ No newline at end of file diff --git a/install.sh b/install.sh index 4175240..0880c5f 100755 --- a/install.sh +++ b/install.sh @@ -15,6 +15,7 @@ gen_config_file() { SUFFIX=`tr -dc A-Za-z0-9 "${CONFIG_PATH}crowdsec-openresty-bouncer.conf" + echo "New API key generated in config '${CONFIG_PATH}crowdsec-openresty-bouncer.conf'" } check_openresty_dependency() { diff --git a/lua/access.lua b/lua/access.lua deleted file mode 100644 index 4f153f0..0000000 --- a/lua/access.lua +++ /dev/null @@ -1,8 +0,0 @@ -ok, err = require "crowdsec".allowIp(ngx.var.remote_addr) -if err ~= nil then - ngx.log(ngx.ERR, "[Crowdsec] bouncer error: " .. err) -end -if not ok then - ngx.log(ngx.ALERT, "[Crowdsec] denied '" .. ngx.var.remote_addr .. "'") - ngx.exit(ngx.HTTP_FORBIDDEN) -end diff --git a/lua/config.lua b/lua/config.lua deleted file mode 100644 index ecaf402..0000000 --- a/lua/config.lua +++ /dev/null @@ -1,92 +0,0 @@ -local config = {} - -function config.file_exists(file) - local f = io.open(file, "rb") - if f then - f:close() - end - return f ~= nil -end - - function split(s, delimiter) - result = {}; - for match in (s..delimiter):gmatch("(.-)"..delimiter.."(.-)") do - table.insert(result, match); - end - return result; -end - -local function has_value (tab, val) - for index, value in ipairs(tab) do - if value == val then - return true - end - end - - return false -end - -local function starts_with(str, start) - return str:sub(1, #start) == start -end - -function config.loadConfig(file) - if not config.file_exists(file) then - return nil, "File ".. file .." doesn't exist" - end - local conf = {} - local valid_params = {'API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE'} - local valid_int_params = {'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY'} - local valid_bouncing_on_type_values = {'ban', 'captcha', 'all'} - local default_values = { - ['REQUEST_TIMEOUT'] = 0.2, - ['BOUNCING_ON_TYPE'] = "ban", - ['MODE'] = "stream", - ['UPDATE_FREQUENCY'] = 10 - } - for line in io.lines(file) do - local isOk = false - if starts_with(line, "#") then - isOk = true - end - if not isOk then - local s = split(line, "=") - for k, v in pairs(s) do - if has_value(valid_params, v) then - if v == "BOUNCING_ON_TYPE" then - local value = s[2] - if not has_value(valid_bouncing_on_type_values, s[2]) then - ngx.log(ngx.ERR, "unsupported value '" .. s[2] .. "' for variable '" .. v .. "'. Using default value 'ban' instead") - break - end - end - if v == "MODE" then - local value = s[2] - if not has_value({'stream', 'live'}, s[2]) then - ngx.log(ngx.ERR, "unsupported value '" .. s[2] .. "' for variable '" .. v .. "'. Using default value 'stream' instead") - break - end - end - local n = next(s, k) - conf[v] = s[n] - break - elseif has_value(valid_int_params, v) then - local n = next(s, k) - conf[v] = tonumber(s[n]) - break - else - ngx.log(ngx.ERR, "unsupported configuration '" .. v .. "'") - break - end - end - end - end - for k, v in pairs(default_values) do - if conf[k] == nil then - conf[k] = v - end - end - return conf, nil -end - -return config \ No newline at end of file diff --git a/lua/crowdsec.lua b/lua/crowdsec.lua deleted file mode 100644 index dbba144..0000000 --- a/lua/crowdsec.lua +++ /dev/null @@ -1,195 +0,0 @@ -package.path = package.path .. ";./?.lua" - -local config = require "plugins.crowdsec.config" -local http = require "resty.http" -local cjson = require "cjson" -cjson.decode_array_with_array_mt(true) - - --- contain runtime = {} -local runtime = {} - - -function ipToInt( str ) - local num = 0 - if str and type(str)=="string" then - local o1,o2,o3,o4 = str:match("(%d+)%.(%d+)%.(%d+)%.(%d+)" ) - num = 2^24*o1 + 2^16*o2 + 2^8*o3 + o4 - end - return num -end - - -local csmod = {} - --- init function -function csmod.init(configFile, userAgent) - local conf, err = config.loadConfig(configFile) - if conf == nil then - return nil, err - end - runtime.conf = conf - runtime.userAgent = userAgent - runtime.cache = ngx.shared.crowdsec - - -- if stream mode, add callback to stream_query and start timer - if runtime.conf["MODE"] == "stream" then - runtime.cache:set("startup", true) - runtime.cache:set("first_run", true) - end - - return true, nil -end - -function http_request(link) - local httpc = http.new() - httpc:set_timeout(runtime.conf['REQUEST_TIMEOUT']) - local res, err = httpc:request_uri(link, { - method = "GET", - headers = { - ['Connection'] = 'close', - ['X-Api-Key'] = runtime.conf["API_KEY"], - ['User-Agent'] = runtime.userAgent - }, - }) - return res, err -end - -function parse_duration(duration) - local match, err = ngx.re.match(duration, "((?[0-9]+)h)?((?[0-9]+)m)?(?[0-9]+)") - local ttl = 0 - if not match then - if err then - return ttl, err - end - end - if match["hours"] ~= nil then - local hours = tonumber(match["hours"]) - ttl = ttl + (hours * 3600) - end - if match["minutes"] ~= nil then - local minutes = tonumber(match["minutes"]) - ttl = ttl + (minutes * 60) - end - if match["seconds"] ~= nil then - local seconds = tonumber(match["seconds"]) - ttl = ttl + seconds - end - return ttl, nil -end - -function stream_query() - -- As this function is running inside coroutine (with ngx.timer.every), - -- we need to raise error instead of returning them - ngx.log(ngx.DEBUG, "Stream Query from worker : " .. tostring(ngx.worker.id()) .. " with startup "..tostring(runtime.cache:get("startup"))) - local link = runtime.conf["API_URL"] .. "/v1/decisions/stream?startup=" .. tostring(runtime.cache:get("startup")) - local res, err = http_request(link) - if not res then - error("request failed: ".. err) - end - - local status = res.status - local body = res.body - if status~=200 then - error("Http error " .. status .. " with message (" .. tostring(body) .. ")") - end - - local decisions = cjson.decode(body) - -- process deleted decisions - if type(decisions.deleted) == "table" then - for i, decision in pairs(decisions.deleted) do - runtime.cache:delete(decision.value) - ngx.log(ngx.DEBUG, "Deleting '" .. decision.value .. "'") - end - end - - -- process new decisions - if type(decisions.new) == "table" then - for i, decision in pairs(decisions.new) do - if runtime.conf["BOUNCING_ON_TYPE"] == decision.type or runtime.conf["BOUNCING_ON_TYPE"] == "all" then - local ttl, err = parse_duration(decision.duration) - if err ~= nil then - ngx.log(ngx.ERR, "[Crowdsec] failed to parse ban duration '" .. decision.duration .. "' : " .. err) - end - local succ, err, forcible = runtime.cache:set(decision.value, false, ttl) - if not succ then - ngx.log(ngx.ERR, "failed to add ".. decision.value .." : "..err) - end - if forcible then - ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config") - end - ngx.log(ngx.DEBUG, "Adding '" .. decision.value .. "' in cache for '" .. ttl .. "' seconds") - end - end - end - - -- not startup anymore after first callback - runtime.cache:set("startup", false) - return nil -end - -function live_query(ip) - local link = runtime.conf["API_URL"] .. "/v1/decisions?ip=" .. ip - local res, err = http_request(link) - if not res then - return true, "request failed: ".. err - end - - local status = res.status - local body = res.body - if status~=200 then - return true, "Http error " .. status .. " while talking to LAPI (" .. link .. ")" - end - if body == "null" then -- no result from API, no decision for this IP - -- set ip in cache and DON'T block it - runtime.cache:set(ip, true,runtime.conf["CACHE_EXPIRATION"]) - return true, nil - end - local decision = cjson.decode(body)[1] - - if runtime.conf["BOUNCING_ON_TYPE"] == decision.type or runtime.conf["BOUNCING_ON_TYPE"] == "all" then - -- set ip in cache and block it - runtime.cache:set(ip, false,runtime.conf["CACHE_EXPIRATION"]) - return false, nil - else - return true, nil - end -end - - -function csmod.allowIp(ip) - if runtime.conf == nil then - return true, "Configuration is bad, cannot run properly" - end - - -- if it stream mode and startup start timer - if runtime.cache:get("first_run") == true then - local ok, err = ngx.timer.every(runtime.conf["UPDATE_FREQUENCY"], stream_query) - if not ok then - runtime.cache:set("first_run", true) - return true, "Failed to create the timer: " .. (err or "unknown") - end - runtime.cache:set("first_run", false) - ngx.log(ngx.DEBUG, "Timer launched") - end - - local data = runtime.cache:get(ip) - if data ~= nil then -- we have it in cache - ngx.log(ngx.DEBUG, "'" .. ip .. "' is in cache") - return data, nil - end - - -- if live mode, query lapi - if runtime.conf["MODE"] == "live" then - local ok, err = live_query(ip) - return ok, err - end - return true, nil -end - - --- Use it if you are able to close at shuttime -function csmod.close() -end - -return csmod diff --git a/lua/recaptcha.lua b/lua/recaptcha.lua deleted file mode 100644 index f871ba0..0000000 --- a/lua/recaptcha.lua +++ /dev/null @@ -1,5 +0,0 @@ -local recaptcha = {} - - - -return recaptcha \ No newline at end of file