diff --git a/TODO b/TODO index 49a283cf8..7e1bb3dc0 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,5 @@ - Ansible - Vagrant - Plugins -- sessions helpers in utils -- sessions security : check IP address, check UA, ... +- Find a way to do rdns in background - fix db warnings (Got an error reading communication packets) diff --git a/docs/assets/img/integration-ansible.svg b/docs/assets/img/integration-ansible.svg index 18cf91235..c41117d5f 100644 --- a/docs/assets/img/integration-ansible.svg +++ b/docs/assets/img/integration-ansible.svg @@ -1,4 +1 @@ - - - -

LINUX MACHINE

LINUX MACHINE
WEB SERVICES
WEB SERVICES
INTERNAL TRAFFIC
(same machine)
INTERNAL TRAFFIC...
INCOMING TRAFFIC
INCOMING TRAFFIC
INTERNAL TRAFFIC
(other machines)
INTERNAL TRAFFIC...
ANSIBLE ROLEINSTALLATION + CONFIGURATION
Text is not SVG - cannot display
\ No newline at end of file + \ No newline at end of file diff --git a/docs/assets/img/integration-linux.svg b/docs/assets/img/integration-linux.svg index b638fa28b..f0c8821a0 100644 --- a/docs/assets/img/integration-linux.svg +++ b/docs/assets/img/integration-linux.svg @@ -1,4 +1 @@ - - - -

LINUX MACHINE

LINUX MACHINE
WEB SERVICES
WEB SERVICES
INTERNAL TRAFFIC
(same machine)
INTERNAL TRAFFIC...
DEB/RPM
INSTALL
DEB/RPM...
INCOMING TRAFFIC
INCOMING TRAFFIC
INTERNAL TRAFFIC
(other machines)
INTERNAL TRAFFIC...
FILE+SYSTEMD
CONFIGURATION
FILE+SYS...
Text is not SVG - cannot display
\ No newline at end of file + \ No newline at end of file diff --git a/docs/security-tuning.md b/docs/security-tuning.md index 3ebbd4e97..33cde0c0d 100644 --- a/docs/security-tuning.md +++ b/docs/security-tuning.md @@ -253,14 +253,19 @@ That kind of security is implemented but not enabled by default in BunkerWeb and Here is the list of related settings : -| Setting | Default | Description | -| :--------------------------------------------------------: | :----------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `USE_ANTIBOT` | `no` | Accepted values to enable Antibot feature : `cookie`, `javascript`, `captcha`, `hcaptcha` and `recaptcha`. | -| `ANTIBOT_URI` | `/challenge` | URI that clients will be redirected to in order to solve the challenge. Be sure that it isn't used in your web application. | -| `ANTIBOT_SESSION_SECRET` | `random` | The secret used to encrypt cookies when using Antibot. The special value `random` will generate one for you. Be sure to set it when you use a clustered integration (32 chars). | -| `ANTIBOT_HCAPTCHA_SITEKEY` and `ANTIBOT_RECAPTCHA_SITEKEY` | | The Sitekey value to use when `USE_ANTIBOT` is set to `hcaptcha` or `recaptcha`. | -| `ANTIBOT_HCAPTCHA_SECRET` and `ANTIBOT_RECAPTCHA_SECRET` | | The Secret value to use when `USE_ANTIBOT` is set to `hcaptcha` or `recaptcha`. | -| `ANTIBOT_RECAPTCHA_SCORE` | `0.7` | The minimum score that clients must have when `USE_ANTIBOT` is set to `recaptcha`. | +| Setting | Default | Context |Multiple| Description | +|---------------------------|------------|---------|--------|------------------------------------------------------------------------------------------------------------------------------| +|`USE_ANTIBOT` |`no` |multisite|no |Activate antibot feature. | +|`ANTIBOT_URI` |`/challenge`|multisite|no |Unused URI that clients will be redirected to to solve the challenge. | +|`ANTIBOT_RECAPTCHA_SCORE` |`0.7` |multisite|no |Minimum score required for reCAPTCHA challenge. | +|`ANTIBOT_RECAPTCHA_SITEKEY`| |multisite|no |Sitekey for reCAPTCHA challenge. | +|`ANTIBOT_RECAPTCHA_SECRET` | |multisite|no |Secret for reCAPTCHA challenge. | +|`ANTIBOT_HCAPTCHA_SITEKEY` | |multisite|no |Sitekey for hCaptcha challenge. | +|`ANTIBOT_HCAPTCHA_SECRET` | |multisite|no |Secret for hCaptcha challenge. | +|`ANTIBOT_TIME_RESOLVE` |`60` |multisite|no |Maximum time (in seconds) clients have to resolve the challenge. Once this time has passed, a new challenge will be generated.| +|`ANTIBOT_TIME_VALID` |`86400` |multisite|no |Maximum validity time of solved challenges. Once this time has passed, clients will need to resolve a new one. | + +Please note that antibot feature is using a cookie to maintain a session with clients. If you are using BunkerWeb in a clustered environment, you will need to set the `SESSIONS_SECRET` and `SESSIONS_NAME` settings to another value than the default one (which is `random`). You will find more info about sessions [here](settings.md#sessions). ## Blacklisting, whitelisting and greylisting diff --git a/docs/settings.md b/docs/settings.md index 146251c53..fecd02f9f 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -498,6 +498,8 @@ Management of session used by other plugins. |`SESSIONS_IDLING_TIMEOUT` |`1800` |global |no |Maximum time (in seconds) of inactivity before the session is invalidated. | |`SESSIONS_ROLLING_TIMEOUT` |`3600` |global |no |Maximum time (in seconds) before a session must be renewed. | |`SESSIONS_ABSOLUTE_TIMEOUT`|`86400` |global |no |Maximum time (in seconds) before a session is destroyed. | +|`SESSIONS_CHECK_IP` |`yes` |global |no |Destroy session if IP address is different than original one. | +|`SESSIONS_CHECK_USER_AGENT`|`yes` |global |no |Destroy session if User-Agent is different than original one. | ### UI diff --git a/misc/integrations/k8s.mysql.ui.yml b/misc/integrations/k8s.mysql.ui.yml index 893681a83..8e9371368 100644 --- a/misc/integrations/k8s.mysql.ui.yml +++ b/misc/integrations/k8s.mysql.ui.yml @@ -124,7 +124,7 @@ spec: - name: KUBERNETES_MODE value: "yes" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" + value: "mysql+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" --- apiVersion: apps/v1 kind: Deployment @@ -151,7 +151,7 @@ spec: - name: KUBERNETES_MODE value: "yes" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" + value: "mysql+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" --- apiVersion: apps/v1 kind: Deployment @@ -213,64 +213,6 @@ spec: --- apiVersion: apps/v1 kind: Deployment -metadata: - name: bunkerweb-redis -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app: bunkerweb-redis - template: - metadata: - labels: - app: bunkerweb-redis - spec: - containers: - - name: bunkerweb-redis - image: redis:7-alpine - imagePullPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: bunkerweb-db -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app: bunkerweb-db - template: - metadata: - labels: - app: bunkerweb-db - spec: - containers: - - name: bunkerweb-db - image: mariadb:10.10 - imagePullPolicy: Always - env: - - name: MYSQL_RANDOM_ROOT_PASSWORD - value: "yes" - - name: "MYSQL_DATABASE" - value: "db" - - name: "MYSQL_USER" - value: "bunkerweb" - - name: "MYSQL_PASSWORD" - value: "changeme" - volumeMounts: - - mountPath: "/var/lib/mysql" - name: vol-db - volumes: - - name: vol-db - persistentVolumeClaim: - claimName: pvc-bunkerweb ---- -apiVersion: apps/v1 -kind: Deployment metadata: name: bunkerweb-ui spec: @@ -300,7 +242,7 @@ spec: - name: KUBERNETES_MODE value: "YES" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:testor@svc-bunkerweb-db:3306/db" + value: "mysql+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" --- apiVersion: v1 kind: Service @@ -363,7 +305,6 @@ spec: resources: requests: storage: 5Gi - volumeName: pv-bunkerweb --- apiVersion: networking.k8s.io/v1 kind: Ingress diff --git a/misc/integrations/k8s.mysql.yml b/misc/integrations/k8s.mysql.yml index 8f83e63bb..c163cb704 100644 --- a/misc/integrations/k8s.mysql.yml +++ b/misc/integrations/k8s.mysql.yml @@ -124,7 +124,7 @@ spec: - name: KUBERNETES_MODE value: "yes" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" + value: "mysql+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" --- apiVersion: apps/v1 kind: Deployment @@ -150,7 +150,7 @@ spec: - name: KUBERNETES_MODE value: "yes" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" + value: "mysql+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" --- apiVersion: apps/v1 kind: Deployment @@ -257,4 +257,3 @@ spec: resources: requests: storage: 5Gi - volumeName: pv-bunkerweb diff --git a/misc/integrations/k8s.postgres.ui.yml b/misc/integrations/k8s.postgres.ui.yml index 50ffaa3e3..ce2a97668 100644 --- a/misc/integrations/k8s.postgres.ui.yml +++ b/misc/integrations/k8s.postgres.ui.yml @@ -124,7 +124,7 @@ spec: - name: KUBERNETES_MODE value: "yes" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" + value: "postgresql://bunkerweb:changeme@svc-bunkerweb-db:5432/db" --- apiVersion: apps/v1 kind: Deployment @@ -151,7 +151,7 @@ spec: - name: KUBERNETES_MODE value: "yes" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" + value: "postgresql://bunkerweb:changeme@svc-bunkerweb-db:5432/db" --- apiVersion: apps/v1 kind: Deployment @@ -201,6 +201,8 @@ spec: value: "bunkerweb" - name: "POSTGRES_PASSWORD" value: "changeme" + - name: "PGDATA" + value: "/var/lib/postgresql/data/pgdata" volumeMounts: - mountPath: "/var/lib/postgresql/data" name: vol-db @@ -240,7 +242,7 @@ spec: - name: KUBERNETES_MODE value: "YES" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:testor@svc-bunkerweb-db:3306/db" + value: "postgresql://bunkerweb:changeme@svc-bunkerweb-db:5432/db" --- apiVersion: v1 kind: Service @@ -303,19 +305,6 @@ spec: resources: requests: storage: 5Gi - volumeName: pv-bunkerweb ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: pvc-bunkerweb -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi - volumeName: pv-bunkerweb --- apiVersion: networking.k8s.io/v1 kind: Ingress diff --git a/misc/integrations/k8s.postgres.yml b/misc/integrations/k8s.postgres.yml index a73ebafc0..f54b8af9e 100644 --- a/misc/integrations/k8s.postgres.yml +++ b/misc/integrations/k8s.postgres.yml @@ -124,7 +124,7 @@ spec: - name: KUBERNETES_MODE value: "yes" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" + value: "postgresql://bunkerweb:changeme@svc-bunkerweb-db:5432/db" --- apiVersion: apps/v1 kind: Deployment @@ -150,7 +150,7 @@ spec: - name: KUBERNETES_MODE value: "yes" - name: "DATABASE_URI" - value: "mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db" + value: "postgresql://bunkerweb:changeme@svc-bunkerweb-db:5432/db" --- apiVersion: apps/v1 kind: Deployment @@ -200,6 +200,8 @@ spec: value: "bunkerweb" - name: "POSTGRES_PASSWORD" value: "changeme" + - name: "PGDATA" + value: "/var/lib/postgresql/data/pgdata" volumeMounts: - mountPath: "/var/lib/postgresql/data" name: vol-db @@ -255,4 +257,3 @@ spec: resources: requests: storage: 5Gi - volumeName: pv-bunkerweb diff --git a/src/bw/lua/bunkerweb/api.lua b/src/bw/lua/bunkerweb/api.lua index ce096abb2..3139ba6b0 100644 --- a/src/bw/lua/bunkerweb/api.lua +++ b/src/bw/lua/bunkerweb/api.lua @@ -1,8 +1,11 @@ local class = require "middleclass" local datastore = require "bunkerweb.datastore" local utils = require "bunkerweb.utils" +local logger = require "bunkerweb.logger" local cjson = require "cjson" local upload = require "resty.upload" +local rsignal = require "resty.signal" +local process = require "ngx.process" local api = class("api") @@ -10,6 +13,32 @@ api.global = { GET = {}, POST = {}, PUT = {}, DELETE = {} } function api:initialize() self.datastore = datastore:new() + self.logger = logger:new("API") +end + +function api:log_cmd(cmd, status, stdout, stderr) + local level = ngx.NOTICE + local prefix = "success" + if status ~= 0 then + level = ngx.ERR + prefix = "error" + end + self.logger:log(level, prefix .. " while running command " .. command) + self.logger:log(level, "stdout = " .. stdout) + self.logger:log(level, "stdout = " .. stderr) +end + +-- TODO : use this if we switch to OpenResty +function api:cmd(cmd) + -- Non-blocking command + local ok, stdout, stderr, reason, status = shell.run(cmd, nil, 10000) + self.logger:log_cmd(cmd, status, stdout, stderr) + -- Timeout + if ok == nil then + return nil, reason + end + -- Other cases : exit 0, exit !0 and killed by signal + return status == 0, reason, status end function api:response(http_status, api_status, msg) @@ -24,19 +53,21 @@ api.global.GET["^/ping$"] = function(self) end api.global.POST["^/reload$"] = function(self) - local status = os.execute("nginx -s reload") - if status == 0 then - return self:response(ngx.HTTP_OK, "success", "reload successful") + -- Send HUP signal to master process + local ok, err = rsignal.kill(process.get_master_pid(), "HUP") + if not ok then + return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "err = " .. err) end - return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status)) + return self:response(ngx.HTTP_OK, "success", "reload successful") end api.global.POST["^/stop$"] = function(self) - local status = os.execute("nginx -s quit") - if status == 0 then - return self:response(ngx.HTTP_OK, "success", "stop successful") + -- Send QUIT signal to master process + local ok, err = rsignal.kill(process.get_master_pid(), "QUIT") + if not ok then + return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "err = " .. err) end - return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status)) + return self:response(ngx.HTTP_OK, "success", "stop successful") end api.global.POST["^/confs$"] = function(self) @@ -74,13 +105,15 @@ api.global.POST["^/confs$"] = function(self) end file:flush() file:close() - local status = os.execute("rm -rf " .. destination .. "/*") - if status ~= 0 then - return self:response(ngx.HTTP_BAD_REQUEST, "error", "can't remove old files") - end - status = os.execute("tar xzf " .. tmp .. " -C " .. destination) - if status ~= 0 then - return self:response(ngx.HTTP_BAD_REQUEST, "error", "can't extract archive") + local cmds = { + "rm -rf " .. destination .. "/*", + "tar xzf " .. tmp .. " -C " .. destination + } + for i, cmd in ipairs(cmds) do + local status = os.execute(cmd) + if status ~= 0 then + return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status)) + end end return self:response(ngx.HTTP_OK, "success", "saved data at " .. destination) end diff --git a/src/bw/lua/bunkerweb/cachestore.lua b/src/bw/lua/bunkerweb/cachestore.lua index db46caa83..0041af7df 100644 --- a/src/bw/lua/bunkerweb/cachestore.lua +++ b/src/bw/lua/bunkerweb/cachestore.lua @@ -1,4 +1,5 @@ local mlcache = require "resty.mlcache" +local clusterstore = require "bunkerweb.clusterstore" local logger = require "bunkerweb.logger" local utils = require "bunkerweb.utils" local class = require "middleclass" @@ -41,17 +42,24 @@ if not cache then module_logger:log(ngx.ERR, "can't instantiate mlcache : " .. err) end -function cachestore:initialize(use_redis) +function cachestore:initialize(use_redis, new_cs) self.cache = cache - self.use_redis = (use_redis and utils.is_cosocket_available()) or false + self.use_redis = use_redis or false self.logger = module_logger + if new_cs then + self.clusterstore = clusterstore:new(false) + self.shared_cs = false + else + self.clusterstore = utils.get_ctx_obj("clusterstore") + self.shared_cs = true + end end function cachestore:get(key) - local callback = function(key) + local callback = function(key, cs) -- Connect to redis - local clusterstore = require "bunkerweb.clusterstore":new() - local ok, err = clusterstore:connect() + local clusterstore = cs or require "bunkerweb.clusterstore":new(false) + local ok, err, reused = clusterstore:connect() if not ok then return nil, "can't connect to redis : " .. err, nil end @@ -88,8 +96,12 @@ function cachestore:get(key) return nil, nil, -1 end local value, err, hit_level - if self.use_redis then - value, err, hit_level = self.cache:get(key, nil, callback, key) + if self.use_redis and utils.is_cosocket_available() then + local cs = nil + if self.shared_cs then + cs = self.clusterstore + end + value, err, hit_level = self.cache:get(key, nil, callback, key, cs) else value, err, hit_level = self.cache:get(key, nil, callback_no_miss) end @@ -101,7 +113,7 @@ function cachestore:get(key) end function cachestore:set(key, value, ex) - if self.use_redis then + if self.use_redis and utils.is_cosocket_available() then local ok, err = self:set_redis(key, value, ex) if not ok then self.logger:log(ngx.ERR, err) @@ -121,24 +133,23 @@ end function cachestore:set_redis(key, value, ex) -- Connect to redis - local clusterstore = require "bunkerweb.clusterstore":new() - local ok, err = clusterstore:connect() + local ok, err, reused = self.clusterstore:connect() if not ok then return false, "can't connect to redis : " .. err end -- Set value with ttl local default_ex = ex or 30 - local ok, err = clusterstore:call("set", key, value, "EX", default_ex) + local ok, err = self.clusterstore:call("set", key, value, "EX", default_ex) if err then - clusterstore:close() + self.clusterstore:close() return false, "SET failed : " .. err end - clusterstore:close() + self.clusterstore:close() return true end function cachestore:delete(key, value, ex) - if self.use_redis then + if self.use_redis and utils.is_cosocket_available() then local ok, err = self.del_redis(key) if not ok then self.logger:log(ngx.ERR, err) @@ -153,18 +164,17 @@ end function cachestore:del_redis(key) -- Connect to redis - local clusterstore = require "bunkerweb.clusterstore":new() - local ok, err = clusterstore:connect() + local ok, err = self.clusterstore:connect() if not ok then return false, "can't connect to redis : " .. err end -- Set value with ttl - local ok, err = clusterstore:del(key) + local ok, err = self.clusterstore:del(key) if err then - clusterstore:close() + self.clusterstore:close() return false, "DEL failed : " .. err end - clusterstore:close() + self.clusterstore:close() return true end diff --git a/src/bw/lua/bunkerweb/clusterstore.lua b/src/bw/lua/bunkerweb/clusterstore.lua index 56490979b..1f96aabc3 100644 --- a/src/bw/lua/bunkerweb/clusterstore.lua +++ b/src/bw/lua/bunkerweb/clusterstore.lua @@ -5,7 +5,7 @@ local redis = require "resty.redis" local clusterstore = class("clusterstore") -function clusterstore:initialize() +function clusterstore:initialize(pool) -- Instantiate logger self.logger = logger:new("CLUSTERSTORE") -- Get variables @@ -29,12 +29,13 @@ function clusterstore:initialize() end -- Don't instantiate a redis object for now self.redis_client = nil + self.pool = pool == nil or pool end function clusterstore:connect() -- Check if we are already connected - if self.redis_client ~= nil then - return true, "already connected" + if self.redis_client then + return true, "already connected", self.redis_client:get_reused_times() end -- Instantiate object local redis_client, err = redis:new() @@ -42,42 +43,50 @@ function clusterstore:connect() return false, err end -- Set timeouts - redis_client:set_timeouts(tonumber(self.variables["REDIS_TIMEOUT"]), tonumber(self.variables["REDIS_TIMEOUT"]), - tonumber(self.variables["REDIS_TIMEOUT"])) + redis_client:set_timeout(tonumber(self.variables["REDIS_TIMEOUT"])) -- Connect local options = { ssl = self.variables["REDIS_SSL"] == "yes", - pool = "bw", - pool_size = tonumber(self.variables["REDIS_KEEPALIVE_POOL"]) } + if self.pool then + options.pool = "bw-redis" + options.pool_size = tonumber(self.variables["REDIS_KEEPALIVE_POOL"]) + end local ok, err = redis_client:connect(self.variables["REDIS_HOST"], tonumber(self.variables["REDIS_PORT"]), options) if not ok then return false, err end - -- Save client self.redis_client = redis_client -- Select database if needed - local times, err = redis_client:get_reused_times() + local times, err = self.redis_client:get_reused_times() if err then self:close() return false, err end if times == 0 then - local select, err = redis_client:select(tonumber(self.variables["REDIS_DATABASE"])) + local select, err = self.redis_client:select(tonumber(self.variables["REDIS_DATABASE"])) if err then self:close() return false, err end end - return true, "success" + return true, "success", times end function clusterstore:close() if self.redis_client then -- Equivalent to close but keep a pool of connections - local ok, err = self.redis_client:set_keepalive(tonumber(self.variables["REDIS_KEEPALIVE_IDLE"]), - tonumber(self.variables["REDIS_KEEPALIVE_POOL"])) - self.redis_client = nil + if self.pool then + local ok, err = self.redis_client:set_keepalive(tonumber(self.variables["REDIS_KEEPALIVE_IDLE"]), tonumber(self.variables["REDIS_KEEPALIVE_POOL"])) + self.redis_client = nil + if not ok then + require "bunkerweb.logger":new("clusterstore-close"):log(ngx.ERR, err) + end + return ok, err + end + -- Close + local ok, err = self.redis_client:close() + self.redis_client.redis_client = nil return ok, err end return false, "not connected" diff --git a/src/bw/lua/bunkerweb/helpers.lua b/src/bw/lua/bunkerweb/helpers.lua index a00a6dd12..e2248039b 100644 --- a/src/bw/lua/bunkerweb/helpers.lua +++ b/src/bw/lua/bunkerweb/helpers.lua @@ -146,45 +146,50 @@ helpers.call_plugin = function(plugin, method) end helpers.fill_ctx = function() - -- Check if ctx is already filled - if ngx.ctx.bw then - return true, "already filled" - end -- Return errors as table local errors = {} - -- Instantiate bw table - local data = {} - -- Common vars - data.kind = "http" - if ngx.shared.datastore_stream then - data.kind = "stream" + -- Check if ctx is already filled + if not ngx.ctx.bw then + -- Instantiate bw table + local data = {} + -- Common vars + data.kind = "http" + if ngx.shared.datastore_stream then + data.kind = "stream" + end + data.remote_addr = ngx.var.remote_addr + data.uri = ngx.var.uri + data.request_uri = ngx.var.request_uri + data.request_method = ngx.var.request_method + data.http_user_agent = ngx.var.http_user_agent + data.http_host = ngx.var.http_host + data.server_name = ngx.var.server_name + data.http_content_type = ngx.var.http_content_type + data.http_origin = ngx.var.http_origin + -- IP data : global + local ip_is_global, err = utils.ip_is_global(data.remote_addr) + if ip_is_global == nil then + table.insert(errors, "can't check if IP is global : " .. err) + else + data.ip_is_global = ip_is_global + end + -- IP data : v4 / v6 + data.ip_is_ipv4 = utils.is_ipv4(data.ip) + data.ip_is_ipv6 = utils.is_ipv6(data.ip) + -- Misc info + data.integration = utils.get_integration() + data.version = utils.get_version() + -- Fill ctx + ngx.ctx.bw = data end - data.remote_addr = ngx.var.remote_addr - data.uri = ngx.var.uri - data.request_uri = ngx.var.request_uri - data.request_method = ngx.var.request_method - data.http_user_agent = ngx.var.http_user_agent - data.http_host = ngx.var.http_host - data.server_name = ngx.var.server_name - data.http_content_type = ngx.var.http_content_type - data.http_origin = ngx.var.http_origin - -- IP data : global - local ip_is_global, err = utils.ip_is_global(data.remote_addr) - if ip_is_global == nil then - table.insert(errors, "can't check if IP is global : " .. err) - else - data.ip_is_global = ip_is_global + -- Always create new objects for current phases in case of cosockets + local use_redis, err = utils.get_variable("USE_REDIS", false) + if not use_redis then + table.insert(errors, "can't get variable from datastore : " .. err) end - -- IP data : v4 / v6 - data.ip_is_ipv4 = utils.is_ipv4(data.ip) - data.ip_is_ipv6 = utils.is_ipv6(data.ip) - -- Misc info - data.integration = utils.get_integration() - data.version = utils.get_version() - -- Plugins - data.plugins = {} - -- Fill ctx - ngx.ctx.bw = data + ngx.ctx.bw.datastore = require "bunkerweb.datastore":new() + ngx.ctx.bw.clusterstore = require "bunkerweb.clusterstore":new() + ngx.ctx.bw.cachestore = require "bunkerweb.cachestore":new(use_redis == "yes") return true, "ctx filled", errors end diff --git a/src/bw/lua/bunkerweb/plugin.lua b/src/bw/lua/bunkerweb/plugin.lua index b72a08d19..b4e791134 100644 --- a/src/bw/lua/bunkerweb/plugin.lua +++ b/src/bw/lua/bunkerweb/plugin.lua @@ -1,17 +1,40 @@ local class = require "middleclass" local logger = require "bunkerweb.logger" local datastore = require "bunkerweb.datastore" +local cachestore = require "bunkerweb.cachestore" +local clusterstore = require "bunkerweb.clusterstore" local utils = require "bunkerweb.utils" local cjson = require "cjson" local plugin = class("plugin") function plugin:initialize(id) - -- Store default values + -- Store common, values self.id = id - self.variables = {} - -- Instantiate objects - self.logger = logger:new(id) - self.datastore = datastore:new() + local multisite = false + local current_phase = ngx.get_phase() + for i, check_phase in ipairs({ "set", "access", "content", "header", "log", "preread", "log_stream", "log_default" }) do + if current_phase == check_phase then + multisite = true + break + end + end + self.is_request = multisite + -- Store common objets + self.logger = logger:new(self.id) + local use_redis, err = utils.get_variable("USE_REDIS", false) + if not use_redis then + self.logger:log(ngx.ERR, err) + end + self.use_redis = use_redis == "yes" + if self.is_request then + self.datastore = utils.get_ctx_obj("datastore") or datastore:new() + self.cachestore = utils.get_ctx_obj("cachestore") or cachestore:new(use_redis == "yes", true) + self.clusterstore = utils.get_ctx_obj("clusterstore") or clusterstore:new(false) + else + self.datastore = datastore:new() + self.cachestore = cachestore:new(use_redis == "yes", true) + self.clusterstore = clusterstore:new(false) + end -- Get metadata local encoded_metadata, err = self.datastore:get("plugin_" .. id) if not encoded_metadata then @@ -19,16 +42,8 @@ function plugin:initialize(id) return end -- Store variables + self.variables = {} local metadata = cjson.decode(encoded_metadata) - local multisite = false - local current_phase = ngx.get_phase() - for i, check_phase in ipairs({ "set", "access", "log", "preread" }) do - if current_phase == check_phase then - multisite = true - break - end - end - self.is_request = multisite for k, v in pairs(metadata.settings) do local value, err = utils.get_variable(k, v.context == "multisite" and multisite) if value == nil then diff --git a/src/bw/lua/bunkerweb/utils.lua b/src/bw/lua/bunkerweb/utils.lua index a14a63d24..95c0ed7e1 100644 --- a/src/bw/lua/bunkerweb/utils.lua +++ b/src/bw/lua/bunkerweb/utils.lua @@ -12,6 +12,8 @@ local datastore = cdatastore:new() local utils = {} +math.randomseed(os.time()) + utils.get_variable = function(var, site_search) -- Default site search to true if site_search == nil then @@ -363,7 +365,6 @@ utils.get_rdns = function(ip) for i, answer in ipairs(answers) do if answer.ptrdname then table.insert(ptrs, answer.ptrdname) - logger:log(ngx.ERR, answer.ptrdname) end end end @@ -510,22 +511,74 @@ utils.get_deny_status = function() return tonumber(status) end +utils.check_session = function() + local _session, err, exists, refreshed = session.start({audience = "metadata"}) + if exists then + for i, check in ipairs(ngx.ctx.bw.sessions_checks) do + local key = check[1] + local value = check[2] + if _session:get(key) ~= value then + local ok, err = _session:destroy() + if not ok then + _session:close() + return false, "session:destroy() error : " .. err + end + logger:log(ngx.WARN, "session check " .. key .. " failed, destroying session") + return utils.check_session() + end + end + else + for i, check in ipairs(ngx.ctx.bw.sessions_checks) do + _session:set(check[1], check[2]) + end + local ok, err = _session:save() + if not ok then + _session:close() + return false, "session:save() error : " .. err + end + end + ngx.ctx.bw.sessions_is_checked = true + _session:close() + return true, exists +end + utils.get_session = function(audience) - -- Session already in context - if ngx.ctx.bw.session then - ngx.ctx.bw.session:set_audience(audience) - return ngx.ctx.bw.session + -- Check session + if not ngx.ctx.bw.sessions_is_checked then + local ok, err = utils.check_session() + if not ok then + return false, "error while checking session, " .. err + end end - -- Open session and fill ctx - local _session, err, exists, refreshed = session.start({ audience = audience }) - if err and err ~= "missing session cookie" and err ~= "no session" then - logger:log(ngx.ERR, "session:start() error : " .. err) + -- Open session with specific audience + local _session, err, exists = session.open({audience = audience}) + if err then + logger:log(ngx.INFO, "session:open() error : " .. err) end - _session:set_audience(audience) - ngx.ctx.bw.session = _session return _session end +utils.get_session_data = function(_session, site) + local site_only = site == nil or site + local data = _session:get_data() + if site_only then + return data[ngx.ctx.bw.server_name] or {} + end + return data +end + +utils.set_session_data = function(_session, data, site) + local site_only = site == nil or site + if site_only then + local all_data = _session:get_data() + all_data[ngx.ctx.bw.server_name] = data + _session:set_data(all_data) + return _session:save() + end + _session:set_data(data) + return _session:save() +end + utils.is_banned = function(ip) -- Check on local datastore local reason, err = datastore:get("bans_ip_" .. ip) @@ -627,7 +680,7 @@ utils.new_cachestore = function() use_redis = use_redis == "yes" end -- Instantiate - return require "bunkerweb.cachestore":new(use_redis) + return require "bunkerweb.cachestore":new(use_redis, true) end utils.regex_match = function(str, regex, options) @@ -672,4 +725,20 @@ utils.is_cosocket_available = function() return false end +utils.kill_all_threads = function(threads) + for i, thread in ipairs(threads) do + local ok, err = ngx.thread.kill(thread) + if not ok then + logger:log(ngx.ERR, "error while killing thread : " .. err) + end + end +end + +utils.get_ctx_obj = function(obj) + if ngx.ctx and ngx.ctx.bw then + return ngx.ctx.bw[obj] + end + return nil +end + return utils diff --git a/src/common/confs/default-server-http.conf b/src/common/confs/default-server-http.conf index b3e626030..e6f600c46 100644 --- a/src/common/confs/default-server-http.conf +++ b/src/common/confs/default-server-http.conf @@ -66,10 +66,6 @@ server { local order, err = datastore:get("plugins_order") if not order then logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err) - local ok, err = lock:unlock() - if not ok then - logger:log(ngx.ERR, "lock:unlock() failed : " .. err) - end return end order = cjson.decode(order) diff --git a/src/common/confs/http.conf b/src/common/confs/http.conf index a4fc165ea..8494be3bb 100644 --- a/src/common/confs/http.conf +++ b/src/common/confs/http.conf @@ -49,6 +49,10 @@ lua_shared_dict cachestore {{ CACHESTORE_MEMORY_SIZE }}; lua_shared_dict cachestore_ipc {{ CACHESTORE_IPC_MEMORY_SIZE }}; lua_shared_dict cachestore_miss {{ CACHESTORE_MISS_MEMORY_SIZE }}; lua_shared_dict cachestore_locks {{ CACHESTORE_LOCKS_MEMORY_SIZE }}; +# only show LUA socket errors at info/debug +{% if LOG_LEVEL != "info" and LOG_LEVEL != "debug" %} +lua_socket_log_errors off; +{% endif %} # LUA init block include /etc/nginx/init-lua.conf; diff --git a/src/common/confs/init-lua.conf b/src/common/confs/init-lua.conf index 00267fb27..66831f70c 100644 --- a/src/common/confs/init-lua.conf +++ b/src/common/confs/init-lua.conf @@ -11,16 +11,9 @@ local logger = clogger:new("INIT") local datastore = cdatastore:new() logger:log(ngx.NOTICE, "init phase started") --- Purge cache -local cachestore = require "bunkerweb.cachestore":new() -local ok, err = cachestore:purge() -if not ok then - logger:log(ngx.ERR, "can't purge cachestore : " .. err) -end - -- Remove previous data from the datastore logger:log(ngx.NOTICE, "deleting old keys from datastore ...") -local data_keys = {"^plugin_", "^variable_", "^plugins$", "^api_", "^misc_"} +local data_keys = {"^plugin", "^variable_", "^api_", "^misc_"} for i, key in pairs(data_keys) do local ok, err = datastore:delete_all(key) if not ok then @@ -50,6 +43,13 @@ for line in io.lines("/etc/nginx/variables.env") do end logger:log(ngx.NOTICE, "saved variables into datastore") +-- Purge cache +local cachestore = require "bunkerweb.cachestore":new(false, true) +local ok, err = cachestore:purge() +if not ok then + logger:log(ngx.ERR, "can't purge cachestore : " .. err) +end + -- Set API values into the datastore logger:log(ngx.NOTICE, "saving API values into datastore ...") local value, err = datastore:get("variable_USE_API") diff --git a/src/common/confs/init-stream-lua.conf b/src/common/confs/init-stream-lua.conf index f3a2b7652..3b994be18 100644 --- a/src/common/confs/init-stream-lua.conf +++ b/src/common/confs/init-stream-lua.conf @@ -1,158 +1,157 @@ init_by_lua_block { - local class = require "middleclass" - local clogger = require "bunkerweb.logger" - local helpers = require "bunkerweb.helpers" - local cdatastore = require "bunkerweb.datastore" - local cjson = require "cjson" - - -- Start init phase - local logger = clogger:new("INIT-STREAM") - local datastore = cdatastore:new() - logger:log(ngx.NOTICE, "init-stream phase started") - - -- Purge cache - local cachestore = require "bunkerweb.cachestore":new() - local ok, err = cachestore:purge() +local class = require "middleclass" +local clogger = require "bunkerweb.logger" +local helpers = require "bunkerweb.helpers" +local cdatastore = require "bunkerweb.datastore" +local cjson = require "cjson" + +-- Start init phase +local logger = clogger:new("INIT") +local datastore = cdatastore:new() +logger:log(ngx.NOTICE, "init-stream phase started") + +-- Remove previous data from the datastore +logger:log(ngx.NOTICE, "deleting old keys from datastore ...") +local data_keys = {"^plugin", "^variable_", "^api_", "^misc_"} +for i, key in pairs(data_keys) do + local ok, err = datastore:delete_all(key) if not ok then - logger:log(ngx.ERR, "can't purge cachestore : " .. err) - end - - -- Remove previous data from the datastore - logger:log(ngx.NOTICE, "deleting old keys from datastore ...") - local data_keys = {"^plugin_", "^variable_", "^plugins$", "^api_", "^misc_"} - for i, key in pairs(data_keys) do - local ok, err = datastore:delete_all(key) - if not ok then - logger:log(ngx.ERR, "can't delete " .. key .. " from datastore : " .. err) - return false - end - logger:log(ngx.INFO, "deleted " .. key .. " from datastore") - end - logger:log(ngx.NOTICE, "deleted old keys from datastore") - - -- Load variables into the datastore - logger:log(ngx.NOTICE, "saving variables into datastore ...") - local file = io.open("/etc/nginx/variables.env") - if not file then - logger:log(ngx.ERR, "can't open /etc/nginx/variables.env file") + logger:log(ngx.ERR, "can't delete " .. key .. " from datastore : " .. err) return false end - file:close() - for line in io.lines("/etc/nginx/variables.env") do - local variable, value = line:match("^([^=]+)=(.*)$") - local ok, err = datastore:set("variable_" .. variable, value) - if not ok then - logger:log(ngx.ERR, "can't save variable " .. variable .. " into datastore : " .. err) - return false - end - logger:log(ngx.INFO, "saved variable " .. variable .. "=" .. value .. " into datastore") + logger:log(ngx.INFO, "deleted " .. key .. " from datastore") +end +logger:log(ngx.NOTICE, "deleted old keys from datastore") + +-- Load variables into the datastore +logger:log(ngx.NOTICE, "saving variables into datastore ...") +local file = io.open("/etc/nginx/variables.env") +if not file then + logger:log(ngx.ERR, "can't open /etc/nginx/variables.env file") + return false +end +file:close() +for line in io.lines("/etc/nginx/variables.env") do + local variable, value = line:match("^([^=]+)=(.*)$") + local ok, err = datastore:set("variable_" .. variable, value) + if not ok then + logger:log(ngx.ERR, "can't save variable " .. variable .. " into datastore : " .. err) + return false end - logger:log(ngx.NOTICE, "saved variables into datastore") - - -- Set API values into the datastore - logger:log(ngx.NOTICE, "saving API values into datastore ...") - local value, err = datastore:get("variable_USE_API") + logger:log(ngx.INFO, "saved variable " .. variable .. "=" .. value .. " into datastore") +end +logger:log(ngx.NOTICE, "saved variables into datastore") + +-- Purge cache +local cachestore = require "bunkerweb.cachestore":new(false, true) +local ok, err = cachestore:purge() +if not ok then + logger:log(ngx.ERR, "can't purge cachestore : " .. err) +end + +-- Set API values into the datastore +logger:log(ngx.NOTICE, "saving API values into datastore ...") +local value, err = datastore:get("variable_USE_API") +if not value then + logger:log(ngx.ERR, "can't get variable USE_API from the datastore : " .. err) + return false +end +if value == "yes" then + local value, err = datastore:get("variable_API_WHITELIST_IP") if not value then - logger:log(ngx.ERR, "can't get variable USE_API from the datastore : " .. err) + logger:log(ngx.ERR, "can't get variable API_WHITELIST_IP from the datastore : " .. err) return false end - if value == "yes" then - local value, err = datastore:get("variable_API_WHITELIST_IP") - if not value then - logger:log(ngx.ERR, "can't get variable API_WHITELIST_IP from the datastore : " .. err) - return false - end - local whitelists = {} - for whitelist in value:gmatch("%S+") do - table.insert(whitelists, whitelist) - end - local ok, err = datastore:set("api_whitelist_ip", cjson.encode(whitelists)) + local whitelists = {} + for whitelist in value:gmatch("%S+") do + table.insert(whitelists, whitelist) + end + local ok, err = datastore:set("api_whitelist_ip", cjson.encode(whitelists)) + if not ok then + logger:log(ngx.ERR, "can't save API whitelist_ip to datastore : " .. err) + return false + end + logger:log(ngx.INFO, "saved API whitelist_ip into datastore") +end +logger:log(ngx.NOTICE, "saved API values into datastore") + +-- Load plugins into the datastore +logger:log(ngx.NOTICE, "saving plugins into datastore ...") +local plugins = {} +local plugin_paths = {"/usr/share/bunkerweb/core", "/etc/bunkerweb/plugins"} +for i, plugin_path in ipairs(plugin_paths) do + local paths = io.popen("find -L " .. plugin_path .. " -maxdepth 1 -type d ! -path " .. plugin_path) + for path in paths:lines() do + local ok, plugin = helpers.load_plugin(path .. "/plugin.json") if not ok then - logger:log(ngx.ERR, "can't save API whitelist_ip to datastore : " .. err) - return false - end - logger:log(ngx.INFO, "saved API whitelist_ip into datastore") - end - logger:log(ngx.NOTICE, "saved API values into datastore") - - -- Load plugins into the datastore - logger:log(ngx.NOTICE, "saving plugins into datastore ...") - local plugins = {} - local plugin_paths = {"/usr/share/bunkerweb/core", "/etc/bunkerweb/plugins"} - for i, plugin_path in ipairs(plugin_paths) do - local paths = io.popen("find -L " .. plugin_path .. " -maxdepth 1 -type d ! -path " .. plugin_path) - for path in paths:lines() do - local ok, plugin = helpers.load_plugin(path .. "/plugin.json") - if not ok then - logger:log(ngx.ERR, plugin) - else - local ok, err = datastore:set("plugin_" .. plugin.id, cjson.encode(plugin)) - if not ok then - logger:log(ngx.ERR, "can't save " .. plugin.id .. " into datastore : " .. err) - else - table.insert(plugins, plugin) - logger:log(ngx.NOTICE, "loaded plugin " .. plugin.id .. " v" .. plugin.version) - end - end - end - end - local ok, err = datastore:set("plugins", cjson.encode(plugins)) - if not ok then - logger:log(ngx.ERR, "can't save plugins into datastore : " .. err) - return false - end - - logger:log(ngx.NOTICE, "saving plugins order into datastore ...") - local ok, order = helpers.order_plugins(plugins) - if not ok then - logger:log(ngx.ERR, "can't compute plugins order : " .. err) - return false - end - for phase, id_list in pairs(order) do - logger:log(ngx.NOTICE, "plugins order for phase " .. phase .. " : " .. cjson.encode(id_list)) - end - local ok, err = datastore:set("plugins_order", cjson.encode(order)) - if not ok then - logger:log(ngx.ERR, "can't save plugins order into datastore : " .. err) - return false - end - logger:log(ngx.NOTICE, "saved plugins order into datastore") - - -- Call init() method - logger:log(ngx.NOTICE, "calling init() methods of plugins ...") - for i, plugin_id in ipairs(order["init"]) do - -- Require call - local plugin_lua, err = helpers.require_plugin(plugin_id) - if plugin_lua == false then - logger:log(ngx.ERR, err) - elseif plugin_lua == nil then - logger:log(ngx.NOTICE, err) + logger:log(ngx.ERR, plugin) else - -- Check if plugin has init method - if plugin_lua.init ~= nil then - -- New call - local ok, plugin_obj = helpers.new_plugin(plugin_lua) - if not ok then - logger:log(ngx.ERR, plugin_obj) - else - local ok, ret = helpers.call_plugin(plugin_obj, "init") - if not ok then - logger:log(ngx.ERR, ret) - elseif not ret.ret then - logger:log(ngx.ERR, plugin_id .. ":init() call failed : " .. ret.msg) - else - logger:log(ngx.NOTICE, plugin_id .. ":init() call successful : " .. ret.msg) - end - end + local ok, err = datastore:set("plugin_" .. plugin.id, cjson.encode(plugin)) + if not ok then + logger:log(ngx.ERR, "can't save " .. plugin.id .. " into datastore : " .. err) else - logger:log(ngx.NOTICE, "skipped execution of " .. plugin.id .. " because method init() is not defined") + table.insert(plugins, plugin) + logger:log(ngx.NOTICE, "loaded plugin " .. plugin.id .. " v" .. plugin.version) end end end - logger:log(ngx.NOTICE, "called init() methods of plugins") - - logger:log(ngx.NOTICE, "init-stream phase ended") - - } - \ No newline at end of file +end +local ok, err = datastore:set("plugins", cjson.encode(plugins)) +if not ok then + logger:log(ngx.ERR, "can't save plugins into datastore : " .. err) + return false +end + +logger:log(ngx.NOTICE, "saving plugins order into datastore ...") +local ok, order = helpers.order_plugins(plugins) +if not ok then + logger:log(ngx.ERR, "can't compute plugins order : " .. err) + return false +end +for phase, id_list in pairs(order) do + logger:log(ngx.NOTICE, "plugins order for phase " .. phase .. " : " .. cjson.encode(id_list)) +end +local ok, err = datastore:set("plugins_order", cjson.encode(order)) +if not ok then + logger:log(ngx.ERR, "can't save plugins order into datastore : " .. err) + return false +end +logger:log(ngx.NOTICE, "saved plugins order into datastore") + +-- Call init() method +logger:log(ngx.NOTICE, "calling init() methods of plugins ...") +for i, plugin_id in ipairs(order["init"]) do + -- Require call + local plugin_lua, err = helpers.require_plugin(plugin_id) + if plugin_lua == false then + logger:log(ngx.ERR, err) + elseif plugin_lua == nil then + logger:log(ngx.NOTICE, err) + else + -- Check if plugin has init method + if plugin_lua.init ~= nil then + -- New call + local ok, plugin_obj = helpers.new_plugin(plugin_lua) + if not ok then + logger:log(ngx.ERR, plugin_obj) + else + local ok, ret = helpers.call_plugin(plugin_obj, "init") + if not ok then + logger:log(ngx.ERR, ret) + elseif not ret.ret then + logger:log(ngx.ERR, plugin_id .. ":init() call failed : " .. ret.msg) + else + logger:log(ngx.NOTICE, plugin_id .. ":init() call successful : " .. ret.msg) + end + end + else + logger:log(ngx.NOTICE, "skipped execution of " .. plugin.id .. " because method init() is not defined") + end + end +end +logger:log(ngx.NOTICE, "called init() methods of plugins") + +logger:log(ngx.NOTICE, "init-stream phase ended") + +} diff --git a/src/common/confs/init-worker-lua.conf b/src/common/confs/init-worker-lua.conf index b5d85ffa0..b7629f12c 100644 --- a/src/common/confs/init-worker-lua.conf +++ b/src/common/confs/init-worker-lua.conf @@ -23,7 +23,7 @@ local ready_work = function(premature) end -- Instantiate lock - local lock = require "resty.lock":new("worker_lock") + local lock = require "resty.lock":new("worker_lock", {timeout = 10}) if not lock then logger:log(ngx.ERR, "lock:new() failed : " .. err) return diff --git a/src/common/confs/server-http/access-lua.conf b/src/common/confs/server-http/access-lua.conf index c697ed3f5..945296bbf 100644 --- a/src/common/confs/server-http/access-lua.conf +++ b/src/common/confs/server-http/access-lua.conf @@ -46,10 +46,6 @@ end local order, err = datastore:get("plugins_order") if not order then logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err) - local ok, err = lock:unlock() - if not ok then - logger:log(ngx.ERR, "lock:unlock() failed : " .. err) - end return end order = cjson.decode(order) diff --git a/src/common/confs/server-http/header-lua.conf b/src/common/confs/server-http/header-lua.conf index 8b8f1c2bb..a8bf6ff80 100644 --- a/src/common/confs/server-http/header-lua.conf +++ b/src/common/confs/server-http/header-lua.conf @@ -27,10 +27,6 @@ logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")") local order, err = datastore:get("plugins_order") if not order then logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err) - local ok, err = lock:unlock() - if not ok then - logger:log(ngx.ERR, "lock:unlock() failed : " .. err) - end return end order = cjson.decode(order) diff --git a/src/common/confs/server-http/log-lua.conf b/src/common/confs/server-http/log-lua.conf index 5689676f9..bb169996c 100644 --- a/src/common/confs/server-http/log-lua.conf +++ b/src/common/confs/server-http/log-lua.conf @@ -27,10 +27,6 @@ logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")") local order, err = datastore:get("plugins_order") if not order then logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err) - local ok, err = lock:unlock() - if not ok then - logger:log(ngx.ERR, "lock:unlock() failed : " .. err) - end return end order = cjson.decode(order) diff --git a/src/common/confs/server-http/set-lua.conf b/src/common/confs/server-http/set-lua.conf index aa6461663..4262454e5 100644 --- a/src/common/confs/server-http/set-lua.conf +++ b/src/common/confs/server-http/set-lua.conf @@ -42,10 +42,6 @@ logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")") local order, err = datastore:get("plugins_order") if not order then logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err) - local ok, err = lock:unlock() - if not ok then - logger:log(ngx.ERR, "lock:unlock() failed : " .. err) - end return end order = cjson.decode(order) diff --git a/src/common/confs/server-stream/log-stream-lua.conf b/src/common/confs/server-stream/log-stream-lua.conf index 97882dee6..ee9da1e46 100644 --- a/src/common/confs/server-stream/log-stream-lua.conf +++ b/src/common/confs/server-stream/log-stream-lua.conf @@ -27,10 +27,6 @@ logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")") local order, err = datastore:get("plugins_order") if not order then logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err) - local ok, err = lock:unlock() - if not ok then - logger:log(ngx.ERR, "lock:unlock() failed : " .. err) - end return end order = cjson.decode(order) @@ -45,7 +41,7 @@ for i, plugin_id in ipairs(order.log_stream) do elseif plugin_lua == nil then logger:log(ngx.INFO, err) else - -- Check if plugin has log method + -- Check if plugin has log_stream method if plugin_lua.log_stream ~= nil then -- New call local ok, plugin_obj = helpers.new_plugin(plugin_lua) diff --git a/src/common/confs/server-stream/preread-stream-lua.conf b/src/common/confs/server-stream/preread-stream-lua.conf index bc151332e..b04bbb0fa 100644 --- a/src/common/confs/server-stream/preread-stream-lua.conf +++ b/src/common/confs/server-stream/preread-stream-lua.conf @@ -77,7 +77,7 @@ for i, plugin_id in ipairs(order.preread) do if ret.status then if ret.status == utils.get_deny_status() then ngx.ctx.reason = plugin_id - logger:log(ngx.WARN, "denied access from " .. plugin_id .. " : " .. ret.msg) + logger:log(ngx.WARN, "denied preread from " .. plugin_id .. " : " .. ret.msg) else logger:log(ngx.NOTICE, plugin_id .. " returned status " .. tostring(ret.status) .. " : " .. ret.msg) end diff --git a/src/common/confs/stream.conf b/src/common/confs/stream.conf index bc3303926..bfa09d7a8 100644 --- a/src/common/confs/stream.conf +++ b/src/common/confs/stream.conf @@ -33,6 +33,10 @@ lua_shared_dict cachestore_stream {{ CACHESTORE_MEMORY_SIZE }}; lua_shared_dict cachestore_ipc_stream {{ CACHESTORE_IPC_MEMORY_SIZE }}; lua_shared_dict cachestore_miss_stream {{ CACHESTORE_MISS_MEMORY_SIZE }}; lua_shared_dict cachestore_locks_stream {{ CACHESTORE_LOCKS_MEMORY_SIZE }}; +# only show LUA socket errors at info/debug +{% if LOG_LEVEL != "info" and LOG_LEVEL != "debug" %} +lua_socket_log_errors off; +{% endif %} # LUA init block include /etc/nginx/init-stream-lua.conf; diff --git a/src/common/core/antibot/antibot.lua b/src/common/core/antibot/antibot.lua index bda29796c..07c235324 100644 --- a/src/common/core/antibot/antibot.lua +++ b/src/common/core/antibot/antibot.lua @@ -26,9 +26,15 @@ function antibot:access() return self:ret(true, "antibot not activated") end - -- Get session and data - self.session = utils.get_session("antibot") - self:get_session_data() + -- Get session data + local session, err = utils.get_session("antibot") + if not session then + return self:ret(false, "can't get session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR) + end + self.session = session + self.session_data = utils.get_session_data(self.session) + -- Check if session is valid + self:check_session() -- Don't go further if client resolved the challenge if self.session_data.resolved then @@ -50,6 +56,11 @@ function antibot:access() return self:ret(true, "redirecting client to the challenge uri", nil, self.variables["ANTIBOT_URI"]) end + -- Cookie case : don't display challenge page + if self.session_data.resolved then + return self:ret(true, "client already resolved the challenge", nil, self.session_data.original_uri) + end + -- Display challenge needed if ngx.ctx.bw.request_method == "GET" then ngx.ctx.bw.antibot_display_content = true @@ -89,13 +100,25 @@ function antibot:content() if self.variables["USE_ANTIBOT"] == "no" then return self:ret(true, "antibot not activated") end + -- Check if display content is needed if not ngx.ctx.bw.antibot_display_content then return self:ret(true, "display content not needed", nil, "/") end - -- Get session and data - self.session = utils.get_session("antibot") - self:get_session_data(true) + + -- Get session data + local session, err = utils.get_session("antibot") + if not session then + return self:ret(false, "can't get session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR) + end + self.session = session + self.session_data = utils.get_session_data(self.session) + + -- Direct access without session + if not self.session_data.prepared then + return self:ret(true, "no session", nil, "/") + end + -- Display content local ok, err = self:display_challenge() if not ok then @@ -104,50 +127,42 @@ function antibot:content() return self:ret(true, "content displayed") end -function antibot:get_session_data(no_check) - local session_data = self.session:get_data() - if session_data[ngx.ctx.bw.server_name] then - local data = cjson.decode(session_data[ngx.ctx.bw.server_name]) - if no_check then - self.session_data = data - return - end - if not data.time_resolve and not data.time_valid then - self.session_data = {} - self.session_updated = true - return - end - local time = ngx.now() - self.session_data = data - -- Check valid time - if data.resolved and (data.time_valid > time or time - data.time_valid > tonumber(self.variables["ANTIBOT_TIME_VALID"])) then - self.session_data.resolved = false - self.session_data.prepared = false - self.session_updated = true - return - end - -- Check resolve time - if not data.resolved and (data.time_resolve > time or time - data.time_resolve > tonumber(self.variables["ANTIBOT_TIME_RESOLVE"])) then - self.session_data.prepared = false - self.session_updated = true - return - end - -- Session is valid +function antibot:check_session() + -- Get values + local time_resolve = self.session_data.time_resolve + local time_valid = self.session_data.time_valid + -- Not resolved and not prepared + if not time_resolve and not time_valid then + self.session_data = {} + self.session_updated = true + return + end + -- Check if still valid + local time = ngx.now() + local resolved = self.session_data.resolved + if resolved and (time_valid > time or time - time_valid > tonumber(self.variables["ANTIBOT_TIME_VALID"])) then + self.session_data = {} + self.session_updated = true + return + end + -- Check if new prepare is needed + if not resolved and (time_resolve > time or time - time_resolve > tonumber(self.variables["ANTIBOT_TIME_RESOLVE"])) then + self.session_data = {} + self.session_updated = true return end - self.session_data = {} - self.session_updated = true - return end function antibot:set_session_data() if self.session_updated then - local session_data = self.session:get_data() - session_data[ngx.ctx.bw.server_name] = cjson.encode(self.session_data) - self.session:set_data(session_data) - return self.session:save() + local ok, err = utils.set_session_data(self.session, self.session_data) + if not ok then + return false, err + end + self.session_updated = false + return true, "updated" end - return true, "no updates" + return true, "no update" end function antibot:prepare_challenge() diff --git a/src/common/core/badbehavior/badbehavior.lua b/src/common/core/badbehavior/badbehavior.lua index 67277f931..11dccdd2f 100644 --- a/src/common/core/badbehavior/badbehavior.lua +++ b/src/common/core/badbehavior/badbehavior.lua @@ -7,12 +7,6 @@ local badbehavior = class("badbehavior", plugin) function badbehavior:initialize() -- Call parent initialize plugin.initialize(self, "badbehavior") - -- Check if redis is enabled - local use_redis, err = utils.get_variable("USE_REDIS", false) - if not use_redis then - self.logger:log(ngx.ERR, err) - end - self.use_redis = use_redis == "yes" end function badbehavior:log() @@ -146,7 +140,7 @@ end function badbehavior.redis_increase(ip, count_time, ban_time) -- Instantiate objects - local clusterstore = require "bunkerweb.clusterstore":new() + local clusterstore = require "bunkerweb.clusterstore":new(false) -- Our LUA script to execute on redis local redis_script = [[ local ret_incr = redis.pcall("INCR", KEYS[1]) @@ -188,7 +182,7 @@ end function badbehavior.redis_decrease(ip, count_time) -- Instantiate objects - local clusterstore = require "bunkerweb.clusterstore":new() + local clusterstore = require "bunkerweb.clusterstore":new(false) -- Our LUA script to execute on redis local redis_script = [[ local ret_decr = redis.pcall("DECR", KEYS[1]) diff --git a/src/common/core/blacklist/blacklist.lua b/src/common/core/blacklist/blacklist.lua index 2d99f637f..ea332ec95 100644 --- a/src/common/core/blacklist/blacklist.lua +++ b/src/common/core/blacklist/blacklist.lua @@ -11,12 +11,6 @@ local blacklist = class("blacklist", plugin) function blacklist:initialize() -- Call parent initialize plugin.initialize(self, "blacklist") - -- Check if redis is enabled - local use_redis, err = utils.get_variable("USE_REDIS", false) - if not use_redis then - self.logger:log(ngx.ERR, err) - end - self.use_redis = use_redis == "yes" -- Decode lists if ngx.get_phase() ~= "init" and self:is_needed() then local lists, err = self.datastore:get("plugin_blacklist_lists") @@ -47,8 +41,6 @@ function blacklist:initialize() end end end - -- Instantiate cachestore - self.cachestore = cachestore:new(self.use_redis) end function blacklist:is_needed() @@ -267,20 +259,21 @@ function blacklist:is_blacklisted_ip() if ngx.ctx.bw.ip_is_global then local asn, err = utils.get_asn(ngx.ctx.bw.remote_addr) if not asn then - return nil, "ASN " .. err - end - local ignore = false - for i, ignore_asn in ipairs(self.lists["IGNORE_ASN"]) do - if ignore_asn == tostring(asn) then - ignore = true - break + self.logger:log(ngx.ERR, "can't get ASN of IP " .. ngx.ctx.bw.remote_addr .. " : " .. err) + else + local ignore = false + for i, ignore_asn in ipairs(self.lists["IGNORE_ASN"]) do + if ignore_asn == tostring(asn) then + ignore = true + break + end end - end - -- Check if ASN is in blacklist - if not ignore then - for i, bl_asn in ipairs(self.lists["ASN"]) do - if bl_asn == tostring(asn) then - return true, "ASN " .. bl_asn + -- Check if ASN is in blacklist + if not ignore then + for i, bl_asn in ipairs(self.lists["ASN"]) do + if bl_asn == tostring(asn) then + return true, "ASN " .. bl_asn + end end end end diff --git a/src/common/core/clientcache/plugin.json b/src/common/core/clientcache/plugin.json index 82af93dff..74b6331af 100644 --- a/src/common/core/clientcache/plugin.json +++ b/src/common/core/clientcache/plugin.json @@ -38,7 +38,7 @@ "help": "Value of the Cache-Control HTTP header.", "id": "client-cache-control", "label": "Cache-Control header", - "regex": "^(?!(, ?| ))((, )?(((max-age|s-maxage|stale-while-revalidate|stale-if-error)=[0-9]+(?!.*6))|((?!.*public)private|(?!.*private)public)|(must|proxy)-revalidate|must-understand|immutable|no-(cache|store|transform)))+$", + "regex": "^(?!(, ?| ))((, )?(((max-age|s-maxage|stale-while-revalidate|stale-if-error)=\\d+(?!.*\\6))|((?!.*public)private|(?!.*private)public)|(must|proxy)-revalidate|must-understand|immutable|no-(cache|store|transform))(?!.*\\4))+$", "type": "text" } } diff --git a/src/common/core/country/country.lua b/src/common/core/country/country.lua index f02cb351b..9b7c56d41 100644 --- a/src/common/core/country/country.lua +++ b/src/common/core/country/country.lua @@ -9,13 +9,6 @@ local country = class("country", plugin) function country:initialize() -- Call parent initialize plugin.initialize(self, "country") - -- Instantiate cachestore - local use_redis, err = utils.get_variable("USE_REDIS", false) - if not use_redis then - self.logger:log(ngx.ERR, err) - end - self.use_redis = use_redis == "yes" - self.cachestore = cachestore:new(self.use_redis) end function country:access() diff --git a/src/common/core/dnsbl/dnsbl.lua b/src/common/core/dnsbl/dnsbl.lua index 1bc2a02cd..614bf2175 100644 --- a/src/common/core/dnsbl/dnsbl.lua +++ b/src/common/core/dnsbl/dnsbl.lua @@ -10,13 +10,6 @@ local dnsbl = class("dnsbl", plugin) function dnsbl:initialize() -- Call parent initialize plugin.initialize(self, "dnsbl") - -- Instantiate cachestore - local use_redis, err = utils.get_variable("USE_REDIS", false) - if not use_redis then - self.logger:log(ngx.ERR, err) - end - self.use_redis = use_redis == "yes" - self.cachestore = cachestore:new(self.use_redis) end function dnsbl:init_worker() @@ -32,9 +25,18 @@ function dnsbl:init_worker() return self:ret(true, "no service uses DNSBL, skipping init_worker") end -- Loop on DNSBL list + local threads = {} for server in self.variables["DNSBL_LIST"]:gmatch("%S+") do - local result, err = self:is_in_dnsbl("127.0.0.2", server) - if result == nil then + -- Create thread + local thread = ngx.thread.spawn(self.is_in_dnsbl, self, "127.0.0.2", server) + threads[server] = thread + end + -- Wait for threads + for dnsbl, thread in pairs(threads) do + local ok, result, server, err = ngx.thread.wait(thread) + if not ok then + self.logger:log(ngx.ERR, "error while waiting thread of " .. dnsbl .. " check : " .. result) + elseif result == nil then self.logger:log(ngx.ERR, "error while sending DNS request to " .. server .. " : " .. err) elseif not result then self.logger:log(ngx.ERR, "dnsbl check for " .. server .. " failed") @@ -69,25 +71,74 @@ function dnsbl:access() utils.get_deny_status()) end -- Loop on DNSBL list + local threads = {} for server in self.variables["DNSBL_LIST"]:gmatch("%S+") do - local result, err = self:is_in_dnsbl(ngx.ctx.bw.remote_addr, server) + -- Create thread + local thread = ngx.thread.spawn(self.is_in_dnsbl, self, ngx.ctx.bw.remote_addr, server) + threads[server] = thread + end + -- Wait for threads + local ret_threads = nil + local ret_err = nil + local ret_server = nil + while true do + -- Compute threads to wait + local wait_threads = {} + for dnsbl, thread in pairs(threads) do + table.insert(wait_threads, thread) + end + -- No server reported IP + if #wait_threads == 0 then + break + end + -- Wait for first thread + local ok, result, server, err = ngx.thread.wait(unpack(wait_threads)) + -- Error case + if not ok then + ret_threads = false + ret_err = "error while waiting thread : " .. result + break + end + -- Remove thread from list + threads[server] = nil + -- DNS error if result == nil then self.logger:log(ngx.ERR, "error while sending DNS request to " .. server .. " : " .. err) end + -- IP is in DNSBL if result then - local ok, err = self:add_to_cache(ngx.ctx.bw.remote_addr, server) + ret_threads = true + ret_err = "IP is blacklisted by " .. server + ret_server = server + break + end + end + if ret_threads ~= nil then + -- Kill other threads + if #threads > 0 then + local wait_threads = {} + for dnsbl, thread in pairs(threads) do + table.insert(wait_threads, thread) + end + utils.kill_all_threads(wait_threads) + end + -- Blacklisted by a server : add to cache and deny access + if ret_threads then + local ok, err = self:add_to_cache(ngx.ctx.bw.remote_addr, ret_server) if not ok then return self:ret(false, "error while adding element to cache : " .. err) end - return self:ret(true, "IP is blacklisted by " .. server, utils.get_deny_status()) + return self:ret(true, "IP is blacklisted by " .. ret_server, utils.get_deny_status()) end + -- Error case + return self:ret(false, ret_err) end -- IP is not in DNSBL local ok, err = self:add_to_cache(ngx.ctx.bw.remote_addr, "ok") if not ok then return self:ret(false, "IP is not in DNSBL (error = " .. err .. ")") end - return self:ret(true, "IP is not in DNSBL", false, nil) + return self:ret(true, "IP is not in DNSBL") end function dnsbl:preread() @@ -114,14 +165,14 @@ function dnsbl:is_in_dnsbl(ip, server) local request = resolver.arpa_str(ip):gsub("%.in%-addr%.arpa", ""):gsub("%.ip6%.arpa", "") .. "." .. server local ips, err = utils.get_ips(request, false) if not ips then - return nil, err + return nil, server, err end for i, ip in ipairs(ips) do if ip:find("^127%.0%.0%.") then - return true, "success" + return true, server end end - return false, "success" + return false, server end return dnsbl diff --git a/src/common/core/greylist/greylist.lua b/src/common/core/greylist/greylist.lua index 0817c5624..0ec9d710a 100644 --- a/src/common/core/greylist/greylist.lua +++ b/src/common/core/greylist/greylist.lua @@ -10,12 +10,6 @@ local greylist = class("greylist", plugin) function greylist:initialize() -- Call parent initialize plugin.initialize(self, "greylist") - -- Check if redis is enabled - local use_redis, err = utils.get_variable("USE_REDIS", false) - if not use_redis then - self.logger:log(ngx.ERR, err) - end - self.use_redis = use_redis == "yes" -- Decode lists if ngx.get_phase() ~= "init" and self:is_needed() then local lists, err = self.datastore:get("plugin_greylist_lists") @@ -41,8 +35,6 @@ function greylist:initialize() end end end - -- Instantiate cachestore - self.cachestore = cachestore:new(self.use_redis) end function greylist:is_needed() @@ -216,11 +208,12 @@ function greylist:is_greylisted_ip() if ngx.ctx.bw.ip_is_global then local asn, err = utils.get_asn(ngx.ctx.bw.remote_addr) if not asn then - return nil, "ASN " .. err - end - for i, bl_asn in ipairs(self.lists["ASN"]) do - if bl_asn == tostring(asn) then - return true, "ASN " .. bl_asn + self.logger:log(ngx.ERR, "can't get ASN of IP " .. ngx.ctx.bw.remote_addr .. " : " .. err) + else + for i, bl_asn in ipairs(self.lists["ASN"]) do + if bl_asn == tostring(asn) then + return true, "ASN " .. bl_asn + end end end end diff --git a/src/common/core/limit/limit.lua b/src/common/core/limit/limit.lua index 6621a8197..61bee7b6e 100644 --- a/src/common/core/limit/limit.lua +++ b/src/common/core/limit/limit.lua @@ -10,13 +10,6 @@ local limit = class("limit", plugin) function limit:initialize() -- Call parent initialize plugin.initialize(self, "limit") - -- Check if redis is enabled - local use_redis, err = utils.get_variable("USE_REDIS", false) - if not use_redis then - self.logger:log(ngx.ERR, err) - end - self.use_redis = use_redis == "yes" - self.clusterstore = clusterstore:new() -- Load rules if needed if ngx.get_phase() ~= "init" and self:is_needed() then -- Get all rules from datastore diff --git a/src/common/core/redis/redis.lua b/src/common/core/redis/redis.lua index 3f5102283..eb911da7f 100644 --- a/src/common/core/redis/redis.lua +++ b/src/common/core/redis/redis.lua @@ -9,7 +9,6 @@ local redis = class("redis", plugin) function redis:initialize() -- Call parent initialize plugin.initialize(self, "redis") - self.clusterstore = clusterstore:new() end function redis:init_worker() diff --git a/src/common/core/reversescan/reversescan.lua b/src/common/core/reversescan/reversescan.lua index 11a5dd2d5..d76d9cb18 100644 --- a/src/common/core/reversescan/reversescan.lua +++ b/src/common/core/reversescan/reversescan.lua @@ -2,19 +2,13 @@ local class = require "middleclass" local plugin = require "bunkerweb.plugin" local utils = require "bunkerweb.utils" local cachestore = require "bunkerweb.cachestore" +local cjson = require "cjson" local reversescan = class("reversescan", plugin) function reversescan:initialize() -- Call parent initialize plugin.initialize(self, "reversescan") - -- Instantiate cachestore - local use_redis, err = utils.get_variable("USE_REDIS", false) - if not use_redis then - self.logger:log(ngx.ERR, err) - end - self.use_redis = use_redis == "yes" - self.cachestore = cachestore:new(self.use_redis) end function reversescan:access() @@ -23,31 +17,103 @@ function reversescan:access() return self:ret(true, "reverse scan not activated") end -- Loop on ports + local threads = {} + local ret_threads = nil + local ret_err = nil for port in self.variables["REVERSE_SCAN_PORTS"]:gmatch("%S+") do -- Check if the scan is already cached local ok, cached = self:is_in_cache(ngx.ctx.bw.remote_addr .. ":" .. port) if not ok then - return self:ret(false, "error getting cache from datastore : " .. cached) - end - if cached == "open" then - return self:ret(true, "port " .. port .. " is opened for IP " .. ngx.ctx.bw.remote_addr, - utils.get_deny_status()) + ret_threads = false + ret_err = "error getting info from cachestore : " .. cached + break + -- Deny access if port opened + elseif cached == "open" then + ret_threads = true + ret_err = "port " .. port .. " is opened for IP " .. ngx.ctx.bw.remote_addr + break + -- Perform scan in a thread elseif not cached then - -- Do the scan - local res = self:scan(ngx.ctx.bw.remote_addr, tonumber(port), - tonumber(self.variables["REVERSE_SCAN_TIMEOUT"])) - -- Cache the result - local ok, err = self:add_to_cache(ngx.ctx.bw.remote_addr .. ":" .. port, res) - if not ok then - return self:ret(false, "error updating cache from datastore : " .. err) - end - -- Deny request if port is open - if res == "open" then - return self:ret(true, "port " .. port .. " is opened for IP " .. ngx.ctx.bw.remote_addr, - utils.get_deny_status()) - end + local thread = ngx.thread.spawn(self.scan, ngx.ctx.bw.remote_addr, tonumber(port), tonumber(self.variables["REVERSE_SCAN_TIMEOUT"])) + threads[port] = thread end end + if ret_threads ~= nil then + if #threads > 0 then + local wait_threads = {} + for port, thread in pairs(threads) do + table.insert(wait_threads, thread) + end + utils.kill_all_threads(wait_threads) + end + -- Open port case + if ret_threads then + return self:ret(true, ret_err, utils.get_deny_status()) + end + -- Error case + return self:ret(false, ret_err) + end + -- Check results of threads + ret_threads = nil + ret_err = nil + local results = {} + while true do + -- Compute threads to wait + local wait_threads = {} + for port, thread in pairs(threads) do + table.insert(wait_threads, thread) + end + -- No port opened + if #wait_threads == 0 then + break + end + -- Wait for first thread + local ok, open, port = ngx.thread.wait(unpack(wait_threads)) + -- Error case + if not ok then + ret_threads = false + ret_err = "error while waiting thread : " .. open + break + end + port = tostring(port) + -- Remove thread from list + threads[port] = nil + -- Add result to cache + local result = "close" + if open then + result = "open" + end + results[port] = result + -- Port is opened + if open then + ret_threads = true + ret_err = "port " .. port .. " is opened for IP " .. ngx.ctx.bw.remote_addr + break + end + end + -- Kill running threads + if #threads > 0 then + local wait_threads = {} + for port, thread in pairs(threads) do + table.insert(wait_threads, thread) + end + utils.kill_all_threads(wait_threads) + end + -- Cache results + for port, result in pairs(results) do + local ok, err = self:add_to_cache(ngx.ctx.bw.remote_addr .. ":" .. port, result) + if not ok then + return self:ret(false, "error while adding element to cache : " .. err) + end + end + if ret_threads ~= nil then + -- Open port case + if ret_threads then + return self:ret(true, ret_err, utils.get_deny_status()) + end + -- Error case + return self:ret(false, ret_err) + end -- No port opened return self:ret(true, "no port open for IP " .. ngx.ctx.bw.remote_addr) end @@ -56,15 +122,15 @@ function reversescan:preread() return self:access() end -function reversescan:scan(ip, port, timeout) +function reversescan.scan(ip, port, timeout) local tcpsock = ngx.socket.tcp() tcpsock:settimeout(timeout) local ok, err = tcpsock:connect(ip, port) tcpsock:close() if not ok then - return "close" + return false, port end - return "open" + return true, port end function reversescan:is_in_cache(ip_port) diff --git a/src/common/core/sessions/plugin.json b/src/common/core/sessions/plugin.json index 7c94a9e4d..a24f7fdfc 100644 --- a/src/common/core/sessions/plugin.json +++ b/src/common/core/sessions/plugin.json @@ -46,9 +46,27 @@ "default": "86400", "help": "Maximum time (in seconds) before a session is destroyed.", "id": "sessions-absolute-timeout", - "label": "SessionS absolute timeout", + "label": "Sessions absolute timeout", "regex": "^\\d+$", "type": "text" + }, + "SESSIONS_CHECK_IP": { + "context": "global", + "default": "yes", + "help": "Destroy session if IP address is different than original one.", + "id": "sessions-check-ip", + "label": "Sessions check IP", + "regex": "^(yes|no)$", + "type": "check" + }, + "SESSIONS_CHECK_USER_AGENT": { + "context": "global", + "default": "yes", + "help": "Destroy session if User-Agent is different than original one.", + "id": "sessions-user-agent", + "label": "Sessions check User-Agent", + "regex": "^(yes|no)$", + "type": "check" } } } diff --git a/src/common/core/sessions/sessions.lua b/src/common/core/sessions/sessions.lua index c042f07fa..adb29c39e 100644 --- a/src/common/core/sessions/sessions.lua +++ b/src/common/core/sessions/sessions.lua @@ -8,6 +8,37 @@ local sessions = class("sessions", plugin) function sessions:initialize() -- Call parent initialize plugin.initialize(self, "sessions") + -- Check if random cookie name and secrets are already generated + local is_random = { + "SESSIONS_SECRET", + "SESSIONS_NAME" + } + self.randoms = {} + for i, var in ipairs(is_random) do + if self.variables[var] == "random" then + local data, err = self.datastore:get("storage_sessions_" .. var) + if data then + self.randoms[var] = data + end + end + end +end + +function sessions:set() + if self.is_loading or self.kind ~= "http" then + return self:ret(true, "set not needed") + end + local checks = { + ["IP"] = ngx.ctx.bw.remote_addr, + ["USER_AGENT"] = ngx.ctx.bw.http_user_agent or "" + } + ngx.ctx.bw.sessions_checks = {} + for check, value in pairs(checks) do + if self.variables["SESSIONS_CHECK_" .. check] == "yes" then + table.insert(ngx.ctx.bw.sessions_checks, {check, value}) + end + end + return self:ret(true, "success") end function sessions:init() @@ -41,10 +72,26 @@ function sessions:init() absolute_timeout = tonumber(self.variables["SESSIONS_ABSOLUTE_TIMEOUT"]) } if self.variables["SESSIONS_SECRET"] == "random" then - config.secret = utils.rand(16) + if self.randoms["SESSIONS_SECRET"] then + config.secret = self.randoms["SESSIONS_SECRET"] + else + config.secret = utils.rand(16) + local ok, err = self.datastore:set("storage_sessions_SESSIONS_SECRET", config.secret) + if not ok then + self.logger:log(ngx.ERR, "error from datastore:set : " .. err) + end + end end if self.variables["SESSIONS_NAME"] == "random" then - config.cookie_name = utils.rand(16) + if self.randoms["SESSIONS_NAME"] then + config.cookie_name = self.randoms["SESSIONS_NAME"] + else + config.cookie_name = utils.rand(16) + local ok, err = self.datastore:set("storage_sessions_SESSIONS_NAME", config.cookie_name) + if not ok then + self.logger:log(ngx.ERR, "error from datastore:set : " .. err) + end + end end if redis_vars["USE_REDIS"] ~= "yes" then config.storage = "cookie" @@ -56,7 +103,7 @@ function sessions:init() send_timeout = tonumber(redis_vars["REDIS_TIMEOUT"]), read_timeout = tonumber(redis_vars["REDIS_TIMEOUT"]), keepalive_timeout = tonumber(redis_vars["REDIS_KEEPALIVE_IDLE"]), - pool = "bw", + pool = "bw-redis", pool_size = tonumber(redis_vars["REDIS_KEEPALIVE_POOL"]), ssl = redis_vars["REDIS_SSL"] == "yes", host = redis_vars["REDIS_HOST"], diff --git a/src/common/core/whitelist/whitelist.lua b/src/common/core/whitelist/whitelist.lua index 644be9801..289817f66 100644 --- a/src/common/core/whitelist/whitelist.lua +++ b/src/common/core/whitelist/whitelist.lua @@ -12,12 +12,6 @@ local whitelist = class("whitelist", plugin) function whitelist:initialize() -- Call parent initialize plugin.initialize(self, "whitelist") - -- Check if redis is enabled - local use_redis, err = utils.get_variable("USE_REDIS", false) - if not use_redis then - self.logger:log(ngx.ERR, err) - end - self.use_redis = use_redis == "yes" -- Decode lists if ngx.get_phase() ~= "init" and self:is_needed() then local lists, err = self.datastore:get("plugin_whitelist_lists") @@ -43,8 +37,6 @@ function whitelist:initialize() end end end - -- Instantiate cachestore - self.cachestore = cachestore:new(self.use_redis) end function whitelist:is_needed() @@ -271,7 +263,6 @@ function whitelist:is_whitelisted_ip() end end if forward_check then - local forward_ok = false local ip_list, err = utils.get_ips(forward_check) if ip_list then for i, ip in ipairs(ip_list) do @@ -293,11 +284,12 @@ function whitelist:is_whitelisted_ip() if ngx.ctx.bw.ip_is_global then local asn, err = utils.get_asn(ngx.ctx.bw.remote_addr) if not asn then - return nil, "ASN " .. err - end - for i, bl_asn in ipairs(self.lists["ASN"]) do - if bl_asn == tostring(asn) then - return true, "ASN " .. bl_asn + self.logger:log(ngx.ERR, "can't get ASN of IP " .. ngx.ctx.bw.remote_addr .. " : " .. err) + else + for i, bl_asn in ipairs(self.lists["ASN"]) do + if bl_asn == tostring(asn) then + return true, "ASN " .. bl_asn + end end end end diff --git a/src/deps/clone.sh b/src/deps/clone.sh index 20d7077f1..cfc2a221d 100755 --- a/src/deps/clone.sh +++ b/src/deps/clone.sh @@ -265,13 +265,13 @@ if [ "$dopatch" = "yes" ] ; then do_and_check_cmd cp deps/misc/lua-pack.Makefile deps/src/lua-pack/Makefile fi -# lua-resty-openssl v0.8.21 +# lua-resty-openssl v0.8.22 echo "ℹ️ Downloading lua-resty-openssl" dopatch="no" if [ ! -d "deps/src/lua-resty-openssl" ] ; then dopatch="yes" fi -git_secure_clone "https://github.com/fffonion/lua-resty-openssl.git" "15bc59b97feb5acf25fbdd9426cf73870cf7c838" +git_secure_clone "https://github.com/fffonion/lua-resty-openssl.git" "484907935e60273d31626ac849b23a2d218173de" if [ "$dopatch" == "yes" ] ; then do_and_check_cmd rm -r deps/src/lua-resty-openssl/t fi @@ -287,6 +287,10 @@ if [ "$dopatch" = "yes" ] ; then do_and_check_cmd patch deps/src/lua-ffi-zlib/lib/ffi-zlib.lua deps/misc/lua-ffi-zlib.patch fi +# lua-resty-signal v0.03 +echo "ℹ️ Downloading lua-resty-signal" +git_secure_clone "https://github.com/openresty/lua-resty-signal.git" "d07163e8cfa673900e66048cd2a1f18523aecf16" + # ModSecurity v3.0.9 echo "ℹ️ Downloading ModSecurity" dopatch="no" diff --git a/src/deps/install.sh b/src/deps/install.sh index 23ddababc..afdf9c822 100755 --- a/src/deps/install.sh +++ b/src/deps/install.sh @@ -154,6 +154,11 @@ do_and_check_cmd cp /tmp/bunkerweb/deps/src/lua-resty-openssl/lib/resty/openssl. echo "ℹ️ Installing lua-ffi-zlib" do_and_check_cmd cp /tmp/bunkerweb/deps/src/lua-ffi-zlib/lib/ffi-zlib.lua /usr/share/bunkerweb/deps/lib/lua +# Installing lua-resty-signal +echo "ℹ️ Installing lua-resty-signal" +CHANGE_DIR="/tmp/bunkerweb/deps/src/lua-resty-signal" do_and_check_cmd make PREFIX=/usr/share/bunkerweb/deps -j $NTASK +CHANGE_DIR="/tmp/bunkerweb/deps/src/lua-resty-signal" do_and_check_cmd make PREFIX=/usr/share/bunkerweb/deps LUA_LIB_DIR=/usr/share/bunkerweb/deps/lib/lua install + # Compile dynamic modules echo "ℹ️ Compiling and installing dynamic modules" CONFARGS="$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p')" diff --git a/src/deps/src/lua-resty-openssl/CHANGELOG.md b/src/deps/src/lua-resty-openssl/CHANGELOG.md index e785b7d68..0aadc4dfe 100644 --- a/src/deps/src/lua-resty-openssl/CHANGELOG.md +++ b/src/deps/src/lua-resty-openssl/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] + +## [0.8.22] - 2023-04-26 +### bug fixes +- **crypto:** use OPENSSL_free in BoringSSL ([#107](https://github.com/fffonion/lua-resty-openssl/issues/107)) [7830212](https://github.com/fffonion/lua-resty-openssl/commit/78302123ac744f2d0b6de1156e459e9ea72b7edb) + + ## [0.8.21] - 2023-03-24 ### features @@ -491,7 +497,8 @@ - **x509:** export pubkey [ede4f81](https://github.com/fffonion/lua-resty-openssl/commit/ede4f817cb0fe092ad6f9ab5d6ecdcde864a9fd8) -[Unreleased]: https://github.com/fffonion/lua-resty-openssl/compare/0.8.21...HEAD +[Unreleased]: https://github.com/fffonion/lua-resty-openssl/compare/0.8.22...HEAD +[0.8.22]: https://github.com/fffonion/lua-resty-openssl/compare/0.8.21...0.8.22 [0.8.21]: https://github.com/fffonion/lua-resty-openssl/compare/0.8.20...0.8.21 [0.8.20]: https://github.com/fffonion/lua-resty-openssl/compare/0.8.19...0.8.20 [0.8.19]: https://github.com/fffonion/lua-resty-openssl/compare/0.8.18...0.8.19 diff --git a/src/deps/src/lua-resty-openssl/Makefile b/src/deps/src/lua-resty-openssl/Makefile index e5e8e8042..39fd2ef28 100644 --- a/src/deps/src/lua-resty-openssl/Makefile +++ b/src/deps/src/lua-resty-openssl/Makefile @@ -11,6 +11,7 @@ all: ; install: all cp -rpv lib/resty/openssl/. $(DESTDIR)$(LUA_LIB_DIR)/resty/openssl + cp -pv lib/resty/openssl.lua $(DESTDIR)$(LUA_LIB_DIR)/resty/ test: all PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r t diff --git a/src/deps/src/lua-resty-openssl/lib/resty/openssl.lua b/src/deps/src/lua-resty-openssl/lib/resty/openssl.lua index a786b4625..d8b153ab8 100644 --- a/src/deps/src/lua-resty-openssl/lib/resty/openssl.lua +++ b/src/deps/src/lua-resty-openssl/lib/resty/openssl.lua @@ -25,7 +25,7 @@ try_require_modules() local _M = { - _VERSION = '0.8.21', + _VERSION = '0.8.22', } local libcrypto_name diff --git a/src/deps/src/lua-resty-openssl/lib/resty/openssl/include/crypto.lua b/src/deps/src/lua-resty-openssl/lib/resty/openssl/include/crypto.lua index 6ca1f0801..29a6a2273 100644 --- a/src/deps/src/lua-resty-openssl/lib/resty/openssl/include/crypto.lua +++ b/src/deps/src/lua-resty-openssl/lib/resty/openssl/include/crypto.lua @@ -3,6 +3,7 @@ local C = ffi.C local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10 local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER +local BORINGSSL = require("resty.openssl.version").BORINGSSL local OPENSSL_free if OPENSSL_10 then @@ -10,6 +11,11 @@ if OPENSSL_10 then void CRYPTO_free(void *ptr); ]] OPENSSL_free = C.CRYPTO_free +elseif BORINGSSL then + ffi.cdef [[ + void OPENSSL_free(void *ptr); + ]] + OPENSSL_free = C.OPENSSL_free elseif OPENSSL_11_OR_LATER then ffi.cdef [[ void CRYPTO_free(void *ptr, const char *file, int line); diff --git a/src/deps/src/lua-resty-openssl/lua-resty-openssl-0.8.21-1.rockspec b/src/deps/src/lua-resty-openssl/lua-resty-openssl-0.8.22-1.rockspec similarity index 99% rename from src/deps/src/lua-resty-openssl/lua-resty-openssl-0.8.21-1.rockspec rename to src/deps/src/lua-resty-openssl/lua-resty-openssl-0.8.22-1.rockspec index 1fca97ebb..68dd7dc40 100644 --- a/src/deps/src/lua-resty-openssl/lua-resty-openssl-0.8.21-1.rockspec +++ b/src/deps/src/lua-resty-openssl/lua-resty-openssl-0.8.22-1.rockspec @@ -1,8 +1,8 @@ package = "lua-resty-openssl" -version = "0.8.21-1" +version = "0.8.22-1" source = { url = "git+https://github.com/fffonion/lua-resty-openssl.git", - tag = "0.8.21" + tag = "0.8.22" } description = { detailed = "FFI-based OpenSSL binding for LuaJIT.", diff --git a/src/deps/src/lua-resty-signal/.gitignore b/src/deps/src/lua-resty-signal/.gitignore new file mode 100644 index 000000000..bea7b6bd4 --- /dev/null +++ b/src/deps/src/lua-resty-signal/.gitignore @@ -0,0 +1,9 @@ +*~ +*.swp +*.swo +t/servroot* +/go +/reindex +/a.lua +*.o +*.so diff --git a/src/deps/src/lua-resty-signal/.travis.yml b/src/deps/src/lua-resty-signal/.travis.yml new file mode 100644 index 000000000..f1a77323a --- /dev/null +++ b/src/deps/src/lua-resty-signal/.travis.yml @@ -0,0 +1,46 @@ +sudo: required +dist: xenial + +os: linux + +language: c + +compiler: + - gcc + - clang + +env: + global: + - JOBS=3 + - NGX_BUILD_JOBS=$JOBS + - LUAJIT_PREFIX=/opt/luajit21 + - LUAJIT_LIB=$LUAJIT_PREFIX/lib + - LUAJIT_INC=$LUAJIT_PREFIX/include/luajit-2.1 + - LD_LIBRARY_PATH=$LUAJIT_LIB:$LD_LIBRARY_PATH + matrix: + - NGINX_VERSION=1.19.3 + +install: + - sudo apt-get install -qq -y cpanminus axel + - sudo cpanm --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1) + - git clone https://github.com/openresty/openresty.git ../openresty + - git clone https://github.com/openresty/nginx-devel-utils.git + - git clone https://github.com/simpl/ngx_devel_kit.git ../ndk-nginx-module + - git clone https://github.com/openresty/lua-nginx-module.git ../lua-nginx-module + - git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core + - git clone https://github.com/openresty/lua-resty-lrucache.git ../lua-resty-lrucache + - git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx + - git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git + +script: + - make + - cd luajit2/ + - make -j$JOBS CCDEBUG=-g Q= PREFIX=$LUAJIT_PREFIX CC=$CC XCFLAGS='-DLUA_USE_APICHECK -DLUA_USE_ASSERT' > build.log 2>&1 || (cat build.log && exit 1) + - sudo make install PREFIX=$LUAJIT_PREFIX > build.log 2>&1 || (cat build.log && exit 1) + - cd .. + - export PATH=$PWD/work/nginx/sbin:$PWD/nginx-devel-utils:$PATH + - export NGX_BUILD_CC=$CC + - ngx-build $NGINX_VERSION --with-http_realip_module --add-module=../ndk-nginx-module --add-module=../lua-nginx-module --with-debug > build.log 2>&1 || (cat build.log && exit 1) + - nginx -V + - ldd `which nginx`|grep -E 'luajit|ssl|pcre' + - prove -r t diff --git a/src/deps/src/lua-resty-signal/Makefile b/src/deps/src/lua-resty-signal/Makefile new file mode 100644 index 000000000..eca63d4f4 --- /dev/null +++ b/src/deps/src/lua-resty-signal/Makefile @@ -0,0 +1,46 @@ +OPENRESTY_PREFIX=/usr/local/openresty + +PREFIX ?= /usr/local +LUA_INCLUDE_DIR ?= $(PREFIX)/include +LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) +INSTALL ?= install + +.PHONY: all test install + +SRC := resty_signal.c +OBJ := $(SRC:.c=.o) + +C_SO_NAME := librestysignal.so + +CFLAGS := -O3 -g -Wall -fpic + +LDFLAGS := -shared +# on Mac OS X, one should set instead: +# LDFLAGS := -bundle -undefined dynamic_lookup + +MY_CFLAGS := $(CFLAGS) +MY_LDFLAGS := $(LDFLAGS) -fvisibility=hidden + +test := t + +.PHONY = all test clean install + +all : $(C_SO_NAME) + +${OBJ} : %.o : %.c + $(CC) $(MY_CFLAGS) -c $< + +${C_SO_NAME} : ${OBJ} + $(CC) $(MY_LDFLAGS) $^ -o $@ + +#export TEST_NGINX_NO_CLEAN=1 + +clean:; rm -f *.o *.so a.out *.d + +install: + $(INSTALL) -d $(DESTDIR)$(LUA_LIB_DIR)/resty + $(INSTALL) lib/resty/*.lua $(DESTDIR)$(LUA_LIB_DIR)/resty + $(INSTALL) $(C_SO_NAME) $(DESTDIR)$(LUA_LIB_DIR)/ + +test : all + PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r $(test) diff --git a/src/deps/src/lua-resty-signal/README.md b/src/deps/src/lua-resty-signal/README.md new file mode 100644 index 000000000..93f89a856 --- /dev/null +++ b/src/deps/src/lua-resty-signal/README.md @@ -0,0 +1,132 @@ +Name +==== + +lua-resty-signal - Lua library for killing or sending signals to Linux processes + +Table of Contents +================= + +* [Name](#name) +* [Synopsis](#synopsis) +* [Functions](#functions) + * [kill](#kill) + * [signum](#signum) +* [Author](#author) +* [Copyright & Licenses](#copyright--licenses) + +Synopsis +======== + +```lua +local resty_signal = require "resty.signal" +local pid = 12345 + +local ok, err = resty_signal.kill(pid, "TERM") +if not ok then + ngx.log(ngx.ERR, "failed to kill process of pid ", pid, ": ", err) + return +end + +-- send the signal 0 to check the existence of a process +local ok, err = resty_signal.kill(pid, "NONE") + +local ok, err = resty_signal.kill(pid, "HUP") + +local ok, err = resty_signal.kill(pid, "KILL") +``` + +Functions +========= + +kill +---- + +**syntax:** `ok, err = resty_signal.kill(pid, signal_name_or_num)` + +Sends a signal with its name string or number value to the process of the +specified pid. + +All signal names accepted by [signum](#signum) are supported, like `HUP`, +`KILL`, and `TERM`. + +Signal numbers are also supported when specifying nonportable system-specific +signals is desired. + +[Back to TOC](#table-of-contents) + +signum +------ + +**syntax:** `num = resty_signal.signum(sig_name)` + +Maps the signal name specified to the system-specific signal number. Returns +`nil` if the signal name is not known. + +All the POSIX and BSD signal names are supported: + +``` +HUP +INT +QUIT +ILL +TRAP +ABRT +BUS +FPE +KILL +USR1 +SEGV +USR2 +PIPE +ALRM +TERM +CHLD +CONT +STOP +TSTP +TTIN +TTOU +URG +XCPU +XFSZ +VTALRM +PROF +WINCH +IO +PWR +EMT +SYS +INFO +``` + +The special signal name `NONE` is also supported, which is mapped to zero (0). + +[Back to TOC](#table-of-contents) + +Author +====== + +Yichun Zhang (agentzh) + +[Back to TOC](#table-of-contents) + +Copyright & Licenses +==================== + +This module is licensed under the BSD license. + +Copyright (C) 2018-2019, [OpenResty Inc.](https://openresty.com) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +[Back to TOC](#table-of-contents) diff --git a/src/deps/src/lua-resty-signal/lib/resty/signal.lua b/src/deps/src/lua-resty-signal/lib/resty/signal.lua new file mode 100644 index 000000000..ad11596c1 --- /dev/null +++ b/src/deps/src/lua-resty-signal/lib/resty/signal.lua @@ -0,0 +1,156 @@ +local _M = { + version = 0.03 +} + + +local ffi = require "ffi" +local base = require "resty.core.base" + + +local C = ffi.C +local ffi_str = ffi.string +local tonumber = tonumber +local assert = assert +local errno = ffi.errno +local type = type +local new_tab = base.new_tab +local error = error +local string_format = string.format + + +local load_shared_lib +do + local string_gmatch = string.gmatch + local string_match = string.match + local io_open = io.open + local io_close = io.close + + local cpath = package.cpath + + function load_shared_lib(so_name) + local tried_paths = new_tab(32, 0) + local i = 1 + + for k, _ in string_gmatch(cpath, "[^;]+") do + local fpath = string_match(k, "(.*/)") + fpath = fpath .. so_name + -- Don't get me wrong, the only way to know if a file exist is + -- trying to open it. + local f = io_open(fpath) + if f ~= nil then + io_close(f) + return ffi.load(fpath) + end + + tried_paths[i] = fpath + i = i + 1 + end + + return nil, tried_paths + end -- function +end -- do + + +local resty_signal, tried_paths = load_shared_lib("librestysignal.so") +if not resty_signal then + error("could not load librestysignal.so from the following paths:\n" .. + table.concat(tried_paths, "\n"), 2) +end + + +ffi.cdef[[ +int resty_signal_signum(int num); +]] + + +if not pcall(function () return C.kill end) then + ffi.cdef("int kill(int32_t pid, int sig);") +end + + +if not pcall(function () return C.strerror end) then + ffi.cdef("char *strerror(int errnum);") +end + + +-- Below is just the ID numbers for each POSIX signal. We map these signal IDs +-- to system-specific signal numbers on the C land (via librestysignal.so). +local signals = { + NONE = 0, + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + BUS = 7, + FPE = 8, + KILL = 9, + USR1 = 10, + SEGV = 11, + USR2 = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + CHLD = 17, + CONT = 18, + STOP = 19, + TSTP = 20, + TTIN = 21, + TTOU = 22, + URG = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + IO = 29, + PWR = 30, + EMT = 31, + SYS = 32, + INFO = 33 +} + + +local function signum(name) + local sig_num + if type(name) == "number" then + sig_num = name + else + local id = signals[name] + if not id then + return nil, "unknown signal name" + end + + sig_num = tonumber(resty_signal.resty_signal_signum(id)) + if sig_num < 0 then + error( + string_format("missing C def for signal %s = %d", name, id), + 2 + ) + end + end + return sig_num +end + + +function _M.kill(pid, sig) + assert(sig) + + local sig_num, err = signum(sig) + if err then + return nil, err + end + + local rc = tonumber(C.kill(assert(pid), sig_num)) + if rc == 0 then + return true + end + + local err = ffi_str(C.strerror(errno())) + return nil, err +end + +_M.signum = signum + +return _M diff --git a/src/deps/src/lua-resty-signal/resty_signal.c b/src/deps/src/lua-resty-signal/resty_signal.c new file mode 100644 index 000000000..78960c84d --- /dev/null +++ b/src/deps/src/lua-resty-signal/resty_signal.c @@ -0,0 +1,152 @@ +#include + + +enum { + RS_NONE = 0, + RS_HUP = 1, + RS_INT = 2, + RS_QUIT = 3, + RS_ILL = 4, + RS_TRAP = 5, + RS_ABRT = 6, + RS_BUS = 7, + RS_FPE = 8, + RS_KILL = 9, + RS_USR1 = 10, + RS_SEGV = 11, + RS_USR2 = 12, + RS_PIPE = 13, + RS_ALRM = 14, + RS_TERM = 15, + RS_CHLD = 17, + RS_CONT = 18, + RS_STOP = 19, + RS_TSTP = 20, + RS_TTIN = 21, + RS_TTOU = 22, + RS_URG = 23, + RS_XCPU = 24, + RS_XFSZ = 25, + RS_VTALRM = 26, + RS_PROF = 27, + RS_WINCH = 28, + RS_IO = 29, + RS_PWR = 30, + RS_EMT = 31, + RS_SYS = 32, + RS_INFO = 33 +}; + + +int +resty_signal_signum(int num) +{ + switch (num) { + + case RS_NONE: + return 0; + + case RS_HUP: + return SIGHUP; + + case RS_INT: + return SIGINT; + + case RS_QUIT: + return SIGQUIT; + + case RS_ILL: + return SIGILL; + + case RS_TRAP: + return SIGTRAP; + + case RS_ABRT: + return SIGABRT; + + case RS_BUS: + return SIGBUS; + + case RS_FPE: + return SIGFPE; + + case RS_KILL: + return SIGKILL; + + case RS_SEGV: + return SIGSEGV; + + case RS_PIPE: + return SIGPIPE; + + case RS_ALRM: + return SIGALRM; + + case RS_TERM: + return SIGTERM; + + case RS_CHLD: + return SIGCHLD; + + case RS_CONT: + return SIGCONT; + + case RS_STOP: + return SIGSTOP; + + case RS_TSTP: + return SIGTSTP; + + case RS_TTIN: + return SIGTTIN; + + case RS_TTOU: + return SIGTTOU; + + case RS_XCPU: + return SIGXCPU; + + case RS_XFSZ: + return SIGXFSZ; + + case RS_VTALRM: + return SIGVTALRM; + + case RS_PROF: + return SIGPROF; + + case RS_WINCH: + return SIGWINCH; + + case RS_IO: + return SIGIO; + +#ifdef __linux__ + case RS_PWR: + return SIGPWR; +#endif + + case RS_USR1: + return SIGUSR1; + + case RS_USR2: + return SIGUSR2; + + case RS_URG: + return SIGURG; + +#ifdef __APPLE__ + case RS_EMT: + return SIGEMT; + + case RS_SYS: + return SIGSYS; + + case RS_INFO: + return SIGINFO; +#endif + + default: + return -1; + } +} diff --git a/src/deps/src/lua-resty-signal/t/TestKiller.pm b/src/deps/src/lua-resty-signal/t/TestKiller.pm new file mode 100644 index 000000000..5599944a1 --- /dev/null +++ b/src/deps/src/lua-resty-signal/t/TestKiller.pm @@ -0,0 +1,32 @@ +package t::TestKiller; + +use v5.10.1; +use Test::Nginx::Socket::Lua -Base; + +add_block_preprocessor(sub { + my $block = shift; + + my $http_config = $block->http_config // ''; + my $init_by_lua_block = $block->init_by_lua_block // 'require "resty.core"'; + + $http_config .= <<_EOC_; + + lua_package_path "./lib/?.lua;../lua-resty-core/lib/?.lua;../lua-resty-lrucache/lib/?.lua;;"; + lua_package_cpath "./?.so;;"; + init_by_lua_block { + $init_by_lua_block + } +_EOC_ + + $block->set_value("http_config", $http_config); + + if (!defined $block->error_log) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } +}); + +1; diff --git a/src/deps/src/lua-resty-signal/t/kill.t b/src/deps/src/lua-resty-signal/t/kill.t new file mode 100644 index 000000000..6b0d6171f --- /dev/null +++ b/src/deps/src/lua-resty-signal/t/kill.t @@ -0,0 +1,207 @@ +# vi:ft= + +use lib '.'; +use t::TestKiller; + +plan tests => 3 * blocks(); + +no_long_string(); +#no_diff(); + +run_tests(); + +__DATA__ + +=== TEST 1: returns an error if signal is unknown +--- config + location = /t { + content_by_lua_block { + local resty_signal = require "resty.signal" + + local ok, err = resty_signal.kill(pid, "FOO") + if not ok then + ngx.say("failed to send FOO signal: ", err) + return + end + ngx.say("ok") + } + } +--- response_body +failed to send FOO signal: unknown signal name + + + +=== TEST 2: send NONE to a non-existing process +--- config + location = /t { + content_by_lua_block { + local resty_signal = require "resty.signal" + + local say = ngx.say + local ngx_pipe = require "ngx.pipe" + local proc = assert(ngx_pipe.spawn("echo ok")) + local pid = assert(proc:pid()) + assert(proc:wait()) + + local ok, err = resty_signal.kill(pid, "NONE") + if not ok then + ngx.say("failed to send NONE signal: ", err) + return + end + ngx.say("ok") + } + } +--- response_body +failed to send NONE signal: No such process + + + +=== TEST 3: send TERM to a non-existing process +--- config + location = /t { + content_by_lua_block { + local resty_signal = require "resty.signal" + + local say = ngx.say + local ngx_pipe = require "ngx.pipe" + local proc = assert(ngx_pipe.spawn("echo ok")) + local pid = assert(proc:pid()) + assert(proc:wait()) + + local ok, err = resty_signal.kill(pid, "TERM") + if not ok then + ngx.say("failed to send TERM signal: ", err) + return + end + ngx.say("ok") + } + } +--- response_body +failed to send TERM signal: No such process + + + +=== TEST 4: send NONE to an existing process +--- config + location = /t { + content_by_lua_block { + local resty_signal = require "resty.signal" + + local say = ngx.say + local ngx_pipe = require "ngx.pipe" + local proc = assert(ngx_pipe.spawn("echo ok")) + local pid = assert(proc:pid()) + -- assert(proc:wait()) + + for i = 1, 2 do + ngx.say("i = ", i) + local ok, err = resty_signal.kill(pid, "NONE") + if not ok then + ngx.say("failed to send NONE signal: ", err) + return + end + end + + ngx.say("ok") + } + } +--- response_body +i = 1 +i = 2 +ok + + + +=== TEST 5: send TERM to an existing process +--- config + location = /t { + content_by_lua_block { + local resty_signal = require "resty.signal" + + local say = ngx.say + local ngx_pipe = require "ngx.pipe" + local proc = assert(ngx_pipe.spawn("echo ok")) + local pid = assert(proc:pid()) + -- assert(proc:wait()) + + for i = 1, 2 do + ngx.say("i = ", i) + local ok, err = resty_signal.kill(pid, "TERM") + if not ok then + ngx.say("failed to send TERM signal: ", err) + return + end + ngx.sleep(0.01) + end + + ngx.say("ok") + } + } +--- response_body +i = 1 +i = 2 +failed to send TERM signal: No such process + + + +=== TEST 6: send KILL to an existing process +--- config + location = /t { + content_by_lua_block { + local resty_signal = require "resty.signal" + + local say = ngx.say + local ngx_pipe = require "ngx.pipe" + local proc = assert(ngx_pipe.spawn("echo ok")) + local pid = assert(proc:pid()) + -- assert(proc:wait()) + + for i = 1, 2 do + ngx.say("i = ", i) + local ok, err = resty_signal.kill(pid, "KILL") + if not ok then + ngx.say("failed to send KILL signal: ", err) + return + end + ngx.sleep(0.01) + end + + ngx.say("ok") + } + } +--- response_body +i = 1 +i = 2 +failed to send KILL signal: No such process + + + +=== TEST 7: send TERM signal value, 15, directly to an existing process +--- config + location = /t { + content_by_lua_block { + local resty_signal = require "resty.signal" + + local say = ngx.say + local ngx_pipe = require "ngx.pipe" + local proc = assert(ngx_pipe.spawn("echo ok")) + local pid = assert(proc:pid()) + -- assert(proc:wait()) + + for i = 1, 2 do + ngx.say("i = ", i) + local ok, err = resty_signal.kill(pid, 15) + if not ok then + ngx.say("failed to send TERM signal: ", err) + return + end + ngx.sleep(0.01) + end + + ngx.say("ok") + } + } +--- response_body +i = 1 +i = 2 +failed to send TERM signal: No such process diff --git a/src/deps/src/lua-resty-signal/t/sanity.t b/src/deps/src/lua-resty-signal/t/sanity.t new file mode 100644 index 000000000..07e2cee7a --- /dev/null +++ b/src/deps/src/lua-resty-signal/t/sanity.t @@ -0,0 +1,35 @@ +# vi:ft= + +use lib '.'; +use t::TestKiller; + +plan tests => 3 * blocks(); + +no_long_string(); +#no_diff(); + +run_tests(); + +__DATA__ + +=== TEST 1: failure to load librestysignal.so +--- config + location = /t { + content_by_lua_block { + local cpath = package.cpath + package.cpath = "/foo/?.so;/bar/?.so;" + + local ok, perr = pcall(require, "resty.signal") + if not ok then + ngx.say(perr) + end + + package.cpath = cpath + } + } +--- response_body +could not load librestysignal.so from the following paths: +/foo/librestysignal.so +/bar/librestysignal.so +--- no_error_log +[error] diff --git a/src/deps/src/lua-resty-signal/t/signum.t b/src/deps/src/lua-resty-signal/t/signum.t new file mode 100644 index 000000000..3d7b94b10 --- /dev/null +++ b/src/deps/src/lua-resty-signal/t/signum.t @@ -0,0 +1,116 @@ +# vi:ft= + +use lib '.'; +use t::TestKiller; + +plan tests => 3 * blocks(); + +no_long_string(); +#no_diff(); + +run_tests(); + +__DATA__ + +=== TEST 1: signals whose values are specified by POSIX +--- config + location = /t { + content_by_lua_block { + local resty_signal = require "resty.signal" + local ffi = require "ffi" + local say = ngx.say + local signum = resty_signal.signum + + for i, signame in ipairs{ "ABRT", "ALRM", "HUP", "INT", "KILL", + "QUIT", "TERM", "TRAP", "BLAH" } do + say(signame, ": ", tostring(signum(signame))) + end + + local linux_signals = { + NONE = 0, + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + BUS = 7, + FPE = 8, + KILL = 9, + USR1 = 10, + SEGV = 11, + USR2 = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + CHLD = 17, + CONT = 18, + STOP = 19, + TSTP = 20, + TTIN = 21, + TTOU = 22, + URG = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + IO = 29, + PWR = 30 + } + + local macosx_signals = { + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31 + } + + if ffi.os == "Linux" then + for signame, num in pairs(linux_signals) do + assert(num == tonumber(signum(signame))) + end + elseif ffi.os == "OSX" then + for signame, num in pairs(macosx_signals) do + assert(num == tonumber(signum(signame))) + end + end + } + } +--- response_body +ABRT: 6 +ALRM: 14 +HUP: 1 +INT: 2 +KILL: 9 +QUIT: 3 +TERM: 15 +TRAP: 5 +BLAH: nil diff --git a/src/deps/src/lua-resty-signal/valgrind.suppress b/src/deps/src/lua-resty-signal/valgrind.suppress new file mode 100644 index 000000000..1cac50666 --- /dev/null +++ b/src/deps/src/lua-resty-signal/valgrind.suppress @@ -0,0 +1,30 @@ +{ + + Memcheck:Param + epoll_ctl(event) + fun:epoll_ctl + fun:ngx_epoll_test_rdhup + fun:ngx_epoll_init + fun:ngx_event_process_init + fun:ngx_single_process_cycle + fun:main +} +{ + + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:ngx_alloc + fun:ngx_set_environment + fun:ngx_single_process_cycle + fun:main +} +{ + + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:ngx_alloc + fun:ngx_set_environment + fun:ngx_worker_process_init +} diff --git a/tests/ansible/autoconf_playbook b/tests/ansible/autoconf_playbook index 39dd3ecc5..74d1c6e59 100644 --- a/tests/ansible/autoconf_playbook +++ b/tests/ansible/autoconf_playbook @@ -38,4 +38,4 @@ ansible.builtin.pause: seconds: 60 - name: Restart GH runner - shell: systemctl restart actions.runner.* + shell: chown -R user:user /opt/actions-runner/ && systemctl restart actions.runner.* diff --git a/tests/ansible/docker_playbook b/tests/ansible/docker_playbook index b297bd15c..36c15d4a3 100644 --- a/tests/ansible/docker_playbook +++ b/tests/ansible/docker_playbook @@ -38,4 +38,4 @@ ansible.builtin.pause: seconds: 60 - name: Restart GH runner - shell: systemctl restart actions.runner.* + shell: chown -R user:user /opt/actions-runner/ && systemctl restart actions.runner.* diff --git a/tests/ansible/linux_playbook b/tests/ansible/linux_playbook index e5daa8394..73717f4ea 100644 --- a/tests/ansible/linux_playbook +++ b/tests/ansible/linux_playbook @@ -38,4 +38,4 @@ ansible.builtin.pause: seconds: 60 - name: Restart GH runner - shell: systemctl restart actions.runner.* + shell: chown -R user:user /opt/actions-runner/ && systemctl restart actions.runner.* diff --git a/tests/ansible/swarm_playbook b/tests/ansible/swarm_playbook index 03e86f713..eb55c2a19 100644 --- a/tests/ansible/swarm_playbook +++ b/tests/ansible/swarm_playbook @@ -49,4 +49,4 @@ ansible.builtin.pause: seconds: 60 - name: Restart GH runner - shell: systemctl restart actions.runner.* + shell: chown -R user:user /opt/actions-runner/ && systemctl restart actions.runner.* diff --git a/tests/core/bwcli/Dockerfile b/tests/core/bwcli/Dockerfile new file mode 100644 index 000000000..4a4619dce --- /dev/null +++ b/tests/core/bwcli/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.11.3-alpine + +WORKDIR /tmp + +COPY requirements.txt . + +RUN MAKEFLAGS="-j $(nproc)" pip install --no-cache -r requirements.txt && \ + rm -f requirements.txt + +WORKDIR /opt/tests + +COPY main.py . + +ENTRYPOINT [ "python3", "main.py" ] diff --git a/tests/core/bwcli/docker-compose.test.yml b/tests/core/bwcli/docker-compose.test.yml new file mode 100644 index 000000000..88a7243a8 --- /dev/null +++ b/tests/core/bwcli/docker-compose.test.yml @@ -0,0 +1,7 @@ +version: "3.5" + +services: + tests: + build: . + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro diff --git a/tests/core/bwcli/docker-compose.yml b/tests/core/bwcli/docker-compose.yml new file mode 100644 index 000000000..52fecbd38 --- /dev/null +++ b/tests/core/bwcli/docker-compose.yml @@ -0,0 +1,55 @@ +version: "3.5" + +services: + bw: + image: bunkerity/bunkerweb:1.5.0-beta + pull_policy: never + depends_on: + - bw-redis + labels: + - "bunkerweb.INSTANCE" + environment: + API_WHITELIST_IP: "127.0.0.0/8 10.20.30.0/24" + USE_BUNKERNET: "no" + USE_BLACKLIST: "no" + LOG_LEVEL: "info" + USE_REDIS: "yes" + REDIS_HOST: "bw-redis" + networks: + - bw-universe + + bw-scheduler: + image: bunkerity/bunkerweb-scheduler:1.5.0-beta + pull_policy: never + depends_on: + - bw + - bw-docker + environment: + DOCKER_HOST: "tcp://bw-docker:2375" + LOG_LEVEL: "info" + networks: + - bw-universe + - bw-docker + + bw-docker: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + CONTAINERS: "1" + networks: + - bw-docker + + bw-redis: + image: redis:7-alpine + networks: + - bw-universe + +networks: + bw-universe: + name: bw-universe + ipam: + driver: default + config: + - subnet: 10.20.30.0/24 + bw-docker: diff --git a/tests/core/bwcli/main.py b/tests/core/bwcli/main.py new file mode 100644 index 000000000..87139958c --- /dev/null +++ b/tests/core/bwcli/main.py @@ -0,0 +1,111 @@ +from os import getenv +from traceback import format_exc +from docker import DockerClient +from docker.models.containers import Container + +try: + docker_host = getenv("DOCKER_HOST", "unix:///var/run/docker.sock") + docker_client = DockerClient(base_url=docker_host) + + bw_instances = docker_client.containers.list( + filters={"label": "bunkerweb.INSTANCE"} + ) + + if not bw_instances: + print("❌ BunkerWeb instance not found ...", flush=True) + exit(1) + + bw_instance: Container = bw_instances[0] + + print( + 'ℹ️ Executing the command "bwcli ban 127.0.0.1 -exp 3600" inside the BW container ...', + flush=True, + ) + + result = bw_instance.exec_run("bwcli ban 127.0.0.1 -exp 3600") + + if result.exit_code != 0: + print( + f'❌ Command "ban" failed, exiting ...\noutput: {result.output.decode()}\nexit_code: {result.exit_code}' + ) + exit(1) + + print(result.output.decode(), flush=True) + + print( + 'ℹ️ Executing the command "bwcli bans" inside the BW container and checking the result ...', + flush=True, + ) + + result = bw_instance.exec_run("bwcli bans") + + if result.exit_code != 0: + print( + f'❌ Command "bans" failed, exiting ...\noutput: {result.output.decode()}\nexit_code: {result.exit_code}' + ) + exit(1) + + if b"- 127.0.0.1" not in result.output: + print( + f'❌ IP 127.0.0.1 not found in the output of "bans", exiting ...\noutput: {result.output.decode()}' + ) + exit(1) + elif b"List of bans for redis:" not in result.output: + print( + f'❌ Redis ban list not found in the output of "bans", exiting ...\noutput: {result.output.decode()}' + ) + exit(1) + elif b"1 hour" not in result.output and b"59 minutes" not in result.output: + print( + f"❌ Ban duration isn't 1 hour, exiting ...\noutput: {result.output.decode()}" + ) + exit(1) + + print(result.output.decode(), flush=True) + + print( + 'ℹ️ Executing the command "bwcli unban 127.0.0.1" inside the BW container ...', + flush=True, + ) + + result = bw_instance.exec_run("bwcli unban 127.0.0.1") + + if result.exit_code != 0: + print( + f'❌ Command "unban" failed, exiting ...\noutput: {result.output.decode()}\nexit_code: {result.exit_code}' + ) + exit(1) + + print(result.output.decode(), flush=True) + + print( + 'ℹ️ Executing the command "bwcli bans" inside the BW container to check if the IP was unbanned ...', + flush=True, + ) + + result = bw_instance.exec_run("bwcli bans") + + if result.exit_code != 0: + print( + f'❌ Command "bans" failed, exiting ...\noutput: {result.output.decode()}\nexit_code: {result.exit_code}' + ) + exit(1) + + found = 0 + for line in result.output.splitlines(): + if b"No ban found" in line: + found += 1 + + if found < 2: + print( + f"❌ IP 127.0.0.1 was not unbanned from both redis and the local ban list, exiting ...\noutput: {result.output.decode()}", + flush=True, + ) + exit(1) + + print(result.output.decode(), flush=True) +except SystemExit: + exit(1) +except: + print(f"❌ Something went wrong, exiting ...\n{format_exc()}", flush=True) + exit(1) diff --git a/tests/core/bwcli/requirements.txt b/tests/core/bwcli/requirements.txt new file mode 100644 index 000000000..837e8178b --- /dev/null +++ b/tests/core/bwcli/requirements.txt @@ -0,0 +1 @@ +docker==6.1.2 diff --git a/tests/core/bwcli/test.sh b/tests/core/bwcli/test.sh new file mode 100755 index 000000000..e0b645970 --- /dev/null +++ b/tests/core/bwcli/test.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +echo "⌨️ Building bunkernet stack ..." + +# Starting stack +docker compose pull bw-docker +if [ $? -ne 0 ] ; then + echo "⌨️ Pull failed ❌" + exit 1 +fi +docker compose -f docker-compose.test.yml build +if [ $? -ne 0 ] ; then + echo "⌨️ Build failed ❌" + exit 1 +fi + +cleanup_stack () { + echo "⌨️ Cleaning up current stack ..." + + docker compose down -v --remove-orphans 2>/dev/null + + if [ $? -ne 0 ] ; then + echo "⌨️ Down failed ❌" + exit 1 + fi + + echo "⌨️ Cleaning up current stack done ✅" +} + +# Cleanup stack on exit +trap cleanup_stack EXIT + +echo "⌨️ Running bwcli tests ..." + +echo "⌨️ Starting stack ..." +docker compose up -d 2>/dev/null +if [ $? -ne 0 ] ; then + echo "⌨️ Up failed, retrying ... ⚠️" + manual=1 + cleanup_stack + manual=0 + docker compose up -d 2>/dev/null + if [ $? -ne 0 ] ; then + echo "⌨️ Up failed ❌" + exit 1 + fi +fi + +# Check if stack is healthy +echo "⌨️ Waiting for stack to be healthy ..." +i=0 +while [ $i -lt 120 ] ; do + containers=("bwcli-bw-1" "bwcli-bw-scheduler-1") + healthy="true" + for container in "${containers[@]}" ; do + check="$(docker inspect --format "{{json .State.Health }}" $container | grep "healthy")" + if [ "$check" = "" ] ; then + healthy="false" + break + fi + done + if [ "$healthy" = "true" ] ; then + echo "⌨️ Docker stack is healthy ✅" + break + fi + sleep 1 + i=$((i+1)) +done +if [ $i -ge 120 ] ; then + docker compose logs + echo "⌨️ Docker stack is not healthy ❌" + exit 1 + fi + +# Start tests + +docker compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from tests 2>/dev/null + +if [ $? -ne 0 ] ; then + echo "⌨️ Test bwcli failed ❌" + echo "🛡️ Showing BunkerWeb and BunkerWeb Scheduler logs ..." + docker compose logs bw bw-scheduler + exit 1 +else + echo "⌨️ Test bwcli succeeded ✅" +fi + +echo "⌨️ Tests are done ! ✅" diff --git a/tests/ui/main.py b/tests/ui/main.py index 2551f9882..15d376d1b 100644 --- a/tests/ui/main.py +++ b/tests/ui/main.py @@ -812,7 +812,7 @@ with webdriver.Firefox( assert_alert_message(driver, "was successfully created") - sleep(15) + sleep(30) driver.execute_script("window.open('http://www.example.com/hello','_blank');") driver.switch_to.window(driver.window_handles[1])