diff --git a/Dockerfile b/Dockerfile index 37f0923..25010d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,9 +9,9 @@ RUN apt-get install -y libssl-dev FROM base AS build -COPY ./lua_resty_netacea-1.5.0-0.rockspec ./ +COPY ./lua_resty_netacea-1.6.0-0.rockspec ./ COPY ./src ./src -RUN /usr/local/openresty/luajit/bin/luarocks make ./lua_resty_netacea-1.5.0-0.rockspec +RUN /usr/local/openresty/luajit/bin/luarocks make ./lua_resty_netacea-1.6.0-0.rockspec FROM build AS test diff --git a/Dockerfile.nginx_lua b/Dockerfile.nginx_lua index 8b55e44..30a0954 100644 --- a/Dockerfile.nginx_lua +++ b/Dockerfile.nginx_lua @@ -69,9 +69,9 @@ RUN cd /usr/src && \ make install # Set up Netacea module -COPY ./lua_resty_netacea-1.5.0-0.rockspec ./ +COPY ./lua_resty_netacea-1.6.0-0.rockspec ./ COPY ./src ./src -RUN luarocks make ./lua_resty_netacea-1.5.0-0.rockspec +RUN luarocks make ./lua_resty_netacea-1.6.0-0.rockspec # Link CA certs so they match expected filename RUN ln -s /etc/ssl/certs/ca-bundle.crt /etc/ssl/certs/ca-certificates.crt diff --git a/README.md b/README.md index e6e8810..90593b7 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ NETACEA_PROTECTOR_API_URL=https://your-protector-api-url | `NETACEA_CAPTCHA_COOKIE_ATTRIBUTES` | `Max-Age=86400; Path=/;` | | `NETACEA_CAPTCHA_COOKIE_NAME` | `_mitatacaptcha` | | `NETACEA_CAPTCHA_PATH` | unset | +| `NETACEA_CHALLENGE_RESPONSE_STATUS` | `403` | | `NETACEA_CHECKPOINT_SIGNAL_PATH` | unset | | `NETACEA_COOKIE_ATTRIBUTES` | `Max-Age=86400; Path=/;` | | `NETACEA_COOKIE_ENCRYPTION_KEY` | none | diff --git a/lua_resty_netacea-1.5.0-0.rockspec b/lua_resty_netacea-1.6.0-0.rockspec similarity index 96% rename from lua_resty_netacea-1.5.0-0.rockspec rename to lua_resty_netacea-1.6.0-0.rockspec index 8c771f4..e649dd5 100644 --- a/lua_resty_netacea-1.5.0-0.rockspec +++ b/lua_resty_netacea-1.6.0-0.rockspec @@ -1,5 +1,5 @@ package = "lua_resty_netacea" -version = "1.5.0-0" +version = "1.6.0-0" source = { url = "git://github.com/Netacea/lua_resty_netacea", branch = "master" diff --git a/src/conf/nginx.conf b/src/conf/nginx.conf index 41a57e9..61037e2 100644 --- a/src/conf/nginx.conf +++ b/src/conf/nginx.conf @@ -15,6 +15,7 @@ env NETACEA_REAL_IP_HEADER_INDEX; env NETACEA_CHECKPOINT_SIGNAL_PATH; env NETACEA_CAPTCHA_PATH; env NETACEA_ENABLE_CAPTCHA_CONTENT_NEGOTIATION; +env NETACEA_CHALLENGE_RESPONSE_STATUS; env NETACEA_BLOCKED_RESPONSE_STATUS; env NETACEA_BLOCKED_RESPONSE_BODY; env NETACEA_BLOCKED_RESPONSE_CONTENT_TYPE; @@ -59,6 +60,7 @@ http { realIpHeaderIndex = tonumber(env('NETACEA_REAL_IP_HEADER_INDEX', '')), checkpointSignalPath = env('NETACEA_CHECKPOINT_SIGNAL_PATH'), netaceaCaptchaPath = env('NETACEA_CAPTCHA_PATH'), + challengeResponseStatus = env('NETACEA_CHALLENGE_RESPONSE_STATUS'), blockedResponseStatus = env('NETACEA_BLOCKED_RESPONSE_STATUS'), blockedResponseBody = env('NETACEA_BLOCKED_RESPONSE_BODY'), blockedResponseContentType = env('NETACEA_BLOCKED_RESPONSE_CONTENT_TYPE'), diff --git a/src/lua_resty_netacea.lua b/src/lua_resty_netacea.lua index 850dff6..0cd19e7 100644 --- a/src/lua_resty_netacea.lua +++ b/src/lua_resty_netacea.lua @@ -8,7 +8,7 @@ local Constants = require("lua_resty_netacea_constants") local mitigation = require("lua_resty_netacea_mitigation") local _N = {} -_N._VERSION = '1.5.0' +_N._VERSION = '1.6.0' _N._TYPE = 'nginx' local ngx = require 'ngx' @@ -41,7 +41,7 @@ local function setInjectHeaders(protector_result) return idType, mitigationType, captchaState end -local function normalizeBlockedResponseStatus(value) +local function normalizeHttpStatus(value) local status = tonumber(value) if not status or status < 100 or status > 599 or status % 1 ~= 0 then return ngx.HTTP_FORBIDDEN @@ -158,7 +158,9 @@ function _N:new(options) -- global:optional:netaceaCaptchaPath n.netaceaCaptchaPath = utils.normalizeRelativePath(utils.parseOption(options.netaceaCaptchaPath, nil)) -- global:optional:blockedResponseStatus - n.blockedResponseStatus = normalizeBlockedResponseStatus(utils.parseOption(options.blockedResponseStatus, nil)) + n.blockedResponseStatus = normalizeHttpStatus(utils.parseOption(options.blockedResponseStatus, nil)) + -- global:optional:challengeResponseStatus + n.challengeResponseStatus = normalizeHttpStatus(utils.parseOption(options.challengeResponseStatus, nil)) -- global:optional:blockedResponseBody n.blockedResponseBody = utils.parseOption(options.blockedResponseBody, nil) -- global:optional:blockedResponseContentType @@ -364,7 +366,8 @@ function _N:mitigate() enableCaptchaContentNegotiation = self.enableCaptchaContentNegotiation, netaceaCaptchaPath = self.netaceaCaptchaPath, captchaPath = self.netaceaCaptchaPath, - trackingId = trackingId + trackingId = trackingId, + challengeResponseStatus = self.challengeResponseStatus }) end return @@ -414,7 +417,8 @@ function _N:mitigate() serveCaptchaFailOpen(captchaBody, { enableCaptchaContentNegotiation = self.enableCaptchaContentNegotiation, netaceaCaptchaPath = self.netaceaCaptchaPath, - captchaPath = self.netaceaCaptchaPath + captchaPath = self.netaceaCaptchaPath, + challengeResponseStatus = self.challengeResponseStatus }) return end @@ -427,7 +431,8 @@ function _N:mitigate() serveCaptchaFailOpen(checkpointBody, { enableCaptchaContentNegotiation = self.enableCaptchaContentNegotiation, netaceaCaptchaPath = self.netaceaCaptchaPath, - captchaPath = self.netaceaCaptchaPath + captchaPath = self.netaceaCaptchaPath, + challengeResponseStatus = self.challengeResponseStatus }) return end diff --git a/src/lua_resty_netacea_mitigation.lua b/src/lua_resty_netacea_mitigation.lua index e632b27..33cefe6 100644 --- a/src/lua_resty_netacea_mitigation.lua +++ b/src/lua_resty_netacea_mitigation.lua @@ -102,7 +102,11 @@ end function _M.serveCaptcha(captchaBody, options) options = options or {} - ngx.status = ngx.HTTP_FORBIDDEN + local status = tonumber(options.challengeResponseStatus) + if not status or status < 100 or status > 599 or status % 1 ~= 0 then + status = ngx.HTTP_FORBIDDEN + end + ngx.status = status if shouldServeCaptchaAsJson(options.enableCaptchaContentNegotiation, options.netaceaCaptchaPath) then ngx.header["content-type"] = "application/json" ngx.header["Cache-Control"] = "max-age=0, no-cache, no-store, must-revalidate" @@ -111,13 +115,13 @@ function _M.serveCaptcha(captchaBody, options) error("NETACEA CAPTCHA - missing trackingId for negotiated JSON response") end ngx.print(buildCaptchaJson(options.captchaPath, trackingId)) - return ngx.exit(ngx.HTTP_OK) + return ngx.exit(status) end ngx.header["content-type"] = "text/html" ngx.header["Cache-Control"] = "max-age=0, no-cache, no-store, must-revalidate" ngx.print(captchaBody) - return ngx.exit(ngx.HTTP_OK) + return ngx.exit(status) end function _M.serveBlock(blockedResponseStatus, blockedResponseBody, blockedResponseContentType) diff --git a/test/lua_resty_netacea_mitigation_spec.lua b/test/lua_resty_netacea_mitigation_spec.lua index 06ecc27..5aa5ba9 100644 --- a/test/lua_resty_netacea_mitigation_spec.lua +++ b/test/lua_resty_netacea_mitigation_spec.lua @@ -63,7 +63,15 @@ describe("lua_resty_netacea_mitigation", function() it("should exit with HTTP_OK", function() mitigation.serveCaptcha("captcha") - assert.spy(ngx_mock.exit).was.called_with(200) + assert.spy(ngx_mock.exit).was.called_with(403) + end) + + it("should use the configured challenge response status", function() + mitigation.serveCaptcha("captcha", { + challengeResponseStatus = 429 + }) + assert.are.equal(429, ngx_mock.status) + assert.spy(ngx_mock.exit).was.called_with(429) end) it("should serve json when html is not accepted but json is accepted", function() @@ -73,13 +81,15 @@ describe("lua_resty_netacea_mitigation", function() enableCaptchaContentNegotiation = true, netaceaCaptchaPath = "/captcha", captchaPath = "/getCaptcha", - trackingId = "a1a16640-e1f0-4de4-a3a6-140c45181383" + trackingId = "a1a16640-e1f0-4de4-a3a6-140c45181383", + challengeResponseStatus = 429 }) assert.are.equal("application/json", ngx_mock.header["content-type"]) assert.spy(ngx_mock.print).was.called_with( '{"captchaRelativeURL":"/getCaptcha?trackingId=a1a16640-e1f0-4de4-a3a6-140c45181383","captchaAbsoluteURL":"https://example.com/getCaptcha?trackingId=a1a16640-e1f0-4de4-a3a6-140c45181383"}' ) + assert.spy(ngx_mock.exit).was.called_with(429) end) it("should keep html when text/html is accepted", function() diff --git a/test/lua_resty_netacea_spec.lua b/test/lua_resty_netacea_spec.lua index 28d8616..cd9a8de 100644 --- a/test/lua_resty_netacea_spec.lua +++ b/test/lua_resty_netacea_spec.lua @@ -157,6 +157,7 @@ insulate("lua_resty_netacea", function() cookieEncryptionKey = options.cookieEncryptionKey, secretKey = options.secretKey or "test-secret-key", blockedResponseStatus = options.blockedResponseStatus, + challengeResponseStatus = options.challengeResponseStatus, blockedResponseBody = options.blockedResponseBody, blockedResponseContentType = options.blockedResponseContentType, kinesisProperties = { @@ -312,6 +313,25 @@ insulate("lua_resty_netacea", function() assert.are.equal("text/plain; charset=utf-8", netacea.blockedResponseContentType) end) + it("should store the configured challenge response status", function() + local netacea = Netacea:new({ + ingestEnabled = true, + mitigationType = "MITIGATE", + mitigationEndpoint = "https://mitigation.example", + apiKey = "test-api-key", + cookieEncryptionKey = "test-cookie-encryption-key", + challengeResponseStatus = "429", + kinesisProperties = { + stream_name = "test-stream", + region = "eu-west-1", + aws_access_key = "test-access-key", + aws_secret_key = "test-secret-key" + } + }) + + assert.are.equal(429, netacea.challengeResponseStatus) + end) + it("should disable mitigation when mitigationType is INGEST", function() local netacea = Netacea:new({ ingestEnabled = true, @@ -591,7 +611,8 @@ insulate("lua_resty_netacea", function() mitigationEndpoint = "https://mitigation.example", apiKey = "test-api-key", cookieEncryptionKey = "test-cookie-encryption-key", - netaceaCaptchaPath = "/captcha" + netaceaCaptchaPath = "/captcha", + challengeResponseStatus = 403 }) netacea:mitigate() @@ -602,7 +623,8 @@ insulate("lua_resty_netacea", function() enableCaptchaContentNegotiation = false, netaceaCaptchaPath = "/captcha", captchaPath = "/captcha", - trackingId = "e334cc64-6cc2-4193-92dd-237e38bab4a7" + trackingId = "e334cc64-6cc2-4193-92dd-237e38bab4a7", + challengeResponseStatus = 403 }) assert.are.equal("ip_flagged,captcha_serve", ngx_mock.ctx.NetaceaState.bc_type) assert.spy(ngx_mock.exit).was_not_called() @@ -627,7 +649,8 @@ insulate("lua_resty_netacea", function() mitigationEndpoint = "https://mitigation.example", apiKey = "test-api-key", cookieEncryptionKey = "test-cookie-encryption-key", - enableCaptchaContentNegotiation = true + enableCaptchaContentNegotiation = true, + challengeResponseStatus = 403 }) mitigation_mock.getBestMitigation = spy.new(function() @@ -637,7 +660,8 @@ insulate("lua_resty_netacea", function() assert.spy(mitigation_mock.serveCaptcha).was.called_with("captcha", { enableCaptchaContentNegotiation = true, - captchaPath = nil + captchaPath = nil, + challengeResponseStatus = 403 }) assert.spy(ngx_mock.exit).was_not_called() end) @@ -662,7 +686,8 @@ insulate("lua_resty_netacea", function() apiKey = "test-api-key", cookieEncryptionKey = "test-cookie-encryption-key", enableCaptchaContentNegotiation = true, - netaceaCaptchaPath = "/captcha" + netaceaCaptchaPath = "/captcha", + challengeResponseStatus = 403 }) mitigation_mock.getBestMitigation = spy.new(function() @@ -673,7 +698,8 @@ insulate("lua_resty_netacea", function() assert.spy(mitigation_mock.serveCaptcha).was.called_with("captcha", { enableCaptchaContentNegotiation = true, netaceaCaptchaPath = "/captcha", - captchaPath = "/captcha" + captchaPath = "/captcha", + challengeResponseStatus = 403 }) assert.spy(ngx_mock.exit).was_not_called() end) @@ -698,7 +724,8 @@ insulate("lua_resty_netacea", function() apiKey = "test-api-key", cookieEncryptionKey = "test-cookie-encryption-key", enableCaptchaContentNegotiation = true, - netaceaCaptchaPath = "/captcha" + netaceaCaptchaPath = "/captcha", + challengeResponseStatus = 403 }) mitigation_mock.getBestMitigation = spy.new(function() @@ -709,7 +736,8 @@ insulate("lua_resty_netacea", function() assert.spy(mitigation_mock.serveCaptcha).was.called_with('{"trackingId":"b0343c30-a382-42ad-9d65-fdb005fef054"}', { enableCaptchaContentNegotiation = true, netaceaCaptchaPath = "/captcha", - captchaPath = "/captcha" + captchaPath = "/captcha", + challengeResponseStatus = 403 }) assert.spy(ngx_mock.exit).was_not_called() end) @@ -733,7 +761,8 @@ insulate("lua_resty_netacea", function() mitigationEndpoint = "https://mitigation.example", apiKey = "test-api-key", cookieEncryptionKey = "test-cookie-encryption-key", - enableCaptchaContentNegotiation = true + enableCaptchaContentNegotiation = true, + challengeResponseStatus = 403 }) mitigation_mock.getBestMitigation = spy.new(function() @@ -744,7 +773,8 @@ insulate("lua_resty_netacea", function() assert.spy(mitigation_mock.serveCaptcha).was.called_with("captcha", { enableCaptchaContentNegotiation = true, netaceaCaptchaPath = nil, - captchaPath = nil + captchaPath = nil, + challengeResponseStatus = 403 }) assert.spy(ngx_mock.exit).was_not_called() end) @@ -767,7 +797,8 @@ insulate("lua_resty_netacea", function() mitigationEndpoint = "https://mitigation.example", apiKey = "test-api-key", cookieEncryptionKey = "test-cookie-encryption-key", - netaceaCaptchaPath = "/captcha" + netaceaCaptchaPath = "/captcha", + challengeResponseStatus = 403 }) netacea:mitigate() @@ -777,7 +808,8 @@ insulate("lua_resty_netacea", function() assert.spy(mitigation_mock.serveCaptcha).was.called_with("captcha", { enableCaptchaContentNegotiation = false, netaceaCaptchaPath = "/captcha", - captchaPath = "/captcha" + captchaPath = "/captcha", + challengeResponseStatus = 403 }) assert.spy(ngx_mock.exit).was_not_called() end)