diff --git a/Dockerfile b/Dockerfile index cc3030f..c74b71a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY fail2ban/ /opt/fail2ban COPY logs/ /opt/logs COPY lua/ /opt/lua -RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua && \ +RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua libgd && \ chmod +x /opt/entrypoint.sh /opt/scripts/* && \ mkdir /opt/entrypoint.d && \ adduser -h /dev/null -g '' -s /sbin/nologin -D -H nginx diff --git a/Dockerfile-amd64 b/Dockerfile-amd64 index 452a0c0..ca306ab 100644 --- a/Dockerfile-amd64 +++ b/Dockerfile-amd64 @@ -12,7 +12,7 @@ COPY fail2ban/ /opt/fail2ban COPY logs/ /opt/logs COPY lua/ /opt/lua -RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua && \ +RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua libgd && \ chmod +x /opt/entrypoint.sh /opt/scripts/* && \ mkdir /opt/entrypoint.d && \ adduser -h /dev/null -g '' -s /sbin/nologin -D -H nginx diff --git a/Dockerfile-arm32v7 b/Dockerfile-arm32v7 index e5e5bbe..84a7bcd 100644 --- a/Dockerfile-arm32v7 +++ b/Dockerfile-arm32v7 @@ -19,7 +19,7 @@ COPY fail2ban/ /opt/fail2ban COPY logs/ /opt/logs COPY lua/ /opt/lua -RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua && \ +RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua libgd && \ chmod +x /opt/entrypoint.sh /opt/scripts/* && \ mkdir /opt/entrypoint.d && \ adduser -h /dev/null -g '' -s /sbin/nologin -D -H nginx diff --git a/Dockerfile-arm64v8 b/Dockerfile-arm64v8 index f4091cf..1aaaaaf 100644 --- a/Dockerfile-arm64v8 +++ b/Dockerfile-arm64v8 @@ -19,7 +19,7 @@ COPY fail2ban/ /opt/fail2ban COPY logs/ /opt/logs COPY lua/ /opt/lua -RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua && \ +RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua libgd && \ chmod +x /opt/entrypoint.sh /opt/scripts/* && \ mkdir /opt/entrypoint.d && \ adduser -h /dev/null -g '' -s /sbin/nologin -D -H nginx diff --git a/Dockerfile-i386 b/Dockerfile-i386 index bf86ae0..f9e817d 100644 --- a/Dockerfile-i386 +++ b/Dockerfile-i386 @@ -12,7 +12,7 @@ COPY fail2ban/ /opt/fail2ban COPY logs/ /opt/logs COPY lua/ /opt/lua -RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua && \ +RUN apk --no-cache add php7-fpm certbot libstdc++ libmaxminddb geoip pcre yajl fail2ban clamav apache2-utils rsyslog openssl lua libgd && \ chmod +x /opt/entrypoint.sh /opt/scripts/* && \ mkdir /opt/entrypoint.d && \ adduser -h /dev/null -g '' -s /sbin/nologin -D -H nginx diff --git a/compile.sh b/compile.sh index 102b4e2..2a99b4e 100644 --- a/compile.sh +++ b/compile.sh @@ -3,7 +3,7 @@ NTASK=$(nproc) # install build dependencies -apk add --no-cache --virtual build autoconf libtool automake git geoip-dev yajl-dev g++ curl-dev libxml2-dev pcre-dev make linux-headers libmaxminddb-dev musl-dev lua-dev +apk add --no-cache --virtual build autoconf libtool automake git geoip-dev yajl-dev g++ curl-dev libxml2-dev pcre-dev make linux-headers libmaxminddb-dev musl-dev lua-dev gd-dev # compile and install ModSecurity library cd /tmp @@ -63,6 +63,11 @@ make -j $NTASK make install make install-extra cd /tmp +git clone https://github.com/ittner/lua-gd.git +cd lua-gd +make -j $NTASK +make INSTALL_PATH=/usr/local/lib/lua/5.1 install +cd /tmp git clone https://github.com/openresty/lua-nginx-module.git export LUAJIT_LIB=/usr/local/lib export LUAJIT_INC=/usr/local/include/luajit-2.1 diff --git a/confs/antibot-captcha.conf b/confs/antibot-captcha.conf new file mode 100644 index 0000000..7722e2f --- /dev/null +++ b/confs/antibot-captcha.conf @@ -0,0 +1,40 @@ +location = %ANTIBOT_URI% { + + default_type 'text/html'; + + if ($request_method = GET) { + content_by_lua_block { + local cookie = require "cookie" + local captcha = require "captcha" + if not cookie.is_set("uri") then + return ngx.exit(ngx.HTTP_FORBIDDEN) + end + local img, res = captcha.get_challenge() + cookie.set({captchares = res}) + local code = captcha.get_code(img, "%ANTIBOT_URI%") + ngx.say(code) + } + } + + if ($request_method = POST) { + access_by_lua_block { + local cookie = require "cookie" + local captcha = require "captcha" + if not cookie.is_set("captchares") then + return ngx.exit(ngx.HTTP_FORBIDDEN) + end + ngx.req.read_body() + local args, err = ngx.req.get_post_args(1) + if err == "truncated" or not args or not args["captcha"] then + return ngx.exit(ngx.HTTP_FORBIDDEN) + end + local captcha_user = args["captcha"] + local check = captcha.check(captcha_user, cookie.get("captchares")) + if not check then + return ngx.redirect("%ANTIBOT_URI%") + end + cookie.set({captcha = "ok"}) + return ngx.redirect(cookie.get("uri")) + } + } +} diff --git a/confs/antibot-javascript.conf b/confs/antibot-javascript.conf index 7ceafc9..2f69019 100644 --- a/confs/antibot-javascript.conf +++ b/confs/antibot-javascript.conf @@ -10,7 +10,7 @@ location = %ANTIBOT_URI% { return ngx.exit(ngx.HTTP_FORBIDDEN) end local challenge = cookie.get("challenge") - local code = javascript.get_code(challenge, "%ANTIBOT_URI%", cookie.get("uri")) + local code = javascript.get_code(challenge, "%ANTIBOT_URI%", cookie.get("uri")) ngx.say(code) } } @@ -32,8 +32,8 @@ location = %ANTIBOT_URI% { if not check then return ngx.exit(ngx.HTTP_FORBIDDEN) end - cookie.set("javascript", "ok") - cookie.save() + cookie.set({javascript = "ok"}) + return ngx.exit(ngx.OK) } } } diff --git a/confs/main-lua.conf b/confs/main-lua.conf index 2f69736..0e232be 100644 --- a/confs/main-lua.conf +++ b/confs/main-lua.conf @@ -7,6 +7,7 @@ local use_blacklist_reverse = %USE_BLACKLIST_REVERSE% local use_dnsbl = %USE_DNSBL% local use_antibot_cookie = %USE_ANTIBOT_COOKIE% local use_antibot_javascript = %USE_ANTIBOT_JAVASCRIPT% +local use_antibot_captcha = %USE_ANTIBOT_CAPTCHA% -- include LUA code local whitelist = require "whitelist" @@ -14,6 +15,7 @@ local blacklist = require "blacklist" local dnsbl = require "dnsbl" local cookie = require "cookie" local javascript = require "javascript" +local captcha = require "captcha" -- antibot local antibot_uri = "%ANTIBOT_URI%" @@ -78,8 +80,7 @@ end if use_antibot_cookie then if not cookie.is_set("uri") then if ngx.var.request_uri ~= antibot_uri then - cookie.set("uri", ngx.var.request_uri) - cookie.save() + cookie.set({uri = ngx.var.request_uri}) return ngx.redirect(antibot_uri) end return ngx.exit(ngx.HTTP_FORBIDDEN) @@ -94,9 +95,17 @@ end if use_antibot_javascript then if not cookie.is_set("javascript") then if ngx.var.request_uri ~= antibot_uri then - cookie.set("uri", ngx.var.request_uri) - cookie.set("challenge", javascript.get_challenge()) - cookie.save() + cookie.set({uri = ngx.var.request_uri, challenge = javascript.get_challenge()}) + return ngx.redirect(antibot_uri) + end + end +end + +-- captcha check +if use_antibot_captcha then + if not cookie.is_set("captcha") then + if ngx.var.request_uri ~= antibot_uri and ngx.var.request_uri ~= "/favicon.ico" then + cookie.set({uri = ngx.var.request_uri}) return ngx.redirect(antibot_uri) end end @@ -107,3 +116,5 @@ ngx.exit(ngx.OK) } %INCLUDE_ANTIBOT_JAVASCRIPT% + +%INCLUDE_ANTIBOT_CAPTCHA% diff --git a/entrypoint.sh b/entrypoint.sh index f68382f..1674875 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -54,7 +54,7 @@ cp -r /opt/confs/owasp-crs /etc/nginx cp /opt/confs/php.ini /etc/php7/php.ini cp /opt/logs/rsyslog.conf /etc/rsyslog.conf cp /opt/logs/logrotate.conf /etc/logrotate.conf -cp /opt/lua/*.lua /usr/local/lib/lua +cp -r /opt/lua/* /usr/local/lib/lua # remove cron jobs echo "" > /etc/crontabs/root @@ -502,17 +502,31 @@ replace_in_file "/etc/nginx/main-lua.conf" "%ANTIBOT_URI%" "$ANTIBOT_URI" if [ "$USE_ANTIBOT" = "cookie" ] ; then replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_COOKIE%" "true" replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_JAVASCRIPT%" "false" + replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_CAPTCHA%" "false" replace_in_file "/etc/nginx/main-lua.conf" "%INCLUDE_ANTIBOT_JAVASCRIPT%" "" + replace_in_file "/etc/nginx/main-lua.conf" "%INCLUDE_ANTIBOT_CAPTCHA%" "" # antibot via javascript elif [ "$USE_ANTIBOT" = "javascript" ] ; then replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_COOKIE%" "false" replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_JAVASCRIPT%" "true" + replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_CAPTCHA%" "false" replace_in_file "/etc/nginx/main-lua.conf" "%INCLUDE_ANTIBOT_JAVASCRIPT%" "include /etc/nginx/antibot-javascript.conf;" + replace_in_file "/etc/nginx/main-lua.conf" "%INCLUDE_ANTIBOT_CAPTCHA%" "" replace_in_file "/etc/nginx/antibot-javascript.conf" "%ANTIBOT_URI%" "$ANTIBOT_URI" +# antibot via captcha +elif [ "$USE_ANTIBOT" = "captcha" ] ; then + replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_COOKIE%" "false" + replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_JAVASCRIPT%" "false" + replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_CAPTCHA%" "true" + replace_in_file "/etc/nginx/main-lua.conf" "%INCLUDE_ANTIBOT_JAVASCRIPT%" "" + replace_in_file "/etc/nginx/main-lua.conf" "%INCLUDE_ANTIBOT_CAPTCHA%" "include /etc/nginx/antibot-captcha.conf;" + replace_in_file "/etc/nginx/antibot-captcha.conf" "%ANTIBOT_URI%" "$ANTIBOT_URI" else replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_COOKIE%" "false" replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_JAVASCRIPT%" "false" + replace_in_file "/etc/nginx/main-lua.conf" "%USE_ANTIBOT_CAPTCHA%" "false" replace_in_file "/etc/nginx/main-lua.conf" "%INCLUDE_ANTIBOT_JAVASCRIPT%" "" + replace_in_file "/etc/nginx/main-lua.conf" "%INCLUDE_ANTIBOT_CAPTCHA%" "" fi if [ "$USE_LIMIT_REQ" = "yes" ] ; then diff --git a/lua/captcha.lua b/lua/captcha.lua new file mode 100644 index 0000000..7244791 --- /dev/null +++ b/lua/captcha.lua @@ -0,0 +1,32 @@ +local M = {} +local captcha = require "misc.captcha" +local base64 = require "misc.base64" + +function M.get_challenge () + local cap = captcha.new() + cap:font("/usr/local/lib/lua/misc/Vera.ttf") + cap:generate() + return cap:jpegStr(70), cap:getStr() +end + +function M.get_code (img, antibot_uri) + return string.format([[ + +
+ + + + + + ]], antibot_uri, base64.encode(img)) +end + +function M.check (captcha_user, captcha_valid) + return captcha_user == captcha_valid +end + +return M diff --git a/lua/cookie.lua b/lua/cookie.lua index 4b783b5..5d69f5d 100644 --- a/lua/cookie.lua +++ b/lua/cookie.lua @@ -2,28 +2,33 @@ local M = {} local session = require "resty.session" -local s = session.open() -if not s then - s = session.start() +function M.session () + local s = session:open() + if not s.started then + s:start() + end + return s end function M.is_set (key) + local s = M.session() if s.data[key] then return true end return false end -function M.set (key, value) - s.data[key] = value -end - -function M.get (key) - return s.data[key] -end - -function M.save () +function M.set (values) + local s = M.session() + for k, v in pairs(values) do + s.data[k] = v + end s:save() end +function M.get (key) + local s = M.session () + return s.data[key] +end + return M diff --git a/lua/misc/Vera.ttf b/lua/misc/Vera.ttf new file mode 100644 index 0000000..58cd6b5 Binary files /dev/null and b/lua/misc/Vera.ttf differ diff --git a/lua/misc/base64.lua b/lua/misc/base64.lua new file mode 100644 index 0000000..9f9c716 --- /dev/null +++ b/lua/misc/base64.lua @@ -0,0 +1,202 @@ +--[[ + + base64 -- v1.5.2 public domain Lua base64 encoder/decoder + no warranty implied; use at your own risk + + Needs bit32.extract function. If not present it's implemented using BitOp + or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua + implementation inspired by Rici Lake's post: + http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html + + author: Ilya Kolbin (iskolbin@gmail.com) + url: github.com/iskolbin/lbase64 + + COMPATIBILITY + + Lua 5.1, 5.2, 5.3, LuaJIT + + LICENSE + + See end of file for license information. + +--]] + + +local base64 = {} + +local extract = _G.bit32 and _G.bit32.extract +if not extract then + if _G.bit then + local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band + extract = function( v, from, width ) + return band( shr( v, from ), shl( 1, width ) - 1 ) + end + elseif _G._VERSION >= "Lua 5.3" then + extract = load[[return function( v, from, width ) + return ( v >> from ) & ((1 << width) - 1) + end]]() + else + extract = function( v, from, width ) + local w = 0 + local flag = 2^from + for i = 0, width-1 do + local flag2 = flag + flag + if v % flag2 >= flag then + w = w + 2^i + end + flag = flag2 + end + return w + end + end +end + + +function base64.makeencoder( s62, s63, spad ) + local encoder = {} + for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J', + 'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y', + 'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n', + 'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2', + '3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do + encoder[b64code] = char:byte() + end + return encoder +end + +function base64.makedecoder( s62, s63, spad ) + local decoder = {} + for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do + decoder[charcode] = b64code + end + return decoder +end + +local DEFAULT_ENCODER = base64.makeencoder() +local DEFAULT_DECODER = base64.makedecoder() + +local char, concat = string.char, table.concat + +function base64.encode( str, encoder, usecaching ) + encoder = encoder or DEFAULT_ENCODER + local t, k, n = {}, 1, #str + local lastn = n % 3 + local cache = {} + for i = 1, n-lastn, 3 do + local a, b, c = str:byte( i, i+2 ) + local v = a*0x10000 + b*0x100 + c + local s + if usecaching then + s = cache[v] + if not s then + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + cache[v] = s + end + else + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + end + t[k] = s + k = k + 1 + end + if lastn == 2 then + local a, b = str:byte( n-1, n ) + local v = a*0x10000 + b*0x100 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64]) + elseif lastn == 1 then + local v = str:byte( n )*0x10000 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64]) + end + return concat( t ) +end + +function base64.decode( b64, decoder, usecaching ) + decoder = decoder or DEFAULT_DECODER + local pattern = '[^%w%+%/%=]' + if decoder then + local s62, s63 + for charcode, b64code in pairs( decoder ) do + if b64code == 62 then s62 = charcode + elseif b64code == 63 then s63 = charcode + end + end + pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) ) + end + b64 = b64:gsub( pattern, '' ) + local cache = usecaching and {} + local t, k = {}, 1 + local n = #b64 + local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0 + for i = 1, padding > 0 and n-4 or n, 4 do + local a, b, c, d = b64:byte( i, i+3 ) + local s + if usecaching then + local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d + s = cache[v0] + if not s then + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] + s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8)) + cache[v0] = s + end + else + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] + s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8)) + end + t[k] = s + k = k + 1 + end + if padding == 1 then + local a, b, c = b64:byte( n-3, n-1 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + t[k] = char( extract(v,16,8), extract(v,8,8)) + elseif padding == 2 then + local a, b = b64:byte( n-3, n-2 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + t[k] = char( extract(v,16,8)) + end + return concat( t ) +end + +return base64 + +--[[ +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2018 Ilya Kolbin +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +--]] + diff --git a/lua/misc/captcha.lua b/lua/misc/captcha.lua new file mode 100644 index 0000000..b2700c3 --- /dev/null +++ b/lua/misc/captcha.lua @@ -0,0 +1,193 @@ +-- Copyright startx