├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .luacheckrc ├── .luacov ├── .travis.yml ├── Makefile ├── README.md ├── dist.ini ├── docker └── tests │ └── docker-compose.yml ├── lib ├── ledge.lua └── ledge │ ├── background.lua │ ├── cache_key.lua │ ├── collapse.lua │ ├── esi.lua │ ├── esi │ ├── processor_1_0.lua │ └── tag_parser.lua │ ├── gzip.lua │ ├── handler.lua │ ├── header_util.lua │ ├── jobs │ ├── collect_entity.lua │ ├── purge.lua │ └── revalidate.lua │ ├── purge.lua │ ├── range.lua │ ├── request.lua │ ├── response.lua │ ├── stale.lua │ ├── state_machine.lua │ ├── state_machine │ ├── actions.lua │ ├── events.lua │ ├── pre_transitions.lua │ └── states.lua │ ├── storage │ └── redis.lua │ ├── util.lua │ ├── validation.lua │ └── worker.lua ├── migrations └── 1.26-1.27.lua ├── t ├── 01-unit │ ├── cache_key.t │ ├── esi.t │ ├── events.t │ ├── handler.t │ ├── jobs.t │ ├── ledge.t │ ├── processor_1_0.t │ ├── purge.t │ ├── range.t │ ├── request.t │ ├── response.t │ ├── stale.t │ ├── state_machine.t │ ├── storage.t │ ├── tag_parser.t │ ├── util.t │ ├── validation.t │ └── worker.t ├── 02-integration │ ├── age.t │ ├── cache.t │ ├── collapsed_forwarding.t │ ├── esi.t │ ├── events.t │ ├── gc.t │ ├── gzip.t │ ├── hop_by_hop_headers.t │ ├── max-stale.t │ ├── max_size.t │ ├── memory_pressure.t │ ├── multiple_headers.t │ ├── on_abort.t │ ├── origin_mode.t │ ├── purge.t │ ├── range.t │ ├── req_body.t │ ├── req_method.t │ ├── request_leak.t │ ├── response.t │ ├── ssl.t │ ├── stale-if-error.t │ ├── stale-while-revalidate.t │ ├── upstream.t │ ├── upstream_client.t │ ├── validation.t │ ├── vary.t │ └── via_header.t ├── 03-sentinel │ ├── 01-master_up.t │ ├── 02-master_down.t │ └── 03-slave_promoted.t ├── LedgeEnv.pm └── cert │ ├── example.com.crt │ ├── example.com.key │ ├── rootCA.key │ ├── rootCA.pem │ └── rootCA.srl └── util └── lua-releng /.gitattributes: -------------------------------------------------------------------------------- 1 | *.t linguist-language=lua 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pintsized 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot/ 2 | t/error.log 3 | dump.rdb 4 | stdout 5 | luacov.* 6 | *.src.rock 7 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | redefined = false 3 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | modules = { 2 | ["ledge"] = "lib/ledge.lua", 3 | ["ledge.esi.*"] = "lib/", 4 | ["ledge.jobs.*"] = "lib/", 5 | ["ledge.state_machine.*"] = "lib/", 6 | ["ledge.storage.*"] = "lib/", 7 | ["ledge.*"] = "lib/" 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - docker 3 | 4 | script: 5 | - cd docker/tests 6 | - docker-compose run --rm runner 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash # Cheat by using bash :) 2 | 3 | OPENRESTY_PREFIX = /usr/local/openresty 4 | 5 | TEST_FILE ?= t/01-unit t/02-integration 6 | SENTINEL_TEST_FILE ?= t/03-sentinel 7 | 8 | TEST_LEDGE_REDIS_HOST ?= 127.0.0.1 9 | TEST_LEDGE_REDIS_PORT ?= 6379 10 | TEST_LEDGE_REDIS_DATABASE ?= 2 11 | TEST_LEDGE_REDIS_QLESS_DATABASE ?= 3 12 | 13 | TEST_NGINX_HOST ?= 127.0.0.1 14 | 15 | # Command line arguments for ledge tests 16 | TEST_LEDGE_REDIS_VARS = PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$(PATH) \ 17 | TEST_LEDGE_REDIS_HOST=$(TEST_LEDGE_REDIS_HOST) \ 18 | TEST_LEDGE_REDIS_PORT=$(TEST_LEDGE_REDIS_PORT) \ 19 | TEST_LEDGE_REDIS_SOCKET=unix://$(TEST_LEDGE_REDIS_SOCKET) \ 20 | TEST_LEDGE_REDIS_DATABASE=$(TEST_LEDGE_REDIS_DATABASE) \ 21 | TEST_LEDGE_REDIS_QLESS_DATABASE=$(TEST_LEDGE_REDIS_QLESS_DATABASE) \ 22 | TEST_NGINX_HOST=$(TEST_NGINX_HOST) \ 23 | TEST_NGINX_NO_SHUFFLE=1 24 | 25 | REDIS_CLI := redis-cli -h $(TEST_LEDGE_REDIS_HOST) -p $(TEST_LEDGE_REDIS_PORT) 26 | 27 | ############################################################################### 28 | # Deprecated, ues docker copose to run Redis instead 29 | ############################################################################### 30 | REDIS_CMD = redis-server 31 | SENTINEL_CMD = $(REDIS_CMD) --sentinel 32 | 33 | REDIS_SOCK = /redis.sock 34 | REDIS_PID = /redis.pid 35 | REDIS_LOG = /redis.log 36 | REDIS_PREFIX = /tmp/redis- 37 | 38 | # Overrideable ledge test variables 39 | TEST_LEDGE_REDIS_PORTS ?= 6379 6380 40 | 41 | REDIS_FIRST_PORT := $(firstword $(TEST_LEDGE_REDIS_PORTS)) 42 | REDIS_SLAVE_ARG := --slaveof 127.0.0.1 $(REDIS_FIRST_PORT) 43 | 44 | # Override ledge socket for running make test on its' own 45 | # (make test TEST_LEDGE_REDIS_SOCKET=/path/to/sock.sock) 46 | TEST_LEDGE_REDIS_SOCKET ?= $(REDIS_PREFIX)$(REDIS_FIRST_PORT)$(REDIS_SOCK) 47 | 48 | # Overrideable ledge + sentinel test variables 49 | TEST_LEDGE_SENTINEL_PORTS ?= 26379 26380 26381 50 | TEST_LEDGE_SENTINEL_MASTER_NAME ?= mymaster 51 | TEST_LEDGE_SENTINEL_PROMOTION_TIME ?= 20 52 | 53 | # Command line arguments for ledge + sentinel tests 54 | TEST_LEDGE_SENTINEL_VARS = PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$(PATH) \ 55 | TEST_LEDGE_SENTINEL_PORT=$(firstword $(TEST_LEDGE_SENTINEL_PORTS)) \ 56 | TEST_LEDGE_SENTINEL_MASTER_NAME=$(TEST_LEDGE_SENTINEL_MASTER_NAME) \ 57 | TEST_LEDGE_REDIS_DATABASE=$(TEST_LEDGE_REDIS_DATABASE) \ 58 | TEST_NGINX_NO_SHUFFLE=1 59 | 60 | # Sentinel configuration can only be set by a config file 61 | define TEST_LEDGE_SENTINEL_CONFIG 62 | sentinel monitor $(TEST_LEDGE_SENTINEL_MASTER_NAME) 127.0.0.1 $(REDIS_FIRST_PORT) 2 63 | sentinel down-after-milliseconds $(TEST_LEDGE_SENTINEL_MASTER_NAME) 2000 64 | sentinel failover-timeout $(TEST_LEDGE_SENTINEL_MASTER_NAME) 10000 65 | sentinel parallel-syncs $(TEST_LEDGE_SENTINEL_MASTER_NAME) 5 66 | endef 67 | 68 | export TEST_LEDGE_SENTINEL_CONFIG 69 | 70 | SENTINEL_CONFIG_PREFIX = /tmp/sentinel 71 | 72 | 73 | 74 | ############################################################################### 75 | 76 | 77 | PREFIX ?= /usr/local 78 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 79 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 80 | PROVE ?= prove -I ../test-nginx/lib 81 | INSTALL ?= install 82 | 83 | .PHONY: all install test test_all start_redis_instances stop_redis_instances \ 84 | start_redis_instance stop_redis_instance cleanup_redis_instance flush_db \ 85 | check_ports test_ledge test_sentinel coverage delete_sentinel_config check 86 | 87 | all: ; 88 | 89 | install: all 90 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/ledge 91 | $(INSTALL) lib/ledge/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/ledge 92 | 93 | test: test_ledge 94 | test_all: start_redis_instances test_ledge test_sentinel stop_redis_instances 95 | 96 | 97 | ############################################################################### 98 | # Deprecated, ues docker copose to run Redis instead 99 | ############################################################################## 100 | start_redis_instances: check_ports 101 | @$(foreach port,$(TEST_LEDGE_REDIS_PORTS), \ 102 | [[ "$(port)" != "$(REDIS_FIRST_PORT)" ]] && \ 103 | SLAVE="$(REDIS_SLAVE_ARG)" || \ 104 | SLAVE="" && \ 105 | $(MAKE) start_redis_instance args="$$SLAVE" port=$(port) \ 106 | prefix=$(REDIS_PREFIX)$(port) && \ 107 | ) true 108 | 109 | @$(foreach port,$(TEST_LEDGE_SENTINEL_PORTS), \ 110 | echo "port $(port)" > "$(SENTINEL_CONFIG_PREFIX)-$(port).conf"; \ 111 | echo "$$TEST_LEDGE_SENTINEL_CONFIG" >> "$(SENTINEL_CONFIG_PREFIX)-$(port).conf"; \ 112 | $(MAKE) start_redis_instance \ 113 | port=$(port) args="$(SENTINEL_CONFIG_PREFIX)-$(port).conf --sentinel" \ 114 | prefix=$(REDIS_PREFIX)$(port) && \ 115 | ) true 116 | 117 | stop_redis_instances: delete_sentinel_config 118 | -@$(foreach port,$(TEST_LEDGE_REDIS_PORTS) $(TEST_LEDGE_SENTINEL_PORTS), \ 119 | $(MAKE) stop_redis_instance cleanup_redis_instance port=$(port) \ 120 | prefix=$(REDIS_PREFIX)$(port) && \ 121 | ) true 2>&1 > /dev/null 122 | 123 | start_redis_instance: 124 | -@echo "Starting redis on port $(port) with args: \"$(args)\"" 125 | -@mkdir -p $(prefix) 126 | @$(REDIS_CMD) $(args) \ 127 | --pidfile $(prefix)$(REDIS_PID) \ 128 | --bind 127.0.0.1 --port $(port) \ 129 | --unixsocket $(prefix)$(REDIS_SOCK) \ 130 | --unixsocketperm 777 \ 131 | --dir $(prefix) \ 132 | --logfile $(prefix)$(REDIS_LOG) \ 133 | --loglevel debug \ 134 | --daemonize yes 135 | 136 | stop_redis_instance: 137 | -@echo "Stopping redis on port $(port)" 138 | -@[[ -f "$(prefix)$(REDIS_PID)" ]] && kill -QUIT \ 139 | `cat $(prefix)$(REDIS_PID)` 2>&1 > /dev/null || true 140 | 141 | cleanup_redis_instance: stop_redis_instance 142 | -@echo "Cleaning up redis files in $(prefix)" 143 | -@rm -rf $(prefix) 144 | 145 | delete_sentinel_config: 146 | -@echo "Cleaning up sentinel config files" 147 | -@rm -f $(SENTINEL_CONFIG_PREFIX)-*.conf 148 | 149 | check_ports: 150 | -@echo "Checking ports $(REDIS_PORTS)" 151 | @$(foreach port,$(REDIS_PORTS),! lsof -i :$(port) &&) true 2>&1 > /dev/null 152 | ############################################################################### 153 | 154 | releng: 155 | @util/lua-releng -eL 156 | 157 | flush_db: 158 | @$(REDIS_CLI) flushall 159 | 160 | test_ledge: releng flush_db 161 | @$(TEST_LEDGE_REDIS_VARS) $(PROVE) $(TEST_FILE) 162 | -@echo "Qless errors:" 163 | @$(REDIS_CLI) -n $(TEST_LEDGE_REDIS_QLESS_DATABASE) llen ql:f:job-error 164 | 165 | test_sentinel: releng flush_db 166 | $(TEST_LEDGE_SENTINEL_VARS) $(PROVE) $(SENTINEL_TEST_FILE)/01-master_up.t 167 | $(REDIS_CLI) shutdown 168 | $(TEST_LEDGE_SENTINEL_VARS) $(PROVE) $(SENTINEL_TEST_FILE)/02-master_down.t 169 | sleep $(TEST_LEDGE_SENTINEL_PROMOTION_TIME) 170 | $(TEST_LEDGE_SENTINEL_VARS) $(PROVE) $(SENTINEL_TEST_FILE)/03-slave_promoted.t 171 | 172 | test_leak: releng flush_db 173 | $(TEST_LEDGE_REDIS_VARS) TEST_NGINX_CHECK_LEAK=1 $(PROVE) $(TEST_FILE) 174 | 175 | coverage: releng flush_db 176 | @rm -f luacov.stats.out 177 | @$(TEST_LEDGE_REDIS_VARS) TEST_COVERAGE=1 $(PROVE) $(TEST_FILE) 178 | @luacov 179 | @tail -30 luacov.report.out 180 | -@echo "Qless errors:" 181 | @$(REDIS_CLI) -n $(TEST_LEDGE_REDIS_QLESS_DATABASE) llen ql:f:job-error 182 | 183 | check: 184 | luacheck lib 185 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=ledge 2 | abstract=An RFC compliant and ESI capable HTTP cache for Nginx / OpenResty, backed by Redis 3 | author=James Hurst, Hamish Forbes 4 | is_original=yes 5 | license=2bsd 6 | lib_dir=lib 7 | repo_link=https://github.com/pintsized/ledge 8 | main_module=lib/ledge.lua 9 | requires = pintsized/lua-resty-http >= 0.11, pintsized/lua-resty-redis-connector >= 0.06, pintsized/lua-resty-qless >= 0.11, p0pr0ck5/lua-resty-cookie >= 0.01, hamishforbes/lua-ffi-zlib >= 0.3.0 10 | -------------------------------------------------------------------------------- /docker/tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | runner: 5 | image: "ledgetech/test-runner:latest" 6 | volumes: 7 | - ../../:/code 8 | 9 | # Use this to mount any local Lua dependencies, overriding 10 | # published versions 11 | - ${EXTLIB-../../lib}:/code/extlib 12 | environment: 13 | - TEST_FILE 14 | command: /bin/bash -c "TEST_LEDGE_REDIS_HOST=redis make coverage" 15 | working_dir: /code 16 | depends_on: 17 | - redis 18 | 19 | redis: 20 | image: "redis:alpine" 21 | -------------------------------------------------------------------------------- /lib/ledge.lua: -------------------------------------------------------------------------------- 1 | local setmetatable, require = 2 | setmetatable, require 3 | 4 | local ngx_get_phase = ngx.get_phase 5 | local ngx_null = ngx.null 6 | 7 | local tbl_insert = table.insert 8 | 9 | local util = require("ledge.util") 10 | local tbl_copy = util.table.copy 11 | local tbl_copy_merge_defaults = util.table.copy_merge_defaults 12 | local fixed_field_metatable = util.mt.fixed_field_metatable 13 | 14 | local redis_connector = require("resty.redis.connector") 15 | 16 | 17 | local _M = { 18 | _VERSION = "2.3.0", 19 | 20 | ORIGIN_MODE_BYPASS = 1, -- Never go to the origin, serve from cache or 503 21 | ORIGIN_MODE_AVOID = 2, -- Avoid the origin, serve from cache where possible 22 | ORIGIN_MODE_NORMAL = 4, -- Assume the origin is happy, use at will 23 | } 24 | 25 | 26 | local config = setmetatable({ 27 | redis_connector_params = { 28 | connect_timeout = 500, -- (ms) 29 | read_timeout = 5000, -- (ms) 30 | keepalive_timeout = 60000, -- (ms) 31 | keepalive_poolsize = 30, 32 | }, 33 | 34 | qless_db = 1, 35 | }, fixed_field_metatable) 36 | 37 | 38 | local function configure(user_config) 39 | assert(ngx_get_phase() == "init", 40 | "attempt to call configure outside the 'init' phase") 41 | 42 | config = setmetatable( 43 | tbl_copy_merge_defaults(user_config, config), 44 | fixed_field_metatable 45 | ) 46 | end 47 | _M.configure = configure 48 | 49 | 50 | local handler_defaults = setmetatable({ 51 | storage_driver = "ledge.storage.redis", 52 | storage_driver_config = {}, 53 | 54 | origin_mode = _M.ORIGIN_MODE_NORMAL, 55 | 56 | -- Note that upstream timeout and keepalive config is shared with outbound 57 | -- ESI request, which are not necessarily configured to use this "upstream" 58 | upstream_connect_timeout = 1000, -- (ms) 59 | upstream_send_timeout = 2000, -- (ms) 60 | upstream_read_timeout = 10000, -- (ms) 61 | upstream_keepalive_timeout = 75000, -- (ms) 62 | upstream_keepalive_poolsize = 64, 63 | 64 | upstream_host = "", 65 | upstream_port = 80, 66 | upstream_use_ssl = false, 67 | upstream_ssl_server_name = "", 68 | upstream_ssl_verify = true, 69 | 70 | advertise_ledge = true, 71 | visible_hostname = util.get_hostname(), 72 | 73 | buffer_size = 2^16, 74 | keep_cache_for = 86400 * 30, -- (sec) 75 | minimum_old_entity_download_rate = 56, 76 | 77 | esi_enabled = false, 78 | esi_content_types = { "text/html" }, 79 | esi_allow_surrogate_delegation = false, 80 | esi_recursion_limit = 10, 81 | esi_args_prefix = "esi_", 82 | esi_max_size = 1024 * 1024, -- (bytes) 83 | esi_custom_variables = {}, 84 | esi_attempt_loopback = true, 85 | esi_vars_cookie_blacklist = {}, 86 | 87 | esi_disable_third_party_includes = false, 88 | esi_third_party_includes_domain_whitelist = {}, 89 | 90 | enable_collapsed_forwarding = false, 91 | collapsed_forwarding_window = 60 * 1000, 92 | 93 | gunzip_enabled = true, 94 | keyspace_scan_count = 10, 95 | 96 | cache_key_spec = {}, -- No default as we don't ever wish to merge it 97 | max_uri_args = 100, 98 | 99 | }, fixed_field_metatable) 100 | 101 | 102 | -- events are not fixed field to avoid runtime fatal errors from bad config 103 | -- ledge.bind() and handler:bind() both check validity of event names however. 104 | local event_defaults = { 105 | after_cache_read = {}, 106 | before_upstream_connect = {}, 107 | before_upstream_request = {}, 108 | after_upstream_request = {}, 109 | before_vary_selection = {}, 110 | before_save = {}, 111 | before_save_revalidation_data = {}, 112 | before_serve = {}, 113 | before_esi_include_request = {}, 114 | } 115 | 116 | 117 | local function set_handler_defaults(user_config) 118 | assert(ngx_get_phase() == "init", 119 | "attempt to call set_handler_defaults outside the 'init' phase") 120 | 121 | handler_defaults = setmetatable( 122 | tbl_copy_merge_defaults(user_config, handler_defaults), 123 | fixed_field_metatable 124 | ) 125 | end 126 | _M.set_handler_defaults = set_handler_defaults 127 | 128 | 129 | local function bind(event, callback) 130 | assert(ngx_get_phase() == "init", 131 | "attempt to call bind outside the 'init' phase") 132 | 133 | local ev = event_defaults[event] 134 | assert(ev, "no such event: " .. tostring(event)) 135 | 136 | tbl_insert(ev, callback) 137 | return true 138 | end 139 | _M.bind = bind 140 | 141 | 142 | local function create_worker(config) 143 | return require("ledge.worker").new(config) 144 | end 145 | _M.create_worker = create_worker 146 | 147 | 148 | local function create_handler(config) 149 | local config = tbl_copy_merge_defaults(config, handler_defaults) 150 | return require("ledge.handler").new(config, tbl_copy(event_defaults)) 151 | end 152 | _M.create_handler = create_handler 153 | 154 | 155 | local function create_redis_connection() 156 | local rc, err = redis_connector.new(config.redis_connector_params) 157 | if not rc then 158 | return nil, err 159 | end 160 | 161 | return rc:connect() 162 | end 163 | _M.create_redis_connection = create_redis_connection 164 | 165 | 166 | local function create_redis_slave_connection() 167 | local params = tbl_copy_merge_defaults( 168 | { role = "slave" }, 169 | config.redis_connector_params 170 | ) 171 | 172 | local rc, err = redis_connector.new(params) 173 | if not rc then 174 | return nil, err 175 | end 176 | 177 | return rc:connect() 178 | end 179 | _M.create_redis_slave_connection = create_redis_slave_connection 180 | 181 | 182 | local function close_redis_connection(redis) 183 | if not next(redis) then 184 | -- Possible for this to be called before we've created a redis conn 185 | -- Ensure we actually have a resty-redis instance to close 186 | return nil, "No redis connection to close" 187 | end 188 | 189 | local rc, err = redis_connector.new(config.redis_connector_params) 190 | if not rc then 191 | return nil, err 192 | end 193 | 194 | return rc:set_keepalive(redis) 195 | end 196 | _M.close_redis_connection = close_redis_connection 197 | 198 | 199 | local function create_qless_connection() 200 | local redis, err = create_redis_connection() 201 | if not redis then return nil, err end 202 | 203 | local ok, err = redis:select(config.qless_db) 204 | if not ok or ok == ngx_null then return nil, err end 205 | 206 | return redis 207 | end 208 | _M.create_qless_connection = create_qless_connection 209 | 210 | 211 | local function create_storage_connection(driver_module, storage_driver_config) 212 | -- Take config by value, and merge with defaults 213 | storage_driver_config = tbl_copy_merge_defaults( 214 | storage_driver_config or {}, 215 | handler_defaults.storage_driver_config 216 | ) 217 | 218 | if not driver_module then 219 | driver_module = handler_defaults.storage_driver 220 | end 221 | 222 | local ok, module = pcall(require, driver_module) 223 | if not ok then return nil, module end 224 | 225 | local ok, driver, err = pcall(module.new) 226 | if not ok then return nil, driver end 227 | if not driver then return nil, err end 228 | 229 | local ok, conn, err = pcall(driver.connect, driver, storage_driver_config) 230 | if not ok then return nil, conn end 231 | if not conn then return nil, err end 232 | 233 | return conn, nil 234 | end 235 | _M.create_storage_connection = create_storage_connection 236 | 237 | 238 | local function close_storage_connection(storage) 239 | return storage:close() 240 | end 241 | _M.close_storage_connection = close_storage_connection 242 | 243 | 244 | return setmetatable(_M, fixed_field_metatable) 245 | -------------------------------------------------------------------------------- /lib/ledge/background.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | local math_ceil = math.ceil 3 | local qless = require("resty.qless") 4 | 5 | local _M = { 6 | _VERSION = "2.3.0", 7 | } 8 | 9 | local function put_background_job( queue, klass, data, options) 10 | local q = qless.new({ 11 | get_redis_client = require("ledge").create_qless_connection 12 | }) 13 | 14 | -- If we've been specified a jid (i.e. a non random jid), putting this 15 | -- job will overwrite any existing job with the same jid. 16 | -- We test for a "running" state, and if so we silently drop this job. 17 | if options.jid then 18 | local existing = q.jobs:get(options.jid) 19 | 20 | if existing and existing.state == "running" then 21 | return nil, "Job with the same jid is currently running" 22 | end 23 | end 24 | 25 | -- Put the job 26 | local res, err = q.queues[queue]:put(klass, data, options) 27 | 28 | q:redis_close() 29 | 30 | if res then 31 | return { 32 | jid = res, 33 | klass = klass, 34 | options = options, 35 | } 36 | else 37 | return res, err 38 | end 39 | end 40 | _M.put_background_job = put_background_job 41 | 42 | 43 | -- Calculate when to GC an entity based on its size and the minimum download 44 | -- rate setting, plus 1 second of arbitrary latency for good measure. 45 | local function gc_wait(entity_size, minimum_download_rate) 46 | local dl_rate_Bps = minimum_download_rate * 128 47 | return math_ceil((entity_size / dl_rate_Bps)) + 1 48 | end 49 | _M.gc_wait = gc_wait 50 | 51 | 52 | return _M 53 | -------------------------------------------------------------------------------- /lib/ledge/collapse.lua: -------------------------------------------------------------------------------- 1 | local _M = { 2 | _VERSION = "2.3.0", 3 | } 4 | 5 | -- Attempts to set a lock key in redis. The lock will expire after 6 | -- the expiry value if it is not cleared (i.e. in case of errors). 7 | -- Returns true if the lock was acquired, false if the lock already 8 | -- exists, and nil, err in case of failure. 9 | local function acquire_lock(redis, lock_key, timeout) 10 | -- We use a Lua script to emulate SETNEX (set if not exists with expiry). 11 | -- This avoids a race window between the GET / SETEX. 12 | -- Params: key, expiry 13 | -- Return: OK or BUSY 14 | local SETNEX = [[ 15 | local lock = redis.call("GET", KEYS[1]) 16 | if not lock then 17 | return redis.call("PSETEX", KEYS[1], ARGV[1], "locked") 18 | else 19 | return "BUSY" 20 | end 21 | ]] 22 | 23 | local res, err = redis:eval(SETNEX, 1, lock_key, timeout) 24 | 25 | if not res then -- Lua script failed 26 | return nil, err 27 | elseif res == "OK" then -- We have the lock 28 | return true 29 | elseif res == "BUSY" then -- Lock is busy 30 | return false 31 | end 32 | end 33 | _M.acquire_lock = acquire_lock 34 | 35 | return _M 36 | -------------------------------------------------------------------------------- /lib/ledge/esi.lua: -------------------------------------------------------------------------------- 1 | local h_util = require "ledge.header_util" 2 | 3 | local type, tonumber = type, tonumber 4 | 5 | local str_sub = string.sub 6 | local str_find = string.find 7 | 8 | local tbl_concat = table.concat 9 | local tbl_insert = table.insert 10 | 11 | local ngx_re_match = ngx.re.match 12 | local ngx_req_get_headers = ngx.req.get_headers 13 | local ngx_req_get_uri_args = ngx.req.get_uri_args 14 | local ngx_encode_args = ngx.encode_args 15 | local ngx_req_set_uri_args = ngx.req.set_uri_args 16 | local ngx_var = ngx.var 17 | local ngx_log = ngx.log 18 | local ngx_ERR = ngx.ERR 19 | 20 | 21 | local _M = { 22 | _VERSION = "2.3.0", 23 | } 24 | 25 | 26 | local esi_processors = { 27 | ["ESI"] = { 28 | ["1.0"] = require "ledge.esi.processor_1_0", 29 | -- 2.0 = require ledge.esi.processor_2_0", -- for example 30 | }, 31 | } 32 | 33 | 34 | function _M.split_esi_token(token) 35 | if token then 36 | local m = ngx_re_match( 37 | token, 38 | [[^([A-Za-z0-9-_]+)\/(\d+\.?\d+)$]], 39 | "oj" 40 | ) 41 | if m then 42 | return m[1], tonumber(m[2]) 43 | end 44 | end 45 | end 46 | 47 | 48 | function _M.esi_capabilities() 49 | local capabilities = {} 50 | for processor_type,processors in pairs(esi_processors) do 51 | for version,_ in pairs(processors) do 52 | tbl_insert(capabilities, processor_type .. "/" .. version) 53 | end 54 | end 55 | return tbl_concat(capabilities, " ") 56 | end 57 | 58 | 59 | -- Returns a processor instance based on Surrogate-Control header 60 | function _M.choose_esi_processor(handler) 61 | local res = handler.response 62 | local res_surrogate_control = res.header["Surrogate-Control"] 63 | 64 | if res_surrogate_control then 65 | -- Get the token value (e.g. "ESI/1.0") 66 | local content_token = 67 | h_util.get_header_token(res_surrogate_control, "content") 68 | 69 | if content_token then 70 | local processor_token, version = _M.split_esi_token(content_token) 71 | 72 | if processor_token and version then 73 | -- Lookup the prcoessor 74 | local processor_type = esi_processors[processor_token] 75 | 76 | if processor_type then 77 | for v,processor in pairs(processor_type) do 78 | if tonumber(version) <= tonumber(v) then 79 | return processor.new(handler) 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | 88 | 89 | -- Returns true of res.header.Content-Type is in allowed_types 90 | function _M.is_allowed_content_type(res, allowed_types) 91 | if allowed_types and type(allowed_types) == "table" then 92 | local res_content_type = res.header["Content-Type"] 93 | if res_content_type then 94 | for _, content_type in ipairs(allowed_types) do 95 | local sep = str_find(res_content_type, ";") 96 | if sep then sep = sep - 1 end 97 | if str_sub(res_content_type, 1, sep) == content_type then 98 | return true 99 | end 100 | end 101 | end 102 | end 103 | end 104 | 105 | 106 | -- Returns true if we're allowed to delegate ESI processing to a downstream 107 | -- surrogate for the current request 108 | function _M.can_delegate_to_surrogate(surrogates, processor_token) 109 | local surrogate_capability = ngx_req_get_headers()["Surrogate-Capability"] 110 | 111 | if surrogate_capability then 112 | -- Surrogate-Capability: host.example.com="ESI/1.0" 113 | local capability_token = h_util.get_header_token( 114 | surrogate_capability, 115 | "[!#\\$%&'\\*\\+\\-.\\^_`\\|~0-9a-zA-Z]+" 116 | ) 117 | 118 | local capability_processor, capability_version = 119 | _M.split_esi_token(capability_token) 120 | 121 | if capability_processor and capability_version then 122 | local control_processor, control_version = 123 | _M.split_esi_token(processor_token) 124 | 125 | if control_processor and control_version 126 | and control_processor == capability_processor 127 | and control_version <= capability_version then 128 | 129 | if type(surrogates) == "boolean" then 130 | if surrogates == true then 131 | return true 132 | end 133 | elseif type(surrogates) == "table" then 134 | local remote_addr = ngx_var.remote_addr 135 | if remote_addr then 136 | for _, ip in ipairs(surrogates) do 137 | if ip == remote_addr then 138 | return true 139 | end 140 | end 141 | end 142 | end 143 | end 144 | end 145 | end 146 | 147 | return false 148 | end 149 | 150 | 151 | function _M.filter_esi_args(handler) 152 | local config = handler.config 153 | local esi_args_prefix = config.esi_args_prefix 154 | if esi_args_prefix then 155 | local args = ngx_req_get_uri_args(config.max_uri_args) 156 | local esi_args = {} 157 | local has_esi_args = false 158 | local non_esi_args = {} 159 | 160 | for k,v in pairs(args) do 161 | -- TODO: optimise 162 | -- If we have the prefix, extract the suffix 163 | local m, err = ngx_re_match( 164 | k, 165 | "^" .. esi_args_prefix .. "(\\S+)", 166 | "oj" 167 | ) 168 | if err then ngx_log(ngx_ERR, err) end 169 | 170 | if m and m[1] then 171 | has_esi_args = true 172 | esi_args[m[1]] = v 173 | else 174 | -- Otherwise, this is a normal arg 175 | non_esi_args[k] = v 176 | end 177 | end 178 | 179 | if has_esi_args then 180 | -- Add them to ctx to be read by the esi processor, along with a 181 | -- __tostsring metamethod for the $(ESI_ARGS) string case 182 | ngx.ctx.__ledge_esi_args = setmetatable(esi_args, { 183 | __tostring = function(t) 184 | local args = {} 185 | for k,v in pairs(t) do 186 | args[esi_args_prefix .. k] = v 187 | end 188 | return ngx_encode_args(args) 189 | end 190 | }) 191 | 192 | -- Set the request args to the ones left over 193 | ngx_req_set_uri_args(non_esi_args) 194 | end 195 | end 196 | end 197 | 198 | 199 | return _M 200 | -------------------------------------------------------------------------------- /lib/ledge/esi/tag_parser.lua: -------------------------------------------------------------------------------- 1 | local setmetatable, type = 2 | setmetatable, type 3 | 4 | local str_sub = string.sub 5 | 6 | local ngx_re_find = ngx.re.find 7 | local ngx_re_match = ngx.re.match 8 | local ngx_log = ngx.log 9 | local ngx_ERR = ngx.ERR 10 | 11 | local get_fixed_field_metatable_proxy = 12 | require("ledge.util").mt.get_fixed_field_metatable_proxy 13 | 14 | 15 | local _M = { 16 | _VERSION = "2.3.0", 17 | } 18 | 19 | 20 | function _M.new(content, offset) 21 | return setmetatable({ 22 | content = content, 23 | pos = (offset or 0), 24 | open_comments = 0, 25 | }, get_fixed_field_metatable_proxy(_M)) 26 | end 27 | 28 | 29 | function _M.next(self, tagname) 30 | local tag = self:find_whole_tag(tagname) 31 | local before, after 32 | if tag then 33 | before = str_sub(self.content, self.pos + 1, tag.opening.from - 1) 34 | 35 | if tag.closing then 36 | -- This is block level (with a closing tag) 37 | after = str_sub(self.content, tag.closing.to + 1) 38 | self.pos = tag.closing.to 39 | else 40 | -- Inline (no closing tag) 41 | after = str_sub(self.content, tag.opening.to + 1) 42 | self.pos = tag.opening.to 43 | end 44 | end 45 | 46 | return tag, before, after 47 | end 48 | 49 | 50 | function _M.open_pattern(tag) 51 | if tag == "!--esi" then 52 | return "<(!--esi)" 53 | else 54 | -- $1: the tag name, $2 the closing characters, e.g. "/>" or ">" 55 | return "<(" .. tag .. [[)(?:\s*(?:[a-z]+=\".+?(?]*?(?:\s*)(\/>|>)?]] 56 | end 57 | end 58 | 59 | 60 | function _M.close_pattern(tag) 61 | if tag == "!--esi" then 62 | return "-->" 63 | else 64 | -- $1: the tag name 65 | return "" 66 | end 67 | end 68 | 69 | 70 | function _M.either_pattern(tag) 71 | if tag == "!--esi" then 72 | return "(?:<(!--esi)|(-->))" 73 | else 74 | -- $1: the tag name, $2 the closing characters, e.g. "/>" or ">" 75 | return [[<[\/]?(]] .. tag .. [[)(?:\s*(?:[a-z]+=\".+?(?]*?(?:\s*)(\s*\\/>|>)?]] 76 | end 77 | end 78 | 79 | 80 | -- Finds the next esi tag, accounting for nesting to find the correct 81 | -- matching closing tag etc. 82 | function _M.find_whole_tag(self, tag) 83 | -- Only work on the remaining markup (after pos) 84 | local markup = str_sub(self.content, self.pos + 1) 85 | 86 | if not tag then 87 | -- Look for anything (including comment syntax) 88 | tag = "(?:!--esi)|(?:esi:[a-z]+)" 89 | end 90 | 91 | -- Find the first opening tag 92 | local opening_f, opening_t, err = ngx_re_find(markup, self.open_pattern(tag), "soj") 93 | if not opening_f then 94 | if err then ngx_log(ngx_ERR, err) end 95 | -- Nothing here 96 | return nil 97 | end 98 | 99 | -- We found an opening tag and has its position, but need to understand it better 100 | -- to handle comments and inline tags. 101 | local opening_m, err = ngx_re_match( 102 | str_sub(markup, opening_f, opening_t), 103 | self.open_pattern(tag), "soj" 104 | ) 105 | if not opening_m then 106 | if err then ngx_log(ngx_ERR, err) end 107 | return nil 108 | end 109 | 110 | -- We return a table with opening tag positions (absolute), as well as 111 | -- tag contents etc. Block level tags will have "closing" data too. 112 | local ret = { 113 | opening = { 114 | from = opening_f + self.pos, 115 | to = opening_t + self.pos, 116 | tag = str_sub(markup, opening_f, opening_t), 117 | }, 118 | tagname = opening_m[1], 119 | closing = nil, 120 | contents = nil, 121 | } 122 | 123 | -- If this is an inline (non-block) tag, we have everything 124 | if type(opening_m[2]) == "string" and str_sub(opening_m[2], -2) == "/>" then 125 | ret.whole = str_sub(markup, opening_f, opening_t) 126 | return ret 127 | end 128 | 129 | -- We must be block level, and could potentially be nesting 130 | 131 | local search = opening_t -- We search from after the opening tag 132 | 133 | local f, t, closing_f, closing_t 134 | local depth = 1 135 | local level = 1 136 | 137 | repeat 138 | -- keep looking for opening or closing tags 139 | f, t = ngx_re_find(str_sub(markup, search + 1), self.either_pattern(ret.tagname), "soj") 140 | if f and t then 141 | -- Move closing markers along 142 | closing_f = f 143 | closing_t = t 144 | 145 | -- Track current level and total depth 146 | local tag = str_sub(markup, search + f, search + t) 147 | if ngx_re_find(tag, self.open_pattern(ret.tagname)) then 148 | depth = depth + 1 149 | level = level + 1 150 | elseif ngx_re_find(tag, self.close_pattern(ret.tagname)) then 151 | level = level - 1 152 | end 153 | -- Move search pos along 154 | search = search + t 155 | end 156 | until level == 0 or not f 157 | 158 | if closing_t and t then 159 | -- We have a complete block tag with the matching closing tag 160 | 161 | -- Make closing tag absolute 162 | closing_t = closing_t + search - t 163 | closing_f = closing_f + search - t 164 | 165 | ret.closing = { 166 | from = closing_f + self.pos, 167 | to = closing_t + self.pos, 168 | tag = str_sub(markup, closing_f, closing_t), 169 | } 170 | ret.contents = str_sub(markup, opening_t + 1, closing_f - 1) 171 | ret.whole = str_sub(markup, opening_f, closing_t) 172 | 173 | return ret 174 | else 175 | -- We have an opening block tag, but not the closing part. Return 176 | -- what we can as the filters will buffer until we find the rest. 177 | return ret 178 | end 179 | end 180 | 181 | 182 | return _M 183 | -------------------------------------------------------------------------------- /lib/ledge/gzip.lua: -------------------------------------------------------------------------------- 1 | local co_yield = coroutine.yield 2 | local co_wrap = require("ledge.util").coroutine.wrap 3 | 4 | local ngx_log = ngx.log 5 | local ngx_ERR = ngx.ERR 6 | 7 | local zlib = require("ffi-zlib") 8 | 9 | 10 | local _M = { 11 | _VERSION = "2.3.0", 12 | } 13 | 14 | 15 | local zlib_output = function(data) 16 | co_yield(data) 17 | end 18 | 19 | 20 | local function get_gzip_decoder(reader) 21 | return co_wrap(function(buffer_size) 22 | local ok, err = zlib.inflateGzip(reader, zlib_output, buffer_size) 23 | if not ok then 24 | ngx_log(ngx_ERR, err) 25 | end 26 | 27 | -- zlib decides it is done when the stream is complete. 28 | -- Call reader() one more time to resume the next coroutine in the 29 | -- chain. 30 | reader(buffer_size) 31 | end) 32 | end 33 | _M.get_gzip_decoder = get_gzip_decoder 34 | 35 | 36 | local function get_gzip_encoder(reader) 37 | return co_wrap(function(buffer_size) 38 | local ok, err = zlib.deflateGzip(reader, zlib_output, buffer_size) 39 | if not ok then 40 | ngx_log(ngx_ERR, err) 41 | end 42 | 43 | -- zlib decides it is done when the stream is complete. 44 | -- Call reader() one more time to resume the next coroutine in the 45 | -- chain 46 | reader(buffer_size) 47 | end) 48 | end 49 | _M.get_gzip_encoder = get_gzip_encoder 50 | 51 | 52 | return _M 53 | -------------------------------------------------------------------------------- /lib/ledge/header_util.lua: -------------------------------------------------------------------------------- 1 | local type, tonumber, setmetatable = 2 | type, tonumber, setmetatable 3 | 4 | local ngx_re_match = ngx.re.match 5 | local ngx_re_find = ngx.re.find 6 | local tbl_concat = table.concat 7 | 8 | 9 | local _M = { 10 | _VERSION = "2.3.0" 11 | } 12 | 13 | local mt = { 14 | __index = _M, 15 | } 16 | 17 | 18 | -- Returns true if the directive appears in the header field value. 19 | -- Set without_token to true to only return bare directives - i.e. 20 | -- directives appearing with no =value part. 21 | function _M.header_has_directive(header, directive, without_token) 22 | if header then 23 | if type(header) == "table" then header = tbl_concat(header, ", ") end 24 | 25 | local pattern = [[(?:\s*|,?)(]] .. directive .. [[)\s*(?:$|=|,)]] 26 | if without_token then 27 | pattern = [[(?:\s*|,?)(]] .. directive .. [[)\s*(?:$|,)]] 28 | end 29 | 30 | return ngx_re_find(header, pattern, "ioj") ~= nil 31 | end 32 | return false 33 | end 34 | 35 | 36 | function _M.get_header_token(header, directive) 37 | if _M.header_has_directive(header, directive) then 38 | if type(header) == "table" then header = tbl_concat(header, ", ") end 39 | 40 | -- Want the string value from a token 41 | local value = ngx_re_match( 42 | header, 43 | directive .. [[="?([a-z0-9_~!#%&/',`\$\*\+\-\|\^\.]+)"?]], 44 | "ioj" 45 | ) 46 | if value ~= nil then 47 | return value[1] 48 | end 49 | return nil 50 | end 51 | return nil 52 | end 53 | 54 | 55 | function _M.get_numeric_header_token(header, directive) 56 | if _M.header_has_directive(header, directive) then 57 | if type(header) == "table" then header = tbl_concat(header, ", ") end 58 | 59 | -- Want the numeric value from a token 60 | local value = ngx_re_match( 61 | header, 62 | directive .. [[="?(\d+)"?]], "ioj" 63 | ) 64 | if value ~= nil then 65 | return tonumber(value[1]) 66 | end 67 | end 68 | end 69 | 70 | return setmetatable(_M, mt) 71 | -------------------------------------------------------------------------------- /lib/ledge/jobs/collect_entity.lua: -------------------------------------------------------------------------------- 1 | local tostring = tostring 2 | local ngx_null = ngx.null 3 | 4 | local create_storage_connection = require("ledge").create_storage_connection 5 | 6 | 7 | local _M = { 8 | _VERSION = "2.3.0", 9 | } 10 | 11 | 12 | -- Cleans up expired items and keeps track of memory usage. 13 | function _M.perform(job) 14 | local storage, err = create_storage_connection( 15 | job.data.storage_driver, 16 | job.data.storage_driver_config 17 | ) 18 | 19 | if not storage then 20 | return nil, "job-error", "could not connect to storage driver: "..tostring(err) 21 | end 22 | 23 | local ok, err = storage:delete(job.data.entity_id) 24 | storage:close() 25 | 26 | if ok == nil or ok == ngx_null then 27 | return nil, "job-error", tostring(err) 28 | end 29 | end 30 | 31 | 32 | return _M 33 | -------------------------------------------------------------------------------- /lib/ledge/jobs/purge.lua: -------------------------------------------------------------------------------- 1 | local ipairs, tonumber = ipairs, tonumber 2 | local ngx_log = ngx.log 3 | local ngx_DEBUG = ngx.DEBUG 4 | local ngx_ERR = ngx.ERR 5 | local ngx_null = ngx.null 6 | 7 | local purge = require("ledge.purge").purge 8 | local create_redis_slave_connection = require("ledge").create_redis_slave_connection 9 | local close_redis_connection = require("ledge").close_redis_connection 10 | 11 | local _M = { 12 | _VERSION = "2.3.0", 13 | } 14 | 15 | 16 | -- Scans the keyspace for keys which match, and expires them. We do this against 17 | -- the slave Redis instance if available. 18 | function _M.perform(job) 19 | if not job.redis then 20 | return nil, "job-error", "no redis connection provided" 21 | end 22 | 23 | local slave, _ = create_redis_slave_connection() 24 | if not slave then 25 | job.redis_slave = job.redis 26 | else 27 | job.redis_slave = slave 28 | end 29 | 30 | -- Setup handler 31 | local handler = require("ledge").create_handler() 32 | handler.redis = job.redis 33 | 34 | local storage, err = require("ledge").create_storage_connection( 35 | job.data.storage_driver, 36 | job.data.storage_driver_config 37 | ) 38 | if not storage then 39 | return nil, "redis-error", err 40 | end 41 | 42 | handler.storage = storage 43 | 44 | -- This runs recursively using the SCAN cursor, until the entire keyspace 45 | -- has been scanned. 46 | local res, err = _M.expire_pattern(0, job, handler) 47 | 48 | if slave then 49 | close_redis_connection(slave) 50 | end 51 | 52 | if not res then 53 | return nil, "redis-error", err 54 | end 55 | end 56 | 57 | 58 | -- Scans the keyspace based on a pattern (asterisk), and runs a purge for each cache entry 59 | function _M.expire_pattern(cursor, job, handler) 60 | if job:ttl() < 10 then 61 | if not job:heartbeat() then 62 | return nil, "Failed to heartbeat job" 63 | end 64 | end 65 | 66 | -- Scan using the "main" key to get a single key per cache entry 67 | local res, err = job.redis_slave:scan( 68 | cursor, 69 | "MATCH", job.data.repset, 70 | "COUNT", job.data.keyspace_scan_count 71 | ) 72 | 73 | if not res or res == ngx_null then 74 | return nil, "SCAN error: " .. tostring(err) 75 | else 76 | for _,key in ipairs(res[2]) do 77 | ngx_log(ngx_DEBUG, "Purging set: ", key) 78 | 79 | local ok, err = purge(handler, job.data.purge_mode, key) 80 | if ok == nil and err then ngx_log(ngx_ERR, tostring(err)) end 81 | 82 | end 83 | 84 | local cursor = tonumber(res[1]) 85 | if cursor == 0 then 86 | return true 87 | end 88 | 89 | -- If we have a valid cursor, recurse to move on. 90 | return _M.expire_pattern(cursor, job, handler) 91 | end 92 | end 93 | 94 | 95 | return _M 96 | -------------------------------------------------------------------------------- /lib/ledge/jobs/revalidate.lua: -------------------------------------------------------------------------------- 1 | local http = require "resty.http" 2 | local http_headers = require "resty.http_headers" 3 | local ngx_null = ngx.null 4 | 5 | local _M = { 6 | _VERSION = "2.3.0", 7 | } 8 | 9 | 10 | -- Utility to return all items in a Redis hash as a Lua table. 11 | local function hgetall(redis, key) 12 | local res, err = redis:hgetall(key) 13 | if not res or res == ngx_null then 14 | return nil, 15 | "could not retrieve " .. tostring(key) .. " data:" .. tostring(err) 16 | end 17 | 18 | return redis:array_to_hash(res) 19 | end 20 | 21 | 22 | function _M.perform(job) 23 | -- Normal background revalidation operates on stored metadata. 24 | -- A background fetch due to partial content from upstream however, uses the 25 | -- current request metadata for reval_headers / reval_params and passes it 26 | -- through as job data. 27 | local reval_params = job.data.reval_params 28 | local reval_headers = job.data.reval_headers 29 | 30 | -- If we don't have the metadata in job data, this is a background 31 | -- revalidation using stored metadata. 32 | if not reval_params and not reval_headers then 33 | local key_chain, redis, err = job.data.key_chain, job.redis 34 | 35 | reval_params, err = hgetall(redis, key_chain.reval_params) 36 | if not reval_params or not next(reval_params) then 37 | return nil, "job-error", 38 | "Revalidation parameters are missing, presumed evicted. " .. 39 | tostring(err) 40 | end 41 | 42 | reval_headers, err = hgetall(redis, key_chain.reval_req_headers) 43 | if not reval_headers or not next(reval_headers) then 44 | return nil, "job-error", 45 | "Revalidation headers are missing, presumed evicted." .. 46 | tostring(err) 47 | end 48 | end 49 | 50 | -- Make outbound http request to revalidate 51 | local httpc = http.new() 52 | httpc:set_timeouts( 53 | reval_params.connect_timeout, 54 | reval_params.send_timeout, 55 | reval_params.read_timeout 56 | ) 57 | 58 | local port = tonumber(reval_params.server_port) 59 | local ok, err 60 | if port then 61 | ok, err = httpc:connect(reval_params.server_addr, port) 62 | else 63 | ok, err = httpc:connect(reval_params.server_addr) 64 | end 65 | 66 | if not ok then 67 | return nil, "job-error", 68 | "could not connect to server: " .. tostring(err) 69 | end 70 | 71 | if reval_params.scheme == "https" then 72 | local ok, err = httpc:ssl_handshake(false, nil, false) 73 | if not ok then 74 | return nil, "job-error", "ssl handshake failed: " .. tostring(err) 75 | end 76 | end 77 | 78 | local headers = http_headers.new() -- Case-insensitive header table 79 | headers["Cache-Control"] = "max-stale=0, stale-if-error=0" 80 | headers["User-Agent"] = 81 | httpc._USER_AGENT .. " ledge_revalidate/" .. _M._VERSION 82 | 83 | -- Add additional headers from parent 84 | for k,v in pairs(reval_headers) do 85 | headers[k] = v 86 | end 87 | 88 | local res, err = httpc:request{ 89 | method = "GET", 90 | path = reval_params.uri, 91 | headers = headers, 92 | } 93 | 94 | if not res then 95 | return nil, "job-error", "revalidate failed: " .. tostring(err) 96 | else 97 | local reader = res.body_reader 98 | -- Read and discard the body 99 | repeat 100 | local chunk, _ = reader() 101 | until not chunk 102 | 103 | httpc:set_keepalive( 104 | reval_params.keepalive_timeout, 105 | reval_params.keepalive_poolsize 106 | ) 107 | end 108 | end 109 | 110 | 111 | return _M 112 | -------------------------------------------------------------------------------- /lib/ledge/request.lua: -------------------------------------------------------------------------------- 1 | local hdr_has_directive = require("ledge.header_util").header_has_directive 2 | 3 | local ngx_req_get_headers = ngx.req.get_headers 4 | local ngx_re_gsub = ngx.re.gsub 5 | local ngx_req_get_uri_args = ngx.req.get_uri_args 6 | local ngx_req_get_method = ngx.req.get_method 7 | 8 | local str_byte = string.byte 9 | 10 | local ngx_var = ngx.var 11 | 12 | local tbl_sort = table.sort 13 | local tbl_insert = table.insert 14 | 15 | 16 | local _M = { 17 | _VERSION = "2.3.0", 18 | } 19 | 20 | 21 | local function purge_mode() 22 | local x_purge = ngx_req_get_headers()["X-Purge"] 23 | if hdr_has_directive(x_purge, "delete") then 24 | return "delete" 25 | elseif hdr_has_directive(x_purge, "revalidate") then 26 | return "revalidate" 27 | else 28 | return "invalidate" 29 | end 30 | end 31 | _M.purge_mode = purge_mode 32 | 33 | 34 | local function relative_uri() 35 | local uri = ngx_re_gsub(ngx_var.uri, "\\s", "%20", "jo") -- encode spaces 36 | 37 | -- encode percentages if an encoded CRLF is in the URI 38 | -- see: http://resources.infosecinstitute.com/http-response-splitting-attack 39 | uri = ngx_re_gsub(uri, "%0D%0A", "%250D%250A", "ijo") 40 | 41 | return uri .. ngx_var.is_args .. (ngx_var.query_string or "") 42 | end 43 | _M.relative_uri = relative_uri 44 | 45 | 46 | local function full_uri() 47 | return ngx_var.scheme .. '://' .. ngx_var.host .. relative_uri() 48 | end 49 | _M.full_uri = full_uri 50 | 51 | 52 | local function accepts_cache() 53 | -- Check for no-cache 54 | local h = ngx_req_get_headers() 55 | if hdr_has_directive(h["Pragma"], "no-cache") 56 | or hdr_has_directive(h["Cache-Control"], "no-cache") 57 | or hdr_has_directive(h["Cache-Control"], "no-store") then 58 | return false 59 | end 60 | 61 | return true 62 | end 63 | _M.accepts_cache = accepts_cache 64 | 65 | 66 | local function sort_args(a, b) 67 | return a[1] < b[1] 68 | end 69 | 70 | 71 | local function args_sorted(max_args) 72 | max_args = max_args or 100 73 | local args = ngx_req_get_uri_args(max_args) 74 | if not next(args) then return nil end 75 | 76 | local sorted = {} 77 | for k, v in pairs(args) do 78 | tbl_insert(sorted, { k, v }) 79 | end 80 | 81 | tbl_sort(sorted, sort_args) 82 | 83 | local sargs = "" 84 | local sortedln = #sorted 85 | for i, v in ipairs(sorted) do 86 | sargs = sargs .. ngx.encode_args({ [v[1]] = v[2] }) 87 | if i < sortedln then sargs = sargs .. "&" end 88 | end 89 | 90 | return sargs 91 | end 92 | _M.args_sorted = args_sorted 93 | 94 | 95 | -- Used to generate a default args string for the cache key (i.e. when there are 96 | -- no URI args present). 97 | -- 98 | -- Returns a zero length string, unless there is an asterisk at the end of the 99 | -- URI on a PURGE request, in which case we return the asterisk. 100 | -- 101 | -- The purpose it to ensure trailing wildcards are greedy across both URI and 102 | -- args portions of a cache key. 103 | -- 104 | -- If you override the "args" field in a cache key spec with your own function, 105 | -- you'll want to use this to ensure wildcard purges operate correctly. 106 | local function default_args() 107 | if ngx_req_get_method() == "PURGE" and 108 | str_byte(ngx_var.request_uri, -1) == 42 109 | then 110 | return "*" 111 | end 112 | return "" 113 | end 114 | _M.default_args = default_args 115 | 116 | 117 | return _M 118 | -------------------------------------------------------------------------------- /lib/ledge/stale.lua: -------------------------------------------------------------------------------- 1 | local math_min = math.min 2 | 3 | local ngx_req_get_headers = ngx.req.get_headers 4 | 5 | local header_has_directive = require("ledge.header_util").header_has_directive 6 | local get_numeric_header_token = 7 | require("ledge.header_util").get_numeric_header_token 8 | 9 | 10 | local _M = { 11 | _VERSION = "2.3.0" 12 | } 13 | 14 | 15 | -- True if the request specifically asks for stale (req.cc.max-stale) and the 16 | -- response doesn't explicitly forbid this res.cc.(must|proxy)-revalidate. 17 | local function can_serve_stale(res) 18 | local req_cc = ngx_req_get_headers()["Cache-Control"] 19 | local req_cc_max_stale = get_numeric_header_token(req_cc, "max-stale") 20 | if req_cc_max_stale then 21 | local res_cc = res.header["Cache-Control"] 22 | 23 | -- Check the response permits this at all 24 | if header_has_directive(res_cc, "(must|proxy)-revalidate") then 25 | return false 26 | else 27 | if (req_cc_max_stale * -1) <= res.remaining_ttl then 28 | return true 29 | end 30 | end 31 | end 32 | return false 33 | end 34 | _M.can_serve_stale = can_serve_stale 35 | 36 | 37 | -- Returns true if stale-while-revalidate or stale-if-error is specified, valid 38 | -- and not constrained by other factors such as max-stale. 39 | -- @param token "stale-while-revalidate" | "stale-if-error" 40 | local function verify_stale_conditions(res, token) 41 | assert(token == "stale-while-revalidate" or token == "stale-if-error", 42 | "unknown token: " .. tostring(token)) 43 | 44 | local res_cc = res.header["Cache-Control"] 45 | local res_cc_stale = get_numeric_header_token(res_cc, token) 46 | 47 | -- Check the response permits this at all 48 | if header_has_directive(res_cc, "(must|proxy)-revalidate") then 49 | return false 50 | end 51 | 52 | -- Get request header tokens 53 | local req_cc = ngx_req_get_headers()["Cache-Control"] 54 | local req_cc_stale = get_numeric_header_token(req_cc, token) 55 | local req_cc_max_age = get_numeric_header_token(req_cc, "max-age") 56 | local req_cc_max_stale = get_numeric_header_token(req_cc, "max-stale") 57 | 58 | local stale_ttl = 0 59 | -- If we have both req and res stale-" .. reason, use the lower value 60 | if req_cc_stale and res_cc_stale then 61 | stale_ttl = math_min(req_cc_stale, res_cc_stale) 62 | -- Otherwise return the req or res value 63 | elseif req_cc_stale then 64 | stale_ttl = req_cc_stale 65 | elseif res_cc_stale then 66 | stale_ttl = res_cc_stale 67 | end 68 | 69 | if stale_ttl <= 0 then 70 | return false -- No stale policy defined 71 | elseif header_has_directive(req_cc, "min-fresh") then 72 | return false -- Cannot serve stale as request demands freshness 73 | elseif req_cc_max_age and 74 | req_cc_max_age < (tonumber(res.header["Age"] or 0) or 0) then 75 | return false -- Cannot serve stale as req max-age is less than res Age 76 | elseif req_cc_max_stale and req_cc_max_stale < stale_ttl then 77 | return false -- Cannot serve stale as req max-stale is less than S-W-R 78 | else 79 | -- We can return stale 80 | return true 81 | end 82 | end 83 | _M.verify_stale_conditions = verify_stale_conditions 84 | 85 | 86 | local function can_serve_stale_while_revalidate(res) 87 | return verify_stale_conditions(res, "stale-while-revalidate") 88 | end 89 | _M.can_serve_stale_while_revalidate = can_serve_stale_while_revalidate 90 | 91 | 92 | local function can_serve_stale_if_error(res) 93 | return verify_stale_conditions(res, "stale-if-error") 94 | end 95 | _M.can_serve_stale_if_error = can_serve_stale_if_error 96 | 97 | 98 | return _M 99 | -------------------------------------------------------------------------------- /lib/ledge/state_machine.lua: -------------------------------------------------------------------------------- 1 | local events = require("ledge.state_machine.events") 2 | local pre_transitions = require("ledge.state_machine.pre_transitions") 3 | local states = require("ledge.state_machine.states") 4 | local actions = require("ledge.state_machine.actions") 5 | 6 | local ngx_log = ngx.log 7 | local ngx_DEBUG = ngx.DEBUG 8 | local ngx_ERR = ngx.ERR 9 | 10 | 11 | local get_fixed_field_metatable_proxy = 12 | require("ledge.util").mt.get_fixed_field_metatable_proxy 13 | 14 | 15 | local _DEBUG = false 16 | 17 | local _M = { 18 | _VERSION = "2.3.0", 19 | set_debug = function(debug) _DEBUG = debug end, 20 | } 21 | 22 | 23 | local function new(handler) 24 | return setmetatable({ 25 | handler = handler, 26 | state_history = {}, 27 | event_history = {}, 28 | current_state = "", 29 | }, get_fixed_field_metatable_proxy(_M)) 30 | end 31 | _M.new = new 32 | 33 | 34 | -- Transition to a new state. 35 | local function t(self, state) 36 | -- Check for any transition pre-tasks 37 | local pre_t = pre_transitions[state] 38 | 39 | if pre_t then 40 | for _,action in ipairs(pre_t) do 41 | if _DEBUG then ngx_log(ngx_DEBUG, "#a: ", action) end 42 | local ok, err = pcall(actions[action], self.handler) 43 | if not ok then 44 | ngx_log(ngx_ERR, "state '", state, "' failed to call action '", action, "': ", tostring(err)) 45 | end 46 | end 47 | end 48 | 49 | if _DEBUG then ngx_log(ngx_DEBUG, "#t: ", state) end 50 | 51 | self.state_history[state] = true 52 | self.current_state = state 53 | return states[state](self, self.handler) 54 | end 55 | _M.t = t 56 | 57 | 58 | -- Process state transitions and actions based on the event fired. 59 | local function e(self, event) 60 | if _DEBUG then ngx_log(ngx_DEBUG, "#e: ", event) end 61 | 62 | self.event_history[event] = true 63 | 64 | -- It's possible for states to call undefined events at run time. 65 | if not events[event] then 66 | ngx_log(ngx.CRIT, event, " is not defined.") 67 | ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR 68 | self:t("exiting") 69 | end 70 | 71 | for _, trans in ipairs(events[event]) do 72 | local t_when = trans["when"] 73 | if t_when == nil or t_when == self.current_state then 74 | local t_after = trans["after"] 75 | if not t_after or self.state_history[t_after] then 76 | local t_in_case = trans["in_case"] 77 | if not t_in_case or self.event_history[t_in_case] then 78 | local t_but_first = trans["but_first"] 79 | if t_but_first then 80 | if type(t_but_first) == "table" then 81 | for _,action in ipairs(t_but_first) do 82 | if _DEBUG then 83 | ngx_log(ngx_DEBUG, "#a: ", action) 84 | end 85 | actions[action](self.handler) 86 | end 87 | else 88 | if _DEBUG then 89 | ngx_log(ngx_DEBUG, "#a: ", t_but_first) 90 | end 91 | actions[t_but_first](self.handler) 92 | end 93 | end 94 | 95 | return self:t(trans["begin"]) 96 | end 97 | end 98 | end 99 | end 100 | end 101 | _M.e = e 102 | 103 | 104 | return _M 105 | -------------------------------------------------------------------------------- /lib/ledge/state_machine/actions.lua: -------------------------------------------------------------------------------- 1 | local type, next = type, next 2 | 3 | local esi = require("ledge.esi") 4 | local response = require("ledge.response") 5 | 6 | local ngx_var = ngx.var 7 | 8 | local ngx_HTTP_NOT_MODIFIED = ngx.HTTP_NOT_MODIFIED 9 | 10 | local ngx_req_set_header = ngx.req.set_header 11 | 12 | local get_gzip_decoder = require("ledge.gzip").get_gzip_decoder 13 | 14 | local _M = { -- luacheck: no unused 15 | _VERSION = "2.3.0", 16 | } 17 | 18 | 19 | -- Actions. Functions which can be called on transition. 20 | return { 21 | redis_close = function(handler) 22 | return require("ledge").close_redis_connection(handler.redis) 23 | end, 24 | 25 | httpc_close = function(handler) 26 | local upstream_client = handler.upstream_client 27 | if next(upstream_client) then 28 | if type(upstream_client.set_keepalive) == "function" then 29 | local config = handler.config 30 | return upstream_client:set_keepalive( 31 | config.upstream_keepalive_timeout, 32 | config.upstream_keepalive_poolsize 33 | ) 34 | end 35 | end 36 | end, 37 | 38 | httpc_close_without_keepalive = function(handler) 39 | local upstream_client = handler.upstream_client 40 | if next(upstream_client) then 41 | return upstream_client:close() 42 | end 43 | end, 44 | 45 | stash_error_response = function(handler) 46 | handler.error_response = handler.response 47 | end, 48 | 49 | restore_error_response = function(handler) 50 | local error_res = handler.error_response 51 | if next(error_res) then 52 | handler.response = error_res 53 | end 54 | end, 55 | 56 | -- If ESI is enabled and we have an esi_args prefix, weed uri args 57 | -- beginning with the prefix (knows as ESI_ARGS) out of the URI (and thus 58 | -- cache key) and stash them in the custom ESI variables table. 59 | filter_esi_args = function(handler) 60 | if handler.config.esi_enabled then 61 | esi.filter_esi_args(handler) 62 | end 63 | end, 64 | 65 | read_cache = function(handler) 66 | handler.response = handler:read_from_cache() 67 | end, 68 | 69 | install_no_body_reader = function(handler) 70 | local res = handler.response 71 | res.body_reader = res.empty_body_reader 72 | end, 73 | 74 | install_gzip_decoder = function(handler) 75 | local res = handler.response 76 | res.header["Content-Encoding"] = nil 77 | res:filter_body_reader( 78 | "gzip_decoder", 79 | get_gzip_decoder(res.body_reader) 80 | ) 81 | end, 82 | 83 | install_range_filter = function(handler) 84 | local res = handler.response 85 | local range = handler.range 86 | res:filter_body_reader( 87 | "range_request_filter", 88 | range:get_range_request_filter(res.body_reader) 89 | ) 90 | end, 91 | 92 | set_esi_scan_enabled = function(handler) 93 | handler.esi_scan_enabled = true 94 | --handler.esi_scan_disabled = false 95 | handler.response.esi_scanned = true 96 | end, 97 | 98 | install_esi_scan_filter = function(handler) 99 | local res = handler.response 100 | local esi_processor = handler.esi_processor 101 | 102 | if next(esi_processor) then 103 | res:filter_body_reader( 104 | "esi_scan_filter", 105 | esi_processor:get_scan_filter(res) 106 | ) 107 | end 108 | end, 109 | 110 | set_esi_scan_disabled = function(handler) 111 | local res = handler.response 112 | handler.esi_scan_enabled = false 113 | res.esi_scanned = false 114 | end, 115 | 116 | install_esi_process_filter = function(handler) 117 | local res = handler.response 118 | local esi_processor = handler.esi_processor 119 | 120 | if next(esi_processor) then 121 | res:filter_body_reader( 122 | "esi_process_filter", 123 | esi_processor:get_process_filter(res) 124 | ) 125 | end 126 | end, 127 | 128 | set_esi_process_enabled = function(handler) 129 | handler.esi_process_enabled = true 130 | end, 131 | 132 | set_esi_process_disabled = function(handler) 133 | handler.esi_process_enabled = false 134 | end, 135 | 136 | zero_downstream_lifetime = function(handler) 137 | local res = handler.response 138 | if res.header then 139 | res.header["Cache-Control"] = "private, max-age=0" 140 | end 141 | end, 142 | 143 | remove_surrogate_control_header = function(handler) 144 | local res = handler.response 145 | if res.header then 146 | res.header["Surrogate-Control"] = nil 147 | end 148 | end, 149 | 150 | fetch = function(handler) 151 | local res = handler:fetch_from_origin() 152 | if res.status ~= ngx_HTTP_NOT_MODIFIED then 153 | handler.response = res 154 | end 155 | end, 156 | 157 | remove_client_validators = function(handler) 158 | -- Keep these in case we need to restore them (after revalidating upstream) 159 | local client_validators = handler.client_validators 160 | client_validators["If-Modified-Since"] = ngx_var.http_if_modified_since 161 | client_validators["If-None-Match"] = ngx_var.http_if_none_match 162 | 163 | ngx_req_set_header("If-Modified-Since", nil) 164 | ngx_req_set_header("If-None-Match", nil) 165 | end, 166 | 167 | restore_client_validators = function(handler) 168 | local client_validators = handler.client_validators 169 | ngx_req_set_header("If-Modified-Since", client_validators["If-Modified-Since"]) 170 | ngx_req_set_header("If-None-Match", client_validators["If-None-Match"]) 171 | end, 172 | 173 | add_stale_warning = function(handler) 174 | return handler:add_warning("110") 175 | end, 176 | 177 | add_disconnected_warning = function(handler) 178 | return handler:add_warning("112") 179 | end, 180 | 181 | set_json_response = function(handler) 182 | local res = response.new(handler) 183 | res.header["Content-Type"] = "application/json" 184 | handler.response = res 185 | end, 186 | 187 | -- Updates the realidation_params key with data from the current request, 188 | -- and schedules a background revalidation job 189 | revalidate_in_background = function(handler) 190 | return handler:revalidate_in_background(handler:cache_key_chain(), true) 191 | end, 192 | 193 | -- Triggered on upstream partial content, assumes no stored 194 | -- revalidation metadata but since we have a rqeuest context (which isn't 195 | -- the case with `revalidate_in_background` we can simply fetch. 196 | fetch_in_background = function(handler) 197 | return handler:fetch_in_background() 198 | end, 199 | 200 | save_to_cache = function(handler) 201 | local res = handler.response 202 | return handler:save_to_cache(res) 203 | end, 204 | 205 | delete_from_cache = function(handler) 206 | return handler:delete_from_cache(handler:cache_key_chain()) 207 | end, 208 | 209 | disable_output_buffers = function(handler) 210 | handler.output_buffers_enabled = false 211 | end, 212 | 213 | reset_cache_key = function(handler) 214 | handler:reset_cache_key() 215 | end, 216 | 217 | set_http_ok = function() 218 | ngx.status = ngx.HTTP_OK 219 | end, 220 | 221 | set_http_not_found = function() 222 | ngx.status = ngx.HTTP_NOT_FOUND 223 | end, 224 | 225 | set_http_not_modified = function() 226 | ngx.status = ngx_HTTP_NOT_MODIFIED 227 | end, 228 | 229 | set_http_service_unavailable = function() 230 | ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE 231 | end, 232 | 233 | set_http_gateway_timeout = function() 234 | ngx.status = ngx.HTTP_GATEWAY_TIMEOUT 235 | end, 236 | 237 | set_http_internal_server_error = function() 238 | ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR 239 | end, 240 | 241 | set_http_status_from_response = function(handler) 242 | local res = handler.response 243 | if res and res.status then 244 | ngx.status = res.status 245 | else 246 | ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR 247 | end 248 | end, 249 | } 250 | -------------------------------------------------------------------------------- /lib/ledge/state_machine/pre_transitions.lua: -------------------------------------------------------------------------------- 1 | local _M = { -- luacheck: no unused 2 | _VERSION = "2.3.0", 3 | } 4 | 5 | 6 | -- Pre-transitions. These actions will *always* be performed before 7 | -- transitioning. 8 | return { 9 | exiting = { "redis_close", "httpc_close" }, 10 | aborting = { "redis_close", "httpc_close_without_keepalive" }, 11 | checking_cache = { "read_cache" }, 12 | 13 | -- Never fetch with client validators, but put them back afterwards. 14 | fetching = { 15 | "remove_client_validators", "fetch", "restore_client_validators" 16 | }, 17 | 18 | -- Need to save the error response before reading from cache in case we 19 | -- need to serve it later 20 | considering_stale_error = { 21 | "stash_error_response", 22 | "read_cache" 23 | }, 24 | 25 | -- Restore the saved response and set the status when serving an error page 26 | serving_upstream_error = { 27 | "restore_error_response", 28 | "set_http_status_from_response" 29 | }, 30 | serving_stale = { 31 | "set_http_status_from_response", 32 | }, 33 | cancelling_abort_request = { 34 | "disable_output_buffers" 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /lib/ledge/util.lua: -------------------------------------------------------------------------------- 1 | local ngx_var = ngx.var 2 | local ffi = require "ffi" 3 | 4 | local type, next, setmetatable, getmetatable, error, tostring = 5 | type, next, setmetatable, getmetatable, error, tostring 6 | 7 | local str_find = string.find 8 | local str_sub = string.sub 9 | local co_create = coroutine.create 10 | local co_status = coroutine.status 11 | local co_resume = coroutine.resume 12 | local math_floor = math.floor 13 | local ffi_cdef = ffi.cdef 14 | local ffi_new = ffi.new 15 | local ffi_string = ffi.string 16 | local C = ffi.C 17 | 18 | 19 | local ok, err = pcall(ffi_cdef, [[ 20 | typedef unsigned char u_char; 21 | u_char * ngx_hex_dump(u_char *dst, const u_char *src, size_t len); 22 | int RAND_pseudo_bytes(u_char *buf, int num); 23 | ]]) 24 | if not ok then ngx.log(ngx.ERR, err) end 25 | 26 | local ok, err = pcall(ffi_cdef, [[ 27 | int gethostname (char *name, size_t size); 28 | ]]) 29 | if not ok then ngx.log(ngx.ERR, err) end 30 | 31 | 32 | local _M = { 33 | _VERSION = "2.3.0", 34 | string = {}, 35 | table = {}, 36 | mt = {}, 37 | coroutine = {}, 38 | } 39 | 40 | 41 | local function randomhex(len) 42 | local len = math_floor(len / 2) 43 | 44 | local bytes = ffi_new("uint8_t[?]", len) 45 | C.RAND_pseudo_bytes(bytes, len) 46 | if not bytes then 47 | return nil, "error getting random bytes via FFI" 48 | end 49 | 50 | local hex = ffi_new("uint8_t[?]", len * 2) 51 | C.ngx_hex_dump(hex, bytes, len) 52 | return ffi_string(hex, len * 2) 53 | end 54 | _M.string.randomhex = randomhex 55 | 56 | 57 | local function str_split(str, delim) 58 | local pos, endpos, prev, i = 0, 0, 0, 0 -- luacheck: ignore pos endpos 59 | local out = {} 60 | repeat 61 | pos, endpos = str_find(str, delim, prev, true) 62 | i = i+1 63 | if pos then 64 | out[i] = str_sub(str, prev, pos-1) 65 | else 66 | if prev <= #str then 67 | out[i] = str_sub(str, prev, -1) 68 | end 69 | break 70 | end 71 | prev = endpos +1 72 | until pos == nil 73 | 74 | return out 75 | end 76 | _M.string.split = str_split 77 | 78 | 79 | -- A metatable which prevents undefined fields from being created / accessed 80 | local fixed_field_metatable = { 81 | __index = 82 | function(t, k) -- luacheck: no unused 83 | error("field " .. tostring(k) .. " does not exist", 3) 84 | end, 85 | __newindex = 86 | function(t, k, v) -- luacheck: no unused 87 | error("attempt to create new field " .. tostring(k), 3) 88 | end, 89 | } 90 | _M.mt.fixed_field_metatable = fixed_field_metatable 91 | 92 | 93 | -- Returns a metatable with fixed fields (as above), which when applied to a 94 | -- table will provide default values via the provided `proxy`. E.g: 95 | -- 96 | -- defaults = { a = 1, b = 2, c = 3 } 97 | -- t = setmetatable({ b = 4 }, get_fixed_field_metatable_proxy(defaults)) 98 | -- 99 | -- `t` now gives: { a = 1, b = 4, c = 3 } 100 | -- 101 | -- @param table proxy table 102 | -- @return table metatable 103 | local function get_fixed_field_metatable_proxy(proxy) 104 | return { 105 | __index = 106 | function(t, k) -- luacheck: no unused 107 | return proxy[k] or 108 | error("field " .. tostring(k) .. " does not exist", 2) 109 | end, 110 | __newindex = 111 | function(t, k, v) 112 | if proxy[k] then 113 | return rawset(t, k, v) 114 | else 115 | error("attempt to create new field " .. tostring(k), 2) 116 | end 117 | end, 118 | } 119 | end 120 | _M.mt.get_fixed_field_metatable_proxy = get_fixed_field_metatable_proxy 121 | 122 | 123 | -- Returns a metatable with fixed fields (as above), which when invoked as a 124 | -- function will call the supplied `func`. E.g.: 125 | -- 126 | -- t = setmetatable( 127 | -- { a = 1, b = 2, c = 3 }, 128 | -- get_callable_fixed_field_metatable( 129 | -- function(t, field) 130 | -- print(t[field]) 131 | -- end 132 | -- ) 133 | -- ) 134 | -- t("a") -- 1 135 | -- t("b") -- 2 136 | -- 137 | -- @param function 138 | -- @return table callable metatable 139 | local function get_callable_fixed_field_metatable(func) 140 | local mt = fixed_field_metatable 141 | mt.__call = func 142 | return mt 143 | end 144 | _M.mt.get_callable_fixed_field_metatable = get_callable_fixed_field_metatable 145 | 146 | 147 | -- Returns a new table, recursively copied from the one given, retaining 148 | -- metatable assignment. 149 | -- 150 | -- @param table table to be copied 151 | -- @return table 152 | local function tbl_copy(orig) 153 | local orig_type = type(orig) 154 | local copy 155 | if orig_type == "table" then 156 | copy = {} 157 | for orig_key, orig_value in next, orig, nil do 158 | copy[tbl_copy(orig_key)] = tbl_copy(orig_value) 159 | end 160 | setmetatable(copy, tbl_copy(getmetatable(orig))) 161 | else -- number, string, boolean, etc 162 | copy = orig 163 | end 164 | return copy 165 | end 166 | _M.table.copy = tbl_copy 167 | 168 | 169 | -- Returns a new table, recursively copied from the combination of the given 170 | -- table `t1`, with any missing fields copied from `defaults`. 171 | -- 172 | -- If `defaults` is of type "fixed field" and `t1` contains a field name not 173 | -- present in the defults, an error will be thrown. 174 | -- 175 | -- @param table t1 176 | -- @param table defaults 177 | -- @return table a new table, recursively copied and merged 178 | local function tbl_copy_merge_defaults(t1, defaults) 179 | if t1 == nil then t1 = {} end 180 | if defaults == nil then defaults = {} end 181 | if type(t1) == "table" and type(defaults) == "table" then 182 | local copy = {} 183 | for t1_key, t1_value in next, t1, nil do 184 | copy[tbl_copy(t1_key)] = tbl_copy_merge_defaults( 185 | t1_value, tbl_copy(defaults[t1_key]) 186 | ) 187 | end 188 | for defaults_key, defaults_value in next, defaults, nil do 189 | if t1[defaults_key] == nil then 190 | copy[tbl_copy(defaults_key)] = tbl_copy(defaults_value) 191 | end 192 | end 193 | return copy 194 | else 195 | return t1 -- not a table 196 | end 197 | end 198 | _M.table.copy_merge_defaults = tbl_copy_merge_defaults 199 | 200 | 201 | local function co_wrap(func) 202 | local co = co_create(func) 203 | if not co then 204 | return nil, "could not create coroutine" 205 | else 206 | return function(...) 207 | if co_status(co) == "suspended" then 208 | -- Handle errors in coroutines 209 | local ok, val1, val2, val3 = co_resume(co, ...) 210 | if ok == true then 211 | return val1, val2, val3 212 | else 213 | return nil, val1 214 | end 215 | else 216 | return nil, "can't resume a " .. co_status(co) .. " coroutine" 217 | end 218 | end 219 | end 220 | end 221 | _M.coroutine.wrap = co_wrap 222 | 223 | 224 | local function get_hostname() 225 | local name = ffi_new("char[?]", 255) 226 | C.gethostname(name, 255) 227 | return ffi_string(name) 228 | end 229 | _M.get_hostname = get_hostname 230 | 231 | 232 | local function append_server_port(name) 233 | -- TODO: compare with scheme? 234 | local server_port = ngx_var.server_port 235 | if server_port ~= "80" and server_port ~= "443" then 236 | name = name .. ":" .. server_port 237 | end 238 | return name 239 | end 240 | _M.append_server_port = append_server_port 241 | 242 | 243 | return _M 244 | -------------------------------------------------------------------------------- /lib/ledge/validation.lua: -------------------------------------------------------------------------------- 1 | local ngx_req_get_headers = ngx.req.get_headers 2 | local ngx_req_set_header = ngx.req.set_header 3 | local ngx_parse_http_time = ngx.parse_http_time 4 | 5 | local get_numeric_header_token = 6 | require("ledge.header_util").get_numeric_header_token 7 | local header_has_directive = require("ledge.header_util").header_has_directive 8 | 9 | 10 | local _M = { 11 | _VERSION = "2.3.0", 12 | } 13 | 14 | 15 | -- True if the request or response (res) demand revalidation. 16 | local function must_revalidate(res) 17 | local req_cc = ngx_req_get_headers()["Cache-Control"] 18 | local req_cc_max_age = get_numeric_header_token(req_cc, "max-age") 19 | if req_cc_max_age == 0 then 20 | return true 21 | else 22 | local res_age = tonumber(res.header["Age"]) 23 | local res_cc = res.header["Cache-Control"] 24 | 25 | if header_has_directive(res_cc, "(must|proxy)-revalidate") then 26 | return true 27 | elseif req_cc_max_age and res_age then 28 | if req_cc_max_age < res_age then 29 | return true 30 | end 31 | end 32 | end 33 | return false 34 | end 35 | _M.must_revalidate = must_revalidate 36 | 37 | 38 | -- True if the request contains valid conditional headers. 39 | local function can_revalidate_locally() 40 | local req_h = ngx_req_get_headers() 41 | local req_ims = req_h["If-Modified-Since"] 42 | 43 | if req_ims then 44 | if not ngx_parse_http_time(req_ims) then 45 | -- Bad IMS HTTP datestamp, lets remove this. 46 | ngx_req_set_header("If-Modified-Since", nil) 47 | else 48 | return true 49 | end 50 | end 51 | 52 | if req_h["If-None-Match"] and req_h["If-None-Match"] ~= "" then 53 | return true 54 | end 55 | 56 | return false 57 | end 58 | _M.can_revalidate_locally = can_revalidate_locally 59 | 60 | 61 | -- True if the request conditions indicate that the response (res) can be served 62 | local function is_valid_locally(res) 63 | local req_h = ngx_req_get_headers() 64 | 65 | local res_lm = res.header["Last-Modified"] 66 | local req_ims = req_h["If-Modified-Since"] 67 | 68 | if res_lm and req_ims then 69 | local res_lm_parsed = ngx_parse_http_time(res_lm) 70 | local req_ims_parsed = ngx_parse_http_time(req_ims) 71 | 72 | if res_lm_parsed and req_ims_parsed then 73 | if res_lm_parsed <= req_ims_parsed then 74 | return true 75 | end 76 | end 77 | end 78 | 79 | local res_etag = res.header["Etag"] 80 | local req_inm = req_h["If-None-Match"] 81 | if res_etag and req_inm and res_etag == req_inm then 82 | return true 83 | end 84 | 85 | return false 86 | end 87 | _M.is_valid_locally = is_valid_locally 88 | 89 | 90 | return _M 91 | -------------------------------------------------------------------------------- /lib/ledge/worker.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local co_yield = coroutine.yield 3 | 4 | local ngx_get_phase = ngx.get_phase 5 | 6 | local tbl_copy_merge_defaults = require("ledge.util").table.copy_merge_defaults 7 | local fixed_field_metatable = require("ledge.util").mt.fixed_field_metatable 8 | 9 | 10 | local _M = { 11 | _VERSION = "2.3.0", 12 | } 13 | 14 | 15 | local defaults = setmetatable({ 16 | interval = 1, 17 | gc_queue_concurrency = 1, 18 | purge_queue_concurrency = 1, 19 | revalidate_queue_concurrency = 1, 20 | }, fixed_field_metatable) 21 | 22 | 23 | local function new(config) 24 | assert(ngx_get_phase() == "init_worker", 25 | "attempt to create ledge worker outside of the init_worker phase") 26 | 27 | -- Take config by value and merge with defaults 28 | local config = tbl_copy_merge_defaults(config, defaults) 29 | return setmetatable({ config = config }, { 30 | __index = _M, 31 | }) 32 | end 33 | _M.new = new 34 | 35 | 36 | local function run(self) 37 | assert(ngx_get_phase() == "init_worker", 38 | "attempt to run ledge worker outside of the init_worker phase") 39 | 40 | local ledge = require("ledge") 41 | 42 | local ql_worker = assert(require("resty.qless.worker").new({ 43 | get_redis_client = ledge.create_qless_connection, 44 | close_redis_client = ledge.close_redis_connection 45 | })) 46 | 47 | -- Runs around job exectution, to instantiate necessary connections 48 | ql_worker.middleware = function(job) 49 | job.redis = ledge.create_redis_connection() 50 | 51 | co_yield() -- Perform the job 52 | 53 | ledge.close_redis_connection(job.redis) 54 | end 55 | 56 | -- Start a worker for each fo the queues 57 | 58 | assert(ql_worker:start({ 59 | interval = self.config.interval, 60 | concurrency = self.config.gc_queue_concurrency, 61 | reserver = "ordered", 62 | queues = { "ledge_gc" }, 63 | })) 64 | 65 | assert(ql_worker:start({ 66 | interval = self.config.interval, 67 | concurrency = self.config.purge_queue_concurrency, 68 | reserver = "ordered", 69 | queues = { "ledge_purge" }, 70 | })) 71 | 72 | assert(ql_worker:start({ 73 | interval = self.config.interval or 1, 74 | concurrency = self.config.revalidate_queue_concurrency, 75 | reserver = "ordered", 76 | queues = { "ledge_revalidate" }, 77 | })) 78 | 79 | return true 80 | end 81 | _M.run = run 82 | 83 | 84 | return setmetatable(_M, fixed_field_metatable) 85 | -------------------------------------------------------------------------------- /migrations/1.26-1.27.lua: -------------------------------------------------------------------------------- 1 | local redis_connector = require("resty.redis.connector").new() 2 | local math_floor = math.floor 3 | local math_ceil = math.ceil 4 | local ffi = require "ffi" 5 | local ffi_cdef = ffi.cdef 6 | local ffi_new = ffi.new 7 | local ffi_string = ffi.string 8 | local C = ffi.C 9 | 10 | ffi_cdef[[ 11 | typedef unsigned char u_char; 12 | u_char * ngx_hex_dump(u_char *dst, const u_char *src, size_t len); 13 | int RAND_pseudo_bytes(u_char *buf, int num); 14 | ]] 15 | 16 | 17 | local function random_hex(len) 18 | local len = math_floor(len / 2) 19 | 20 | local bytes = ffi_new("uint8_t[?]", len) 21 | C.RAND_pseudo_bytes(bytes, len) 22 | if not bytes then 23 | ngx_log(ngx_ERR, "error getting random bytes via FFI") 24 | return nil 25 | end 26 | 27 | local hex = ffi_new("uint8_t[?]", len * 2) 28 | C.ngx_hex_dump(hex, bytes, len) 29 | return ffi_string(hex, len * 2) 30 | end 31 | 32 | 33 | function delete(redis, cache_key, entities) 34 | redis:multi() 35 | -- Entities list is intact, so delete them too 36 | for _, entity in ipairs(entities) do 37 | delete_entity(redis, cache_key .. "::entities", entity) 38 | end 39 | 40 | local keys = { 41 | cache_key .. "::key", 42 | cache_key .. "::memused", 43 | cache_key .. "::entities", 44 | } 45 | redis:del(unpack(keys)) 46 | return redis:exec() 47 | end 48 | 49 | 50 | function delete_entity(redis, set, entity) 51 | local keys = { 52 | entity, 53 | entity .. ":reval_req_headers", 54 | entity .. ":reval_params", 55 | entity .. ":headers", 56 | entity .. ":body", 57 | entity .. ":body_esi", 58 | } 59 | local res, err = redis:del(unpack(keys)) 60 | 61 | -- Remove from the entities set 62 | local res, err = redis:zrem(set, entity) 63 | end 64 | 65 | 66 | function delete_old_entities(redis, set, members, current_entity) 67 | for _, entity in ipairs(members) do 68 | if entity ~= current_entity then 69 | delete_entity(redis, set, entity) 70 | end 71 | end 72 | end 73 | 74 | 75 | function scan(cursor, redis) 76 | local res, err = redis:scan( 77 | cursor, 78 | "MATCH", "ledge:cache:*::key", -- We use the "main" key to single out a cache entry 79 | "COUNT", 100 80 | ) 81 | 82 | if not res or res == ngx_null then 83 | return nil, "SCAN error: " .. tostring(err) 84 | else 85 | for _,key in ipairs(res[2]) do 86 | -- Strip the "main" suffix to find the cache key 87 | local cache_key = string.sub(key, 1, -(string.len("::key") + 1)) 88 | local skip = false 89 | 90 | local entity, entity_err = redis:get(cache_key .. "::key") 91 | if entity_err == nil then entity = nil end -- prevent concatentation error 92 | local memused, memused_err = redis:get(cache_key .. "::memused") 93 | local score = redis:zscore(cache_key .. "::entities", cache_key .. "::" .. (entity or "")) 94 | local entity_count = redis:zcard(cache_key .. "::entities") 95 | local entity_members = redis:zrange(cache_key .. "::entities", 0, -1) 96 | 97 | for _, val in ipairs({ entity, memused, score, entity_count, entity_members }) do 98 | if not val or val == ngx.null then 99 | -- If we're missing something we need (likely evicted) -- delete this key 100 | if delete(redis, cache_key, entity_members) then 101 | keys_deleted = keys_deleted + 1 102 | else 103 | keys_failed = keys_failed + 1 104 | end 105 | skip = true 106 | end 107 | end 108 | 109 | -- Watch the main key - if it gets created by real traffic from here on in then 110 | -- the transaction will simply fail. 111 | local res = redis:watch(cache_key .. "::main") 112 | 113 | -- Find out if real traffic already created this cache entry 114 | local new_entity = redis:hget(cache_key .. "::main", "entity") 115 | if new_entity and new_entity ~= ngx.null then 116 | -- The old entities refs will still exist, so clean them up 117 | delete_old_entities(redis, cache_key .. "::entities", entity_members, new_entity) 118 | keys_processed = keys_processed + 1 119 | skip = true 120 | end 121 | 122 | if not skip then 123 | -- Start transaction 124 | redis:multi() 125 | 126 | -- Move main entity to main key 127 | local ok, err = redis:rename(cache_key .. "::" .. entity, cache_key .. "::main") 128 | 129 | -- Rename headers etc 130 | for _, k in ipairs({ "headers", "reval_req_headers", "reval_params" }) do 131 | local ok, err = redis:rename( 132 | cache_key .. "::" .. entity .. ":" .. k, 133 | cache_key .. "::" .. k 134 | ) 135 | end 136 | 137 | -- Create a new entity id and rename the live entity to it 138 | local new_entity_id = random_hex(32) 139 | for _, k in ipairs({ "body", "body_esi" }) do 140 | local ok, err = redis:rename( 141 | cache_key .. "::" .. entity .. ":" .. k, 142 | "ledge:entity:" .. new_entity_id .. ":" .. k 143 | ) 144 | end 145 | 146 | -- Add the entity to the entities set 147 | local res, err = redis:zadd(cache_key .. "::entities", score, new_entity_id) 148 | 149 | -- Remove the old form 150 | local res, err = redis:zrem( 151 | cache_key .. "::entities", 152 | cache_key .. "::" .. entity 153 | ) 154 | 155 | -- Add the live entity pointer to the main hash, and delete the old pointer 156 | local ok, err = redis:hset(cache_key .. "::main", "entity", new_entity_id) 157 | local ok, err = redis:del(cache_key .. "::key") 158 | 159 | -- Add the memused to the main hash, and delete the old key 160 | local ok, err = redis:hset(cache_key .. "::main", "memused", memused) 161 | local ok, err = redis:del(cache_key .. "::memused") 162 | 163 | -- Delete entities scheduled for GC but will fail on new codebase 164 | delete_old_entities(redis, cache_key .. "::entities", entity_members, new_entity_id) 165 | 166 | local res, err = redis:exec() 167 | if not res or res == ngx.null then 168 | ngx.say("transaction failed") 169 | -- Something went wrong, lets try and delete this cache entry 170 | if delete(redis, cache_key, entity_members) then 171 | keys_deleted = keys_deleted + 1 172 | else 173 | keys_failed = keys_failed + 1 174 | end 175 | else 176 | keys_processed = keys_processed + 1 177 | end 178 | end 179 | end 180 | end 181 | 182 | local cursor = tonumber(res[1]) 183 | if cursor > 0 then 184 | -- If we have a valid cursor, recurse to move on. 185 | return scan(cursor, redis) 186 | end 187 | 188 | return true 189 | end 190 | 191 | local dsn = arg[1] 192 | if not dsn then 193 | ngx.say("Please provide a Redis Connector DSN as the first argument, in the form: redis://[PASSWORD@]HOST:PORT/DB") 194 | else 195 | local redis, err = redis_connector:connect{ url = dsn } 196 | if not redis then 197 | ngx.say("Could not connect to Redis with DSN: ", dsn, " - ", err) 198 | return 199 | end 200 | 201 | keys_processed = 0 202 | keys_deleted = 0 203 | keys_failed = 0 204 | 205 | ngx.say("Migrating Ledge data structure from v1.26 to v1.27\n") 206 | 207 | local res, err = scan(0, redis) 208 | if not res or res == ngx.null then 209 | ngx.say("Faied to scan keyspace: ", err) 210 | else 211 | ngx.say("> ", keys_processed .. " cache entries successfully updated") 212 | ngx.say("> ", keys_deleted .. " incomplete / broken cache entries cleaned up") 213 | ngx.say("> ", keys_failed .. " failures\n") 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /t/01-unit/esi.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: split_esi_token 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /t { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | local esi = assert(require("ledge.esi"), 20 | "module should load without errors") 21 | 22 | local capability, version = esi.split_esi_token("ESI/1.0") 23 | assert(capability == "ESI" and version == 1.0, 24 | "capability and version should be returned") 25 | 26 | local ok, cap, ver = pcall(esi.split_esi_token) 27 | assert(ok and not cap and not ver, 28 | "split_esi_token without a token should safely return nil") 29 | } 30 | } 31 | 32 | --- request 33 | GET /t 34 | --- no_error_log 35 | [error] 36 | 37 | 38 | === TEST 2: esi_capabilities 39 | --- http_config eval: $::HttpConfig 40 | --- config 41 | location /t { 42 | rewrite ^(.*)_prx$ $1 break; 43 | content_by_lua_block { 44 | assert(require("ledge.esi").esi_capabilities() == "ESI/1.0", 45 | "capabilities should be ESI/1.0") 46 | } 47 | } 48 | 49 | --- request 50 | GET /t 51 | --- no_error_log 52 | [error] 53 | 54 | 55 | === TEST 3: choose_esi_processor 56 | --- http_config eval: $::HttpConfig 57 | --- config 58 | location /t { 59 | rewrite ^(.*)_prx$ $1 break; 60 | content_by_lua_block { 61 | -- handler stub 62 | local handler = { 63 | response = { 64 | header = { 65 | ["Surrogate-Control"] = [[content=ESI/1.0]], 66 | } 67 | } 68 | } 69 | 70 | local processor = require("ledge.esi").choose_esi_processor(handler) 71 | 72 | assert(next(processor), "processor should be a table") 73 | 74 | assert(type(processor.get_scan_filter) == "function", 75 | "get_scan_filter should be a function") 76 | 77 | assert(type(processor.get_process_filter) == "function", 78 | "get_process_filter should be a function") 79 | 80 | -- unknown processor 81 | handler.response.header["Surrogate-Control"] = [[content=FOO/2.0]] 82 | 83 | assert(not require("ledge.esi").choose_esi_processor(handler), 84 | "processor should be nil") 85 | } 86 | } 87 | 88 | --- request 89 | GET /t 90 | --- no_error_log 91 | [error] 92 | 93 | 94 | === TEST 4: is_allowed_content_type 95 | --- http_config eval: $::HttpConfig 96 | --- config 97 | location /t { 98 | rewrite ^(.*)_prx$ $1 break; 99 | content_by_lua_block { 100 | local res = { 101 | header = { 102 | ["Content-Type"] = "text/html", 103 | } 104 | } 105 | 106 | local allowed_types = { 107 | "text/html" 108 | } 109 | 110 | local is_allowed_content_type = 111 | require("ledge.esi").is_allowed_content_type 112 | 113 | assert(is_allowed_content_type(res, allowed_types), 114 | "text/html is allowed") 115 | 116 | res.header["Content-Type"] = "text/ht" 117 | assert(not is_allowed_content_type(res, allowed_types), 118 | "text/ht is not allowed") 119 | 120 | res.header["Content-Type"] = "text/html_foo" 121 | assert(not is_allowed_content_type(res, allowed_types), 122 | "text/html_foo is not allowed") 123 | 124 | res.header["Content-Type"] = "text/html; charset=utf-8" 125 | assert(is_allowed_content_type(res, allowed_types), 126 | "text/html; charset=utf-8 is allowed") 127 | 128 | res.header["Content-Type"] = "text/json" 129 | assert(not is_allowed_content_type(res, allowed_types), 130 | "text/json is not allowed") 131 | 132 | 133 | table.insert(allowed_types, "text/json") 134 | assert(is_allowed_content_type(res, allowed_types), 135 | "text/json is allowed") 136 | 137 | } 138 | } 139 | 140 | --- request 141 | GET /t 142 | --- no_error_log 143 | [error] 144 | 145 | 146 | === TEST 5: can_delegate_to_surrogate 147 | --- http_config eval: $::HttpConfig 148 | --- config 149 | location /t { 150 | rewrite ^(.*)_prx$ $1 break; 151 | content_by_lua_block { 152 | local can_delegate_to_surrogate = 153 | require("ledge.esi").can_delegate_to_surrogate 154 | 155 | assert(not can_delegate_to_surrogate(true, "ESI/1.0"), 156 | "cannot delegate without capability") 157 | 158 | ngx.req.set_header("Surrogate-Capability", "localhost=ESI/1.0") 159 | 160 | assert(can_delegate_to_surrogate(true, "ESI/1.0"), 161 | "can delegate with capability") 162 | 163 | assert(not can_delegate_to_surrogate(true, "FOO/1.2"), 164 | "cannnot delegate to non-supported capability") 165 | 166 | assert(can_delegate_to_surrogate({ "127.0.0.1" }, "ESI/1.0" ), 167 | "can delegate to loopback with capability") 168 | 169 | assert(not can_delegate_to_surrogate({ "127.0.0.2" }, "ESI/1.0" ), 170 | "cant delegate to non-loopback with capability") 171 | 172 | } 173 | } 174 | --- request 175 | GET /t 176 | --- no_error_log 177 | [error] 178 | 179 | 180 | === TEST 6: filter_esi_args 181 | --- http_config eval: $::HttpConfig 182 | --- config 183 | location /t { 184 | rewrite ^(.*)_prx$ $1 break; 185 | content_by_lua_block { 186 | local handler = require("ledge").create_handler() 187 | 188 | local filter_esi_args = require("ledge.esi").filter_esi_args 189 | 190 | local args = ngx.req.get_uri_args() 191 | assert(args.a == "1" and args.esi_foo == "bar bar" and args.b == "2", 192 | "request args should be intact") 193 | 194 | filter_esi_args(handler) 195 | 196 | local args = ngx.req.get_uri_args() 197 | assert(args.a == "1" and not args.esi_foo and args.b == "2", 198 | "esi args should be removed") 199 | 200 | assert(ngx.ctx.__ledge_esi_args.foo == "bar bar", 201 | "esi args should have foo: bar bar") 202 | 203 | assert(tostring(ngx.ctx.__ledge_esi_args) == "esi_foo=bar%20bar", 204 | "esi_args as a string should be foo=bar%20bar") 205 | 206 | } 207 | } 208 | --- request 209 | GET /t?a=1&esi_foo=bar+bar&b=2 210 | --- no_error_log 211 | [error] 212 | -------------------------------------------------------------------------------- /t/01-unit/events.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Bind and emit 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /t { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | local handler = require("ledge").create_handler() 20 | 21 | local ok, err = handler:bind("non_event", function(arg) end) 22 | assert(not ok and err == "no such event: non_event", 23 | "err should be set") 24 | 25 | local function say(arg) ngx.say(arg) end 26 | 27 | local ok, err = handler:bind("after_cache_read", say) 28 | assert(ok and not err, "bind should return positively") 29 | 30 | local ok, err = pcall(handler.emit, handler, "non_event") 31 | assert(not ok and err == "attempt to emit non existent event: non_event", 32 | "emit should fail with non-event") 33 | 34 | -- Bind and emit all events 35 | handler:bind("before_upstream_request", say) 36 | handler:bind("after_upstream_request", say) 37 | handler:bind("before_save", say) 38 | handler:bind("before_save_revalidation_data", say) 39 | handler:bind("before_serve", say) 40 | handler:bind("before_esi_include_request", say) 41 | handler:bind("before_vary_selection", say) 42 | 43 | handler:emit("after_cache_read", "after_cache_read") 44 | handler:emit("before_upstream_request", "before_upstream_request") 45 | handler:emit("after_upstream_request", "after_upstream_request") 46 | handler:emit("before_save", "before_save") 47 | handler:emit("before_save_revalidation_data", "before_save_revalidation_data") 48 | handler:emit("before_serve", "before_serve") 49 | handler:emit("before_esi_include_request", "before_esi_include_request") 50 | handler:emit("before_vary_selection", "before_vary_selection") 51 | } 52 | } 53 | 54 | --- request 55 | GET /t 56 | --- response_body 57 | after_cache_read 58 | before_upstream_request 59 | after_upstream_request 60 | before_save 61 | before_save_revalidation_data 62 | before_serve 63 | before_esi_include_request 64 | before_vary_selection 65 | --- error_log 66 | no such event: non_event 67 | 68 | 69 | === TEST 2: Bind multiple functions to an event 70 | --- http_config eval: $::HttpConfig 71 | --- config 72 | location /t { 73 | rewrite ^(.*)_prx$ $1 break; 74 | content_by_lua_block { 75 | local handler = require("ledge").create_handler() 76 | 77 | for i = 1, 3 do 78 | handler:bind("after_cache_read", function() 79 | ngx.say("function ", i) 80 | end) 81 | end 82 | 83 | handler:emit("after_cache_read") 84 | } 85 | } 86 | --- request 87 | GET /t 88 | --- response_body 89 | function 1 90 | function 2 91 | function 3 92 | --- no_error_log 93 | [error] 94 | 95 | 96 | === TEST 3: Default binds 97 | --- http_config eval 98 | qq { 99 | lua_package_path "./lib/?.lua;../lua-resty-redis-connector/lib/?.lua;../lua-resty-qless/lib/?.lua;../lua-resty-http/lib/?.lua;../lua-ffi-zlib/lib/?.lua;;"; 100 | 101 | init_by_lua_block { 102 | if $LedgeEnv::test_coverage == 1 then 103 | require("luacov.runner").init() 104 | end 105 | 106 | require("ledge").bind("after_cache_read", function(arg) 107 | ngx.say("default 1: ", arg) 108 | end) 109 | 110 | require("ledge").bind("after_cache_read", function(arg) 111 | ngx.say("default 2: ", arg) 112 | end) 113 | } 114 | } 115 | --- config 116 | location /t { 117 | rewrite ^(.*)_prx$ $1 break; 118 | content_by_lua_block { 119 | local ledge = require("ledge") 120 | local ok, err = pcall(ledge.bind, "after_cache_read", function(arg) 121 | ngx.say(arg) 122 | end) 123 | 124 | assert(not ok and string.find(err, "attempt to call bind outside the 'init' phase"), err) 125 | 126 | local handler = require("ledge").create_handler() 127 | 128 | handler:bind("after_cache_read", function(arg) 129 | ngx.say("instance 1: ", arg) 130 | end) 131 | 132 | handler:bind("after_cache_read", function(arg) 133 | ngx.say("instance 2: ", arg) 134 | end) 135 | 136 | handler:emit("after_cache_read", "foo") 137 | } 138 | } 139 | --- request 140 | GET /t 141 | --- response_body 142 | default 1: foo 143 | default 2: foo 144 | instance 1: foo 145 | instance 2: foo 146 | --- no_error_log 147 | [error] 148 | -------------------------------------------------------------------------------- /t/01-unit/ledge.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(extra_lua_config => qq{ 7 | qless_db = $LedgeEnv::redis_qless_database 8 | }); 9 | 10 | no_long_string(); 11 | no_diff(); 12 | run_tests(); 13 | 14 | __DATA__ 15 | === TEST 1: Load module without errors. 16 | --- http_config eval: $::HttpConfig 17 | --- config 18 | location /ledge_1 { 19 | content_by_lua_block { 20 | assert(require("ledge"), "module should load without errors") 21 | } 22 | } 23 | --- request 24 | GET /ledge_1 25 | --- no_error_log 26 | [error] 27 | 28 | 29 | === TEST 2: Module cannot be externally modified 30 | --- http_config eval: $::HttpConfig 31 | --- config 32 | location /ledge_2 { 33 | content_by_lua_block { 34 | local ledge = require("ledge") 35 | local ok, err = pcall(function() 36 | ledge.foo = "bar" 37 | end) 38 | assert(string.find(err, "attempt to create new field foo"), 39 | "error 'field foo does not exist' should be thrown") 40 | } 41 | } 42 | --- request 43 | GET /ledge_2 44 | --- no_error_log 45 | [error] 46 | 47 | 48 | === TEST 3: Non existent params cannot be set 49 | --- http_config eval 50 | qq { 51 | lua_package_path "./lib/?.lua;../lua-resty-redis-connector/lib/?.lua;../lua-resty-qless/lib/?.lua;;"; 52 | init_by_lua_block { 53 | if $LedgeEnv::test_coverage == 1 then 54 | require("luacov.runner").init() 55 | end 56 | local ok, err = pcall(require("ledge").configure, { foo = "bar" }) 57 | assert(string.find(err, "field foo does not exist"), 58 | "error 'field foo does not exist' should be thrown") 59 | } 60 | } 61 | --- config 62 | location /ledge_3 { 63 | echo "OK"; 64 | } 65 | --- request 66 | GET /ledge_3 67 | --- no_error_log 68 | [error] 69 | 70 | 71 | === TEST 4: Params cannot be set outside of init 72 | --- http_config eval: $::HttpConfig 73 | --- config 74 | location /ledge_4 { 75 | content_by_lua_block { 76 | require("ledge").configure({ qless_db = 4 }) 77 | } 78 | } 79 | --- request 80 | GET /ledge_4 81 | --- error_code: 500 82 | --- error_log 83 | attempt to call configure outside the 'init' phase 84 | 85 | 86 | === TEST 5: Create redis connection 87 | --- http_config eval: $::HttpConfig 88 | --- config 89 | location /ledge_5 { 90 | content_by_lua_block { 91 | local redis = assert(require("ledge").create_redis_connection(), 92 | "create_redis_connection() should return positively") 93 | 94 | assert(redis:set("ledge_5:cat", "dog"), 95 | "redis:set() should return positively") 96 | 97 | local val, err = redis:get("ledge_5:cat") 98 | ngx.say(val) 99 | 100 | assert(require("ledge").close_redis_connection(redis), 101 | "close_redis_connection() should return positively") 102 | } 103 | } 104 | --- request 105 | GET /ledge_5 106 | --- response_body 107 | dog 108 | --- no_error_log 109 | [error] 110 | 111 | 112 | === TEST 6: Create bad redis connection 113 | --- http_config eval 114 | qq{ 115 | lua_package_path "./lib/?.lua;../lua-resty-redis-connector/lib/?.lua;../lua-resty-qless/lib/?.lua;;"; 116 | 117 | init_by_lua_block { 118 | if $LedgeEnv::test_coverage == 1 then 119 | require("luacov.runner").init() 120 | end 121 | require("ledge").configure({ 122 | redis_connector_params = { 123 | port = 0, -- bad port 124 | }, 125 | }) 126 | } 127 | } 128 | --- config 129 | location /ledge_6 { 130 | content_by_lua_block { 131 | assert(not require("ledge").create_redis_connection(), 132 | "create_redis_connection() should return negatively") 133 | } 134 | } 135 | --- request 136 | GET /ledge_6 137 | --- error_log eval: qr/connect\(\)( to 127.0.0.1:0)? failed/ 138 | 139 | 140 | === TEST 7: Create storage connection 141 | --- http_config eval: $::HttpConfig 142 | --- config 143 | location /ledge_7 { 144 | content_by_lua_block { 145 | local storage = assert(require("ledge").create_storage_connection(), 146 | "create_storage_connection should return positively") 147 | 148 | ngx.say(storage:exists("ledge_7:123456")) 149 | 150 | assert(require("ledge").close_storage_connection(storage), 151 | "close_storage_connection() should return positively") 152 | } 153 | } 154 | --- request 155 | GET /ledge_7 156 | --- response_body 157 | false 158 | --- no_error_log 159 | [error] 160 | 161 | 162 | === TEST 8: Create bad storage connection 163 | --- http_config eval 164 | qq{ 165 | lua_package_path "./lib/?.lua;../lua-resty-redis-connector/lib/?.lua;../lua-resty-qless/lib/?.lua;;"; 166 | 167 | init_by_lua_block { 168 | if $LedgeEnv::test_coverage == 1 then 169 | require("luacov.runner").init() 170 | end 171 | require("ledge").set_handler_defaults({ 172 | storage_driver_config = { 173 | redis_connector_params = { 174 | port = 0, 175 | }, 176 | } 177 | }) 178 | } 179 | } 180 | --- config 181 | location /ledge_8 { 182 | content_by_lua_block { 183 | assert(not require("ledge").create_storage_connection(), 184 | "create_storage_connection() should return negatively") 185 | } 186 | } 187 | --- request 188 | GET /ledge_8 189 | --- error_log eval: qr/connect\(\)( to 127.0.0.1:0)? failed/ 190 | 191 | 192 | === TEST 9: Create qless connection 193 | --- http_config eval: $::HttpConfig 194 | --- config 195 | location /ledge_9 { 196 | content_by_lua_block { 197 | local redis = assert(require("ledge").create_qless_connection(), 198 | "create_qless_connection() should return positively") 199 | 200 | assert(redis:set("ledge_9:cat", "dog"), 201 | "redis:set() should return positively") 202 | 203 | assert(require("ledge").close_redis_connection(redis), 204 | "close_redis_connection() should return positively") 205 | 206 | local redis = require("ledge").create_redis_connection() 207 | assert(redis:select(qless_db), "select() shoudl return positively") 208 | 209 | local val, err = redis:get("ledge_9:cat") 210 | ngx.say(val) 211 | 212 | assert(require("ledge").close_redis_connection(redis), 213 | "close_redis_connection() should return positively") 214 | } 215 | } 216 | --- request 217 | GET /ledge_9 218 | --- response_body 219 | dog 220 | --- no_error_log 221 | [error] 222 | 223 | === TEST 10: Bad redis-connector params are caught 224 | --- http_config eval 225 | qq{ 226 | lua_package_path "./lib/?.lua;../lua-resty-redis-connector/lib/?.lua;../lua-resty-qless/lib/?.lua;;"; 227 | 228 | init_by_lua_block { 229 | if $LedgeEnv::test_coverage == 1 then 230 | require("luacov.runner").init() 231 | end 232 | require("ledge").configure({ 233 | redis_connector_params = { 234 | bad_time = true 235 | }, 236 | }) 237 | require("ledge").set_handler_defaults({ 238 | storage_driver_config = { 239 | redis_connector_params = { 240 | bad_time2 = true 241 | }, 242 | } 243 | }) 244 | } 245 | } 246 | --- config 247 | location /ledge_10 { 248 | content_by_lua_block { 249 | local ok, err = require("ledge").create_redis_connection() 250 | assert(ok == nil and err ~= nil, 251 | "create_redis_connection() should return negatively with error") 252 | 253 | local ok, err = require("ledge").create_storage_connection() 254 | assert(ok == nil and err ~= nil, 255 | "create_storage_connection() should return negatively with error") 256 | 257 | local ok, err = require("ledge").create_qless_connection() 258 | assert(ok == nil and err ~= nil, 259 | "create_qless_connection() should return negatively with error") 260 | 261 | local ok, err = require("ledge").create_redis_slave_connection() 262 | assert(ok == nil and err ~= nil, 263 | "create_redis_slave_connection() should return negatively with error") 264 | 265 | -- Test broken redis-connector params are caught when closing redis somehow 266 | local ok, err = require("ledge").close_redis_connection({dummy = true}) 267 | assert(ok == nil and err ~= nil, 268 | "close_redis_connection() should return negatively with error") 269 | 270 | -- Test trying to close a non-existent redis instance 271 | local ok, err = require("ledge").close_redis_connection({}) 272 | assert(ok == nil and err ~= nil, 273 | "close_redis_connection() should return negatively with error") 274 | 275 | ngx.say("OK") 276 | } 277 | } 278 | --- request 279 | GET /ledge_10 280 | --- error_code: 200 281 | --- response_body 282 | OK 283 | 284 | === TEST 11: Closing an empty redis instance 285 | --- http_config eval: $::HttpConfig 286 | --- config 287 | location /ledge_11 { 288 | content_by_lua_block { 289 | local ok, err = require("ledge").close_redis_connection({}) 290 | assert(ok == nil, 291 | "close_redis_connection() should return negatively") 292 | 293 | ngx.say("OK") 294 | } 295 | } 296 | --- request 297 | GET /ledge_11 298 | --- error_code: 200 299 | --- response_body 300 | OK 301 | -------------------------------------------------------------------------------- /t/01-unit/request.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(extra_lua_config => qq{ 7 | TEST_NGINX_HOST = "$LedgeEnv::nginx_host" 8 | TEST_NGINX_PORT = $LedgeEnv::nginx_port 9 | }); 10 | 11 | no_long_string(); 12 | no_diff(); 13 | run_tests(); 14 | 15 | __DATA__ 16 | === TEST 1: Purge mode 17 | --- http_config eval: $::HttpConfig 18 | --- config 19 | location /t { 20 | content_by_lua_block { 21 | local req_purge_mode = assert(require("ledge.request").purge_mode, 22 | "request module should load without errors") 23 | 24 | local mode = ngx.req.get_uri_args()["p"] 25 | assert(req_purge_mode() == mode, 26 | "req_purge_mode should equal " .. mode) 27 | 28 | 29 | } 30 | } 31 | --- more_headers eval 32 | [ 33 | "X-Purge: delete", 34 | "X-Purge: revalidate", 35 | "X-Purge: invalidate", 36 | "" 37 | ] 38 | --- request eval 39 | [ 40 | "GET /t?p=delete", 41 | "GET /t?p=revalidate", 42 | "GET /t?p=invalidate", 43 | "GET /t?p=invalidate" 44 | ] 45 | --- no_error_log 46 | [error] 47 | 48 | 49 | === TEST 2: relative_uri - spaces encoded 50 | --- http_config eval: $::HttpConfig 51 | --- config 52 | location /t { 53 | content_by_lua_block { 54 | local http = require("resty.http").new() 55 | http:connect( 56 | TEST_NGINX_HOST, TEST_NGINX_PORT 57 | ) 58 | 59 | local res, err = http:request({ 60 | path = "/t with spaces", 61 | }) 62 | 63 | http:close() 64 | } 65 | 66 | } 67 | 68 | location "/t with spaces" { 69 | content_by_lua_block { 70 | local req_relative_uri = require("ledge.request").relative_uri 71 | assert(req_relative_uri() == "/t%20with%20spaces", 72 | "uri should have spaces encoded") 73 | } 74 | } 75 | --- request 76 | GET /t 77 | --- no_error_log 78 | [error] 79 | 80 | 81 | === TEST 3: relative_uri - Percent encode encoded CRLF 82 | http://resources.infosecinstitute.com/http-response-splitting-attack 83 | --- http_config eval: $::HttpConfig 84 | --- config 85 | location /t { 86 | content_by_lua_block { 87 | local http = require("resty.http").new() 88 | http:connect( 89 | TEST_NGINX_HOST, TEST_NGINX_PORT 90 | ) 91 | 92 | local res, err = http:request({ 93 | path = "/t_crlf_encoded_%250d%250A", 94 | }) 95 | 96 | http:close() 97 | } 98 | 99 | } 100 | 101 | location /t_crlf_encoded_ { 102 | content_by_lua_block { 103 | local req_relative_uri = require("ledge.request").relative_uri 104 | assert(req_relative_uri() == "/t_crlf_encoded_%250D%250A", 105 | "encoded crlf in uri should be escaped") 106 | } 107 | } 108 | --- request 109 | GET /t 110 | --- no_error_log 111 | [error] 112 | 113 | 114 | === TEST 4: full_uri 115 | --- http_config eval: $::HttpConfig 116 | --- config 117 | location /t { 118 | content_by_lua_block { 119 | local full_uri = require("ledge.request").full_uri 120 | assert(full_uri() == "http://localhost/t", 121 | "full_uri should be http://localhost/t") 122 | } 123 | 124 | } 125 | --- request 126 | GET /t 127 | --- no_error_log 128 | [error] 129 | 130 | 131 | === TEST 5: accepts_cache 132 | --- http_config eval: $::HttpConfig 133 | --- config 134 | location /t { 135 | content_by_lua_block { 136 | local accepts_cache = require("ledge.request").accepts_cache 137 | assert(tostring(accepts_cache()) == ngx.req.get_uri_args().c, 138 | "accepts_cache should be " .. ngx.req.get_uri_args().c) 139 | } 140 | 141 | } 142 | --- more_headers eval 143 | [ 144 | "Cache-Control: no-cache", 145 | "Cache-Control: no-store", 146 | "Pragma: no-cache", 147 | "Cache-Control: no-cache, max-age=60", 148 | "Cache-Control: s-maxage=20, no-cache", 149 | "", 150 | "Cache-Control: max-age=60", 151 | "Cache-Control: max-age=0", 152 | "Pragma: cache", 153 | "Cache-Control: no-cachey", 154 | ] 155 | --- request eval 156 | [ 157 | "GET /t?c=false", 158 | "GET /t?c=false", 159 | "GET /t?c=false", 160 | "GET /t?c=false", 161 | "GET /t?c=false", 162 | "GET /t?c=true", 163 | "GET /t?c=true", 164 | "GET /t?c=true", 165 | "GET /t?c=true", 166 | "GET /t?c=true" 167 | ] 168 | --- no_error_log 169 | [error] 170 | -------------------------------------------------------------------------------- /t/01-unit/stale.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: can_serve_stale 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /t { 17 | content_by_lua_block { 18 | local can_serve_stale = require("ledge.stale").can_serve_stale 19 | 20 | local args = ngx.req.get_uri_args() 21 | local res = { 22 | header = { 23 | ["Cache-Control"] = args.rescc, 24 | }, 25 | remaining_ttl = tonumber(args.ttl), 26 | } 27 | 28 | assert(tostring(can_serve_stale(res)) == ngx.req.get_uri_args().stale, 29 | "can_serve_stale should be " .. ngx.req.get_uri_args().stale) 30 | 31 | } 32 | } 33 | --- more_headers eval 34 | [ 35 | "", 36 | "Cache-Control: max-stale=60", 37 | "Cache-Control: max-stale=60", 38 | "Cache-Control: max-stale=60", 39 | "Cache-Control: max-stale=9", 40 | ] 41 | --- request eval 42 | [ 43 | "GET /t?rescc=&ttl=0&stale=false", 44 | "GET /t?rescc=&ttl=0&stale=true", 45 | "GET /t?rescc=must-revalidate&ttl=0&stale=false", 46 | "GET /t?rescc=proxy-revalidate&ttl=0&stale=false", 47 | "GET /t?rescc=&ttl=-10&stale=false", 48 | ] 49 | --- no_error_log 50 | [error] 51 | 52 | 53 | === TEST 2: verify_stale_conditions 54 | --- http_config eval: $::HttpConfig 55 | --- config 56 | location /t { 57 | content_by_lua_block { 58 | local verify_stale_conditions = 59 | require("ledge.stale").verify_stale_conditions 60 | 61 | local args = ngx.req.get_uri_args() 62 | 63 | local res = { 64 | header = { 65 | ["Cache-Control"] = ngx.req.get_headers().x_res_cache_control, 66 | ["Age"] = ngx.req.get_headers().x_res_age, 67 | }, 68 | remaining_ttl = tonumber(args.ttl), 69 | } 70 | 71 | local token = ngx.req.get_uri_args().token 72 | local stale = ngx.req.get_uri_args().stale 73 | assert(tostring(verify_stale_conditions(res, token)) == stale, 74 | "verify_stale_conditions should be " .. stale) 75 | 76 | if token == "stale-while-revalidate" then 77 | 78 | local can_serve_stale_while_revalidate = 79 | require("ledge.stale").can_serve_stale_while_revalidate 80 | 81 | assert(tostring(can_serve_stale_while_revalidate(res)) == stale, 82 | "can_serve_stale_while_revalidate should be " .. stale) 83 | elseif token == "stale-if-error" then 84 | 85 | local can_serve_stale_if_error = 86 | require("ledge.stale").can_serve_stale_if_error 87 | 88 | assert(tostring(can_serve_stale_if_error(res)) == stale, 89 | "can_serve_stale_if_error should be " .. stale) 90 | end 91 | 92 | } 93 | } 94 | --- more_headers eval 95 | [ 96 | "", 97 | "Cache-Control: stale-while-revalidate=60", 98 | "X-Res-Cache-Control: stale-while-revalidate=60", 99 | "Cache-Control: min-fresh=10 100 | X-Res-Cache-Control: stale-while-revalidate=60", 101 | "Cache-Control: max-age=10, stale-while-revalidate=60 102 | X-Res-Age: 5", 103 | "Cache-Control: max-age=4, stale-while-revalidate=60 104 | X-Res-Age: 5", 105 | "Cache-Control: max-stale=10, stale-while-revalidate=60", 106 | "Cache-Control: max-stale=60, stale-while-revalidate=60", 107 | ] 108 | --- request eval 109 | [ 110 | "GET /t?token=stale-while-revalidate&stale=false", 111 | "GET /t?token=stale-while-revalidate&stale=true", 112 | "GET /t?token=stale-while-revalidate&stale=true", 113 | "GET /t?token=stale-while-revalidate&stale=false", 114 | "GET /t?token=stale-while-revalidate&stale=true", 115 | "GET /t?token=stale-while-revalidate&stale=false", 116 | "GET /t?token=stale-while-revalidate&stale=false", 117 | "GET /t?token=stale-while-revalidate&stale=true", 118 | ] 119 | --- no_error_log 120 | [error] 121 | -------------------------------------------------------------------------------- /t/01-unit/state_machine.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Load module 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /t { 17 | content_by_lua_block { 18 | assert(require("ledge.state_machine"), 19 | "state machine module should include without error") 20 | 21 | assert(require("ledge.state_machine.events"), 22 | "events module should include without error") 23 | 24 | assert(require("ledge.state_machine.pre_transitions"), 25 | "pre_transitions module should include without error") 26 | 27 | assert(require("ledge.state_machine.states"), 28 | "events module should include without error") 29 | } 30 | } 31 | --- request 32 | GET /t 33 | --- no_error_log 34 | [error] 35 | 36 | 37 | === TEST 2: Prove station machine compiles 38 | --- http_config eval: $::HttpConfig 39 | --- config 40 | location /t { 41 | content_by_lua_block { 42 | local events = require("ledge.state_machine.events") 43 | local pre_transitions = require("ledge.state_machine.pre_transitions") 44 | local states = require("ledge.state_machine.states") 45 | local actions = require("ledge.state_machine.actions") 46 | 47 | for ev,t in pairs(events) do 48 | for _,trans in ipairs(t) do 49 | -- Check states 50 | for _,kw in ipairs { "when", "after", "begin" } do 51 | if trans[kw] then 52 | if "function" ~= type(states[trans[kw]]) then 53 | ngx.say("State '", trans[kw], "' requested during ", 54 | ev, " is not defined") 55 | end 56 | end 57 | end 58 | 59 | -- Check "in_case" previous event 60 | if trans["in_case"] then 61 | if not events[trans["in_case"]] then 62 | ngx.say("Event '", trans["in_case"], 63 | "' filtered for but not in transition table") 64 | end 65 | end 66 | 67 | -- Check actions 68 | if trans["but_first"] then 69 | local action = trans["but_first"] 70 | if type(action) == "table" then 71 | for _,ac in ipairs(action) do 72 | if "function" ~= type(actions[ac]) then 73 | ngx.say("Action '", ac, "' called during ", ev, 74 | " is not defined") 75 | end 76 | end 77 | else 78 | if "function" ~= type(actions[action]) then 79 | ngx.say("Action '", action, "' called during ", ev, 80 | " is not defined") 81 | end 82 | end 83 | end 84 | end 85 | end 86 | 87 | for t,v in pairs(pre_transitions) do 88 | if "function" ~= type(states[t]) then 89 | ngx.say("Pre-transitions defined for missing state '", t, "'") 90 | end 91 | if type(v) ~= "table" or #v == 0 then 92 | ngx.say("No pre-transition actions defined for '", t, "'") 93 | else 94 | for _,action in ipairs(v) do 95 | if "function" ~= type(actions[action]) then 96 | ngx.say("Pre-transition action '", action, 97 | "' is not defined") 98 | end 99 | end 100 | end 101 | end 102 | 103 | for state, v in pairs(states) do 104 | local found = false 105 | for ev, t in pairs(events) do 106 | for _, trans in ipairs(t) do 107 | if trans["begin"] == state then 108 | found = true 109 | end 110 | end 111 | end 112 | 113 | if found == false then 114 | ngx.say("State '", state, "' is never transitioned to") 115 | end 116 | end 117 | 118 | 119 | local states_file = "lib/ledge/state_machine/states.lua" 120 | local handler_file = "lib/ledge/handler.lua" 121 | 122 | -- event in a table 123 | local events_called = {} 124 | for _, file in ipairs({ states_file, handler_file }) do 125 | assert(io.open(file, "r"), 126 | "Could not find states.lua (are you running from the root dir?") 127 | 128 | -- Run luac to extract self:e(event) calls by event name 129 | local cmd = "luac -p -l " .. file 130 | cmd = cmd .. [[ | grep -A2 'SELF .* "e"' | awk '{print $7}']] 131 | cmd = cmd .. [[ | grep "\".*\""]] 132 | local f, err = io.popen(cmd, "r") 133 | 134 | -- For each call, check the event being triggered exists, and place the 135 | repeat 136 | local event = f:read('*l') 137 | if event then 138 | event = ngx.re.gsub(event, "\"", "") -- remove quotes 139 | events_called[event] = true 140 | if not events[event] then 141 | ngx.say("Event '", event, "' is called but does not exist") 142 | end 143 | end 144 | until not event 145 | 146 | f:close() 147 | end 148 | 149 | for event, t_table in pairs(events) do 150 | if not events_called[event] then 151 | ngx.say("Event '", event, "' exits but is never called") 152 | end 153 | end 154 | 155 | ngx.say("OK") 156 | } 157 | } 158 | --- request 159 | GET /t 160 | --- response_body 161 | OK 162 | --- no_error_log 163 | [error] 164 | -------------------------------------------------------------------------------- /t/01-unit/validation.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: must_revalidate 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /t { 17 | content_by_lua_block { 18 | local must_revalidate = require("ledge.validation").must_revalidate 19 | 20 | local res = { 21 | header = { 22 | ["Cache-Control"] = ngx.req.get_headers().x_res_cache_control, 23 | ["Age"] = ngx.req.get_headers().x_res_age, 24 | }, 25 | } 26 | 27 | local result = ngx.req.get_uri_args().result 28 | assert(tostring(must_revalidate(res)) == result, 29 | "must_revalidate should be " .. result) 30 | 31 | } 32 | } 33 | --- more_headers eval 34 | [ 35 | "", 36 | "Cache-Control: max-age=0", 37 | "Cache-Control: max-age=1 38 | X-Res-Age: 1", 39 | "Cache-Control: max-age=1 40 | X-Res-Age: 2", 41 | "X-Res-Cache-Control: must-revalidate", 42 | "X-Res-Cache-Control: proxy-revalidate", 43 | ] 44 | --- request eval 45 | [ 46 | "GET /t?&result=false", 47 | "GET /t?&result=true", 48 | "GET /t?&result=false", 49 | "GET /t?&result=true", 50 | "GET /t?&result=true", 51 | "GET /t?&result=true", 52 | ] 53 | --- no_error_log 54 | [error] 55 | 56 | 57 | === TEST 2: can_revalidate_locally 58 | --- http_config eval: $::HttpConfig 59 | --- config 60 | location /t { 61 | content_by_lua_block { 62 | local can_revalidate_locally = 63 | require("ledge.validation").can_revalidate_locally 64 | 65 | local result = ngx.req.get_uri_args().result 66 | assert(tostring(can_revalidate_locally()) == result, 67 | "can_revalidate_locally should be " .. result) 68 | 69 | } 70 | } 71 | --- more_headers eval 72 | [ 73 | "", 74 | "If-None-Match:" , 75 | "If-None-Match: foo", 76 | "If-Modified-Since: Sun, 06 Nov 1994 08:49:37 GMT", 77 | "If-Modified-Since:", 78 | "If-Modified-Since: foo", 79 | ] 80 | --- request eval 81 | [ 82 | "GET /t?&result=false", 83 | "GET /t?&result=false", 84 | "GET /t?&result=true", 85 | "GET /t?&result=true", 86 | "GET /t?&result=false", 87 | "GET /t?&result=false", 88 | ] 89 | --- no_error_log 90 | [error] 91 | 92 | 93 | === TEST 3: is_valid_locally 94 | --- http_config eval: $::HttpConfig 95 | --- config 96 | location /t { 97 | content_by_lua_block { 98 | local is_valid_locally = require("ledge.validation").is_valid_locally 99 | 100 | local res = { 101 | header = { 102 | ["Last-Modified"] = ngx.req.get_headers().x_res_last_modified, 103 | ["Etag"] = ngx.req.get_headers().x_res_etag, 104 | }, 105 | } 106 | 107 | local result = ngx.req.get_uri_args().result 108 | assert(tostring(is_valid_locally(res)) == result, 109 | "is_valid_locally should be " .. result) 110 | 111 | } 112 | } 113 | --- more_headers eval 114 | [ 115 | "", 116 | "If-Modified-Since: Sun, 05 Nov 1994 08:49:37 GMT 117 | X-Res-Last-Modified: Sun, 06 Nov 1994 08:48:37 GMT", 118 | "If-Modified-Since: Sun, 06 Nov 1994 08:49:37 GMT 119 | X-Res-Last-Modified: Sun, 06 Nov 1994 08:48:37 GMT", 120 | "If-Modified-Since: Sun, 06 Nov 1994 08:49:38 GMT 121 | X-Res-Last-Modified: Sun, 06 Nov 1994 08:48:37 GMT", 122 | "If-Modified-Since: Sun, 06 Nov 1994 08:49:36 GMT 123 | X-Res-Last-Modified: Sun, 06 Nov 1994 08:49:37 GMT", 124 | "If-None-Match: foo 125 | X-Res-Etag: foo", 126 | "If-None-Match: foo 127 | X-Res-Etag: bar", 128 | "If-None-Match: foo", 129 | "X-Res-Etag: bar", 130 | ] 131 | --- request eval 132 | [ 133 | "GET /t?&result=false", 134 | "GET /t?&result=false", 135 | "GET /t?&result=true", 136 | "GET /t?&result=true", 137 | "GET /t?&result=false", 138 | "GET /t?&result=true", 139 | "GET /t?&result=false", 140 | "GET /t?&result=false", 141 | "GET /t?&result=false", 142 | ] 143 | --- no_error_log 144 | [error] 145 | -------------------------------------------------------------------------------- /t/01-unit/worker.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | our $HttpConfig_Test6 = LedgeEnv::http_config(extra_lua_config => qq{ 9 | foo = 1 10 | package.loaded["ledge.job.test"] = { 11 | perform = function(job) 12 | foo = foo + 1 13 | return true 14 | end 15 | } 16 | }, run_worker => 1); 17 | 18 | no_long_string(); 19 | no_diff(); 20 | run_tests(); 21 | 22 | __DATA__ 23 | === TEST 1: Load module without errors. 24 | --- http_config eval: $::HttpConfig 25 | --- config 26 | location /worker_1 { 27 | echo "OK"; 28 | } 29 | --- request 30 | GET /worker_1 31 | --- no_error_log 32 | [error] 33 | 34 | 35 | === TEST 2: Create worker with default config 36 | --- http_config eval: $::HttpConfig 37 | --- config 38 | location /worker_2 { 39 | echo "OK"; 40 | } 41 | --- request 42 | GET /worker_2 43 | --- no_error_log 44 | [error] 45 | 46 | 47 | === TEST 4: Create worker with bad config key 48 | --- http_config eval 49 | qq { 50 | lua_package_path "./lib/?.lua;;"; 51 | init_by_lua_block { 52 | if $LedgeEnv::test_coverage == 1 then 53 | require("luacov.runner").init() 54 | end 55 | } 56 | init_worker_by_lua_block { 57 | require("ledge.worker").new({ 58 | foo = "one", 59 | }) 60 | } 61 | } 62 | --- config 63 | location /worker_4 { 64 | echo "OK"; 65 | } 66 | --- request 67 | GET /worker_4 68 | --- error_log 69 | field foo does not exist 70 | 71 | 72 | === TEST 5: Run workers without errors 73 | --- http_config eval 74 | qq { 75 | lua_package_path "./lib/?.lua;;"; 76 | init_by_lua_block { 77 | if $LedgeEnv::test_coverage == 1 then 78 | require("luacov.runner").init() 79 | end 80 | } 81 | init_worker_by_lua_block { 82 | require("ledge.worker").new():run() 83 | } 84 | } 85 | --- config 86 | location /worker_5 { 87 | echo "OK"; 88 | } 89 | --- request 90 | GET /worker_5 91 | --- no_error_log 92 | [error] 93 | 94 | 95 | === TEST 6: Push a job and confirm it runs 96 | --- http_config eval: $::HttpConfig_Test6 97 | --- config 98 | location /worker_6 { 99 | content_by_lua_block { 100 | local qless = assert(require("resty.qless").new({ 101 | get_redis_client = require("ledge").create_qless_connection 102 | })) 103 | 104 | local jid = assert(qless.queues["ledge_gc"]:put("ledge.job.test")) 105 | 106 | ngx.sleep(2) 107 | ngx.say(foo) 108 | local job = qless.jobs:get(jid) 109 | ngx.say(job.state) 110 | } 111 | } 112 | --- request 113 | GET /worker_6 114 | --- response_body 115 | 2 116 | complete 117 | --- timeout: 5 118 | --- no_error_log 119 | [error] 120 | -------------------------------------------------------------------------------- /t/02-integration/age.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: No calculated Age header on cache MISS. 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /age_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler({ 20 | origin_mode = require("ledge").ORIGIN_MODE_AVOID 21 | }):run() 22 | } 23 | } 24 | location /age { 25 | more_set_headers "Cache-Control public, max-age=600"; 26 | echo "OK"; 27 | } 28 | --- request 29 | GET /age_prx 30 | --- response_headers 31 | Age: 32 | --- no_error_log 33 | [error] 34 | 35 | 36 | === TEST 2: Age header on cache HIT 37 | --- http_config eval: $::HttpConfig 38 | --- config 39 | location /age_prx { 40 | rewrite ^(.*)_prx$ $1 break; 41 | content_by_lua_block { 42 | require("ledge").create_handler({ 43 | origin_mode = require("ledge").ORIGIN_MODE_AVOID 44 | }):run() 45 | } 46 | } 47 | location /age { 48 | more_set_headers "Cache-Control public, max-age=600"; 49 | echo "OK"; 50 | } 51 | --- request 52 | GET /age_prx 53 | --- response_headers_like 54 | Age: \d+ 55 | --- no_error_log 56 | [error] 57 | -------------------------------------------------------------------------------- /t/02-integration/events.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: before_serve (add response header) 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /events_1_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | local handler = require("ledge").create_handler() 20 | handler:bind("before_serve", function(res) 21 | res.header["X-Modified"] = "Modified" 22 | end) 23 | handler:run() 24 | } 25 | } 26 | location /events_1 { 27 | echo "ORIGIN"; 28 | } 29 | --- request 30 | GET /events_1_prx 31 | --- error_code: 200 32 | --- response_headers 33 | X-Modified: Modified 34 | --- no_error_log 35 | [error] 36 | 37 | 38 | === TEST 2: before_upstream_request (modify request params) 39 | --- http_config eval: $::HttpConfig 40 | --- config 41 | location /events_2 { 42 | rewrite ^(.*)_prx$ $1 break; 43 | content_by_lua_block { 44 | local handler = require("ledge").create_handler() 45 | handler:bind("before_upstream_request", function(params) 46 | params.path = "/modified" 47 | end) 48 | handler:run() 49 | } 50 | } 51 | location /modified { 52 | echo "ORIGIN"; 53 | } 54 | --- request 55 | GET /events_2 56 | --- error_code: 200 57 | --- response_body 58 | ORIGIN 59 | --- no_error_log 60 | [error] 61 | -------------------------------------------------------------------------------- /t/02-integration/gc.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(extra_nginx_config => qq{ 7 | lua_check_client_abort on; 8 | }, extra_lua_config => qq{ 9 | require("ledge").set_handler_defaults({ 10 | keep_cache_for = 0, 11 | }) 12 | }, run_worker => 1); 13 | 14 | no_long_string(); 15 | no_diff(); 16 | run_tests(); 17 | 18 | __DATA__ 19 | === TEST 1: Prime cache 20 | --- http_config eval: $::HttpConfig 21 | --- config 22 | location /gc_prx { 23 | rewrite ^(.*)_prx$ $1 break; 24 | content_by_lua_block { 25 | require("ledge").create_handler():run() 26 | } 27 | } 28 | location /gc { 29 | more_set_headers "Cache-Control: public, max-age=60"; 30 | echo "OK"; 31 | } 32 | --- request 33 | GET /gc_prx 34 | --- no_error_log 35 | [error] 36 | --- response_body 37 | OK 38 | 39 | 40 | === TEST 2: Force revaldation (creates new entity) 41 | --- http_config eval: $::HttpConfig 42 | --- config 43 | location /gc_prx { 44 | rewrite ^(.*)_prx$ $1 break; 45 | echo_location_async '/gc_a'; 46 | echo_sleep 0.1; 47 | echo_location_async '/gc_b'; 48 | echo_sleep 2.5; 49 | } 50 | location /gc_a { 51 | rewrite ^(.*)_a$ $1 break; 52 | content_by_lua_block { 53 | require("ledge").create_handler():run(); 54 | } 55 | } 56 | location /gc_b { 57 | rewrite ^(.*)_b$ $1 break; 58 | content_by_lua_block { 59 | local redis = require("ledge").create_redis_connection() 60 | local handler = require("ledge").create_handler() 61 | handler.redis = redis 62 | 63 | local key_chain = handler:cache_key_chain() 64 | local num_entities, err = redis:scard(key_chain.entities) 65 | ngx.say(num_entities) 66 | } 67 | } 68 | location /gc { 69 | more_set_headers "Cache-Control: public, max-age=5"; 70 | content_by_lua_block { 71 | ngx.say("UPDATED") 72 | } 73 | } 74 | --- more_headers 75 | Cache-Control: no-cache 76 | --- request 77 | GET /gc_prx 78 | --- response_body 79 | UPDATED 80 | 1 81 | --- wait: 1 82 | 83 | 84 | === TEST 3: Check we now have just one entity 85 | --- http_config eval: $::HttpConfig 86 | --- config 87 | location /gc { 88 | content_by_lua_block { 89 | local redis = require("ledge").create_redis_connection() 90 | local handler = require("ledge").create_handler() 91 | handler.redis = redis 92 | 93 | local key_chain = handler:cache_key_chain() 94 | local num_entities, err = redis:scard(key_chain.entities) 95 | ngx.say(num_entities) 96 | } 97 | } 98 | --- request 99 | GET /gc 100 | --- no_error_log 101 | [error] 102 | --- response_body 103 | 1 104 | --- wait: 2 105 | 106 | 107 | === TEST 4: Entity will have expired, check Redis has cleaned up all keys. 108 | --- http_config eval: $::HttpConfig 109 | --- config 110 | location /gc { 111 | rewrite ^(.*)_prx$ $1 break; 112 | content_by_lua_block { 113 | local redis = require("ledge").create_redis_connection() 114 | local handler = require("ledge").create_handler() 115 | handler.redis = redis 116 | local key_chain = handler:cache_key_chain() 117 | local res, err = redis:keys(key_chain.full .. "*") 118 | assert(not next(res), "res should be empty") 119 | } 120 | } 121 | --- request 122 | GET /gc 123 | --- no_error_log 124 | [error] 125 | 126 | 127 | === TEST 5: Prime cache 128 | --- http_config eval: $::HttpConfig 129 | --- config 130 | location /gc_5_prx { 131 | rewrite ^(.*)_prx$ $1 break; 132 | content_by_lua_block { 133 | require("ledge").create_handler():run() 134 | } 135 | } 136 | location /gc_5 { 137 | more_set_headers "Cache-Control: public, max-age=60"; 138 | echo "OK"; 139 | } 140 | --- request 141 | GET /gc_5_prx 142 | --- no_error_log 143 | [error] 144 | --- response_body 145 | OK 146 | 147 | 148 | === TEST 5b: Delete one part of the key chain 149 | Simulate eviction under memory pressure. Will cause a MISS. 150 | --- http_config eval: $::HttpConfig 151 | --- config 152 | location /gc_5_prx { 153 | rewrite ^(.*)_prx$ $1 break; 154 | content_by_lua_block { 155 | local redis = require("ledge").create_redis_connection() 156 | local handler = require("ledge").create_handler() 157 | handler.redis = redis 158 | local key_chain = handler:cache_key_chain() 159 | redis:del(key_chain.headers) 160 | handler:run() 161 | } 162 | } 163 | location /gc_5 { 164 | more_set_headers "Cache-Control: public, max-age=60"; 165 | echo "OK 2"; 166 | } 167 | --- request 168 | GET /gc_5_prx 169 | --- wait: 3 170 | --- no_error_log 171 | [error] 172 | --- response_body 173 | OK 2 174 | 175 | 176 | === TEST 5c: Missing keys should cause colleciton of the old entity. 177 | --- http_config eval: $::HttpConfig 178 | --- config 179 | location /gc_5 { 180 | rewrite ^(.*)_prx$ $1 break; 181 | content_by_lua_block { 182 | local redis = require("ledge").create_redis_connection() 183 | local handler = require("ledge").create_handler() 184 | handler.redis = redis 185 | local key_chain = handler:cache_key_chain() 186 | local res, err = redis:keys(key_chain.full .. "*") 187 | if res then 188 | ngx.say(#res) 189 | end 190 | } 191 | } 192 | --- request 193 | GET /gc_5 194 | --- no_error_log 195 | [error] 196 | --- response_body 197 | 5 198 | -------------------------------------------------------------------------------- /t/02-integration/gzip.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Prime gzipped response 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /gzip_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler():run() 20 | } 21 | } 22 | location /gzip { 23 | gzip on; 24 | gzip_proxied any; 25 | gzip_min_length 1; 26 | gzip_http_version 1.0; 27 | default_type text/html; 28 | more_set_headers "Cache-Control: public, max-age=600"; 29 | more_set_headers "Content-Type: text/html"; 30 | echo "OK"; 31 | } 32 | --- request 33 | GET /gzip_prx 34 | --- more_headers 35 | Accept-Encoding: gzip 36 | --- response_body_unlike: OK 37 | --- no_error_log 38 | [error] 39 | 40 | 41 | === TEST 2: Client doesnt support gzip, gets plain response 42 | --- http_config eval: $::HttpConfig 43 | --- config 44 | location /gzip_prx { 45 | rewrite ^(.*)_prx$ $1 break; 46 | content_by_lua_block { 47 | require("ledge").create_handler():run() 48 | } 49 | } 50 | --- request 51 | GET /gzip_prx 52 | --- response_body 53 | OK 54 | --- no_error_log 55 | [error] 56 | 57 | 58 | === TEST 2b: Client doesnt support gzip, gunzip is disabled, gets zipped response 59 | --- http_config eval: $::HttpConfig 60 | --- config 61 | location /gzip_prx { 62 | rewrite ^(.*)_prx$ $1 break; 63 | content_by_lua_block { 64 | require("ledge").create_handler({ 65 | gunzip_enabled = false, 66 | }):run() 67 | } 68 | } 69 | --- request 70 | GET /gzip_prx 71 | --- response_body_unlike: OK 72 | --- no_error_log 73 | [error] 74 | 75 | 76 | === TEST 3: Client does support gzip, gets zipped response 77 | --- http_config eval: $::HttpConfig 78 | --- config 79 | location /gzip_prx { 80 | rewrite ^(.*)_prx$ $1 break; 81 | content_by_lua_block { 82 | require("ledge").create_handler():run() 83 | } 84 | } 85 | --- request 86 | GET /gzip_prx 87 | --- more_headers 88 | Accept-Encoding: gzip 89 | --- response_body_unlike: OK 90 | --- no_error_log 91 | [error] 92 | 93 | 94 | === TEST 4: Client does support gzip, but sends a range, gets plain full response 95 | --- http_config eval: $::HttpConfig 96 | --- config 97 | location /gzip_prx { 98 | rewrite ^(.*)_prx$ $1 break; 99 | content_by_lua_block { 100 | require("ledge").create_handler():run() 101 | } 102 | } 103 | --- request 104 | GET /gzip_prx 105 | --- more_headers 106 | Accept-Encoding: gzip 107 | --- more_headers 108 | Range: bytes=0-0 109 | --- error_code: 200 110 | --- response_body 111 | OK 112 | --- no_error_log 113 | [error] 114 | 115 | 116 | === TEST 5: Prime gzipped response with ESI, auto unzips. 117 | --- http_config eval: $::HttpConfig 118 | --- config 119 | location /gzip_5_prx { 120 | rewrite ^(.*)_prx$ $1 break; 121 | content_by_lua_block { 122 | require("ledge").create_handler({ 123 | esi_enabled = true, 124 | }):run() 125 | } 126 | } 127 | location /gzip_5 { 128 | gzip on; 129 | gzip_proxied any; 130 | gzip_min_length 1; 131 | gzip_http_version 1.0; 132 | default_type text/html; 133 | more_set_headers "Cache-Control: public, max-age=600"; 134 | more_set_headers "Content-Type: text/html"; 135 | more_set_headers 'Surrogate-Control: content="ESI/1.0"'; 136 | echo "OK"; 137 | } 138 | --- request 139 | GET /gzip_5_prx 140 | --- more_headers 141 | Accept-Encoding: gzip 142 | --- response_body 143 | OK 144 | --- no_error_log 145 | [error] 146 | 147 | 148 | === TEST 6: Client does support gzip, but content had to be unzipped on save 149 | --- http_config eval: $::HttpConfig 150 | --- config 151 | location /gzip_5_prx { 152 | rewrite ^(.*)_prx$ $1 break; 153 | content_by_lua_block { 154 | require("ledge").create_handler():run() 155 | } 156 | } 157 | --- request 158 | GET /gzip_5_prx 159 | --- more_headers 160 | Accept-Encoding: gzip 161 | --- response_body 162 | OK 163 | --- no_error_log 164 | [error] 165 | 166 | 167 | === TEST 7: HEAD request for gzipped response with ESI, auto unzips. 168 | --- http_config eval: $::HttpConfig 169 | --- config 170 | location /gzip_7_prx { 171 | rewrite ^(.*)_prx$ $1 break; 172 | content_by_lua_block { 173 | require("ledge").create_handler({ 174 | esi_enabled = true, 175 | }):run() 176 | } 177 | } 178 | location /gzip_7 { 179 | gzip on; 180 | gzip_proxied any; 181 | gzip_min_length 1; 182 | gzip_http_version 1.0; 183 | default_type text/html; 184 | more_set_headers "Cache-Control: public, max-age=600"; 185 | more_set_headers "Content-Type: text/html"; 186 | more_set_headers 'Surrogate-Control: content="ESI/1.0"'; 187 | echo "OK"; 188 | } 189 | --- request 190 | HEAD /gzip_7_prx 191 | --- more_headers 192 | Accept-Encoding: gzip 193 | --- response_body 194 | --- no_error_log 195 | [error] 196 | -------------------------------------------------------------------------------- /t/02-integration/hop_by_hop_headers.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Test hop-by-hop headers are not passed on. 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /hop_by_hop_headers_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler():run() 20 | } 21 | } 22 | location /hop_by_hop_headers { 23 | more_set_headers "Cache-Control public, max-age=600"; 24 | more_set_headers "Proxy-Authenticate foo"; 25 | more_set_headers "Upgrade foo"; 26 | echo "OK"; 27 | } 28 | --- request 29 | GET /hop_by_hop_headers_prx 30 | --- response_headers 31 | Proxy-Authenticate: 32 | Upgrade: 33 | --- no_error_log 34 | [error] 35 | 36 | 37 | === TEST 2: Test hop-by-hop headers were not cached. 38 | --- http_config eval: $::HttpConfig 39 | --- config 40 | location /hop_by_hop_headers_prx { 41 | rewrite ^(.*)_prx$ $1 break; 42 | content_by_lua_block { 43 | require("ledge").create_handler():run() 44 | } 45 | } 46 | --- request 47 | GET /hop_by_hop_headers_prx 48 | --- response_headers 49 | Proxy-Authenticate: 50 | Upgrade: 51 | --- no_error_log 52 | [error] 53 | -------------------------------------------------------------------------------- /t/02-integration/max-stale.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(extra_lua_config => qq{ 7 | package.loaded["state"] = { 8 | miss_count = 0, 9 | } 10 | }, run_worker => 1); 11 | 12 | no_long_string(); 13 | no_diff(); 14 | run_tests(); 15 | 16 | __DATA__ 17 | === TEST 1: Honour max-stale request header for an expired item 18 | --- http_config eval: $::HttpConfig 19 | --- config 20 | location /stale_1_prx { 21 | rewrite ^(.*)_prx$ $1 break; 22 | content_by_lua_block { 23 | local handler = require("ledge").create_handler() 24 | handler:bind("before_save", function(res) 25 | -- immediately expire 26 | res.header["Cache-Control"] = "max-age=0" 27 | end) 28 | handler:run() 29 | } 30 | } 31 | location /stale_1 { 32 | content_by_lua_block { 33 | local state = require("state") 34 | state.miss_count = state.miss_count + 1 35 | ngx.status = 404 36 | ngx.header["Cache-Control"] = "max-age=60" 37 | ngx.print("TEST 1: ", state.miss_count) 38 | } 39 | } 40 | --- more_headers 41 | Cache-Control: max-stale=1000 42 | --- request eval 43 | ["GET /stale_1_prx", "GET /stale_1_prx"] 44 | --- response_body eval 45 | ["TEST 1: 1", "TEST 1: 1"] 46 | --- response_headers_like eval 47 | ["", 'Warning: 110 (?:[^\s]*) "Response is stale"'] 48 | --- error_code eval 49 | [404, 404] 50 | --- no_error_log 51 | [error] 52 | 53 | 54 | === TEST 1b: Confirm nothing was revalidated in the background 55 | --- http_config eval: $::HttpConfig 56 | --- config 57 | location /stale_1_prx { 58 | rewrite ^(.*)_prx$ $1 break; 59 | content_by_lua_block { 60 | require("ledge").create_handler():run() 61 | } 62 | } 63 | --- more_headers 64 | Cache-Control: max-stale=1000 65 | --- request 66 | GET /stale_1_prx 67 | --- response_body: TEST 1: 1 68 | --- response_headers_like 69 | Warning: 110 (?:[^\s]*) "Response is stale" 70 | --- error_code eval 71 | 404 72 | --- no_error_log 73 | [error] 74 | 75 | 76 | === TEST 5: proxy-revalidate must revalidate (not serve stale) 77 | --- http_config eval: $::HttpConfig 78 | --- config 79 | location /stale_5_prx { 80 | rewrite ^(.*)_prx$ $1 break; 81 | content_by_lua_block { 82 | require("ledge.state_machine").set_debug(true) 83 | local handler = require("ledge").create_handler() 84 | handler:bind("before_save", function(res) 85 | -- immediately expire 86 | res.header["Cache-Control"] = "max-age=0, proxy-revalidate" 87 | end) 88 | handler:run() 89 | } 90 | } 91 | location /stale_5 { 92 | content_by_lua_block { 93 | local state = require("state") 94 | state.miss_count = state.miss_count + 1 95 | ngx.status = 404 96 | ngx.header["Cache-Control"] = "max-age=3600, proxy-revalidate" 97 | ngx.print("TEST 5: ", state.miss_count) 98 | } 99 | } 100 | --- more_headers 101 | Cache-Control: max-stale=120 102 | --- request eval 103 | ["GET /stale_5_prx", "GET /stale_5_prx"] 104 | --- response_body eval 105 | ["TEST 5: 1", "TEST 5: 2"] 106 | --- raw_response_headers_unlike eval 107 | ["Warning: 110", "Warning: 110"] 108 | --- error_code eval 109 | [404, 404] 110 | --- no_error_log 111 | [error] 112 | 113 | 114 | === TEST 6: must-revalidate must revalidate (not serve stale) 115 | --- http_config eval: $::HttpConfig 116 | --- config 117 | location /stale_6_prx { 118 | rewrite ^(.*)_prx$ $1 break; 119 | content_by_lua_block { 120 | local handler = require("ledge").create_handler() 121 | handler:bind("before_save", function(res) 122 | -- immediately expire 123 | res.header["Cache-Control"] = "max-age=0, must-revalidate" 124 | end) 125 | handler:run() 126 | } 127 | } 128 | location /stale_6 { 129 | content_by_lua_block { 130 | local state = require("state") 131 | state.miss_count = state.miss_count + 1 132 | ngx.status = 404 133 | ngx.header["Cache-Control"] = "max-age=3600, must-revalidate" 134 | ngx.print("TEST 6: ", state.miss_count) 135 | } 136 | } 137 | --- more_headers 138 | Cache-Control: max-stale=120 139 | --- request eval 140 | ["GET /stale_6_prx", "GET /stale_6_prx"] 141 | --- response_body eval 142 | ["TEST 6: 1", "TEST 6: 2"] 143 | --- raw_response_headers_unlike eval 144 | ["Warning: 110", "Warning: 110"] 145 | --- error_code eval 146 | [404, 404] 147 | --- no_error_log 148 | [error] 149 | 150 | 151 | === TEST 7: Can serve stale but must revalidate because of Age 152 | --- http_config eval: $::HttpConfig 153 | --- config 154 | location /stale_7_prx { 155 | rewrite ^(.*)_prx$ $1 break; 156 | content_by_lua_block { 157 | local handler = require("ledge").create_handler() 158 | handler:bind("before_save", function(res) 159 | -- immediately expire 160 | res.header["Cache-Control"] = "max-age=0" 161 | end) 162 | handler:run() 163 | } 164 | } 165 | location /stale_7 { 166 | content_by_lua_block { 167 | local state = require("state") 168 | state.miss_count = state.miss_count + 1 169 | ngx.status = 404 170 | ngx.header["Cache-Control"] = "max-age=3600" 171 | ngx.print("TEST 7: ", state.miss_count) 172 | } 173 | } 174 | --- more_headers 175 | Cache-Control: max-stale=120, max-age=1 176 | --- request eval 177 | ["GET /stale_7_prx", "GET /stale_7_prx"] 178 | --- response_body eval 179 | ["TEST 7: 1", "TEST 7: 2"] 180 | --- raw_response_headers_unlike eval 181 | ["Warning: 110", "Warning: 110"] 182 | --- error_code eval 183 | [404, 404] 184 | --- no_error_log 185 | [error] 186 | --- wait: 2 187 | -------------------------------------------------------------------------------- /t/02-integration/max_size.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(extra_lua_config => qq{ 7 | require("ledge").set_handler_defaults({ 8 | storage_driver_config = { 9 | max_size = 8, 10 | } 11 | }) 12 | }); 13 | 14 | no_long_string(); 15 | no_diff(); 16 | run_tests(); 17 | 18 | __DATA__ 19 | === TEST 1: Response larger than cache_max_memory. 20 | --- http_config eval: $::HttpConfig 21 | --- config 22 | location /max_memory_prx { 23 | rewrite ^(.*)_prx$ $1 break; 24 | content_by_lua_block { 25 | require("ledge").create_handler():run() 26 | } 27 | } 28 | location /max_memory { 29 | content_by_lua_block { 30 | ngx.header["Cache-Control"] = "max-age=3600" 31 | ngx.say("RESPONSE IS TOO LARGE TEST 1") 32 | } 33 | } 34 | --- request 35 | GET /max_memory_prx 36 | --- response_body 37 | RESPONSE IS TOO LARGE TEST 1 38 | --- response_headers_like 39 | X-Cache: MISS from .* 40 | --- error_log 41 | storage failed to write: body is larger than 8 bytes 42 | 43 | 44 | === TEST 2: Test we did not store in previous test. 45 | --- http_config eval: $::HttpConfig 46 | --- config 47 | location /max_memory_prx { 48 | rewrite ^(.*)_prx$ $1 break; 49 | content_by_lua_block { 50 | require("ledge").create_handler():run() 51 | } 52 | } 53 | location /max_memory { 54 | content_by_lua_block { 55 | ngx.header["Cache-Control"] = "max-age=3600" 56 | ngx.say("TEST 2") 57 | } 58 | } 59 | --- request 60 | GET /max_memory_prx 61 | --- response_body 62 | TEST 2 63 | --- response_headers_like 64 | X-Cache: MISS from .* 65 | --- no_error_log 66 | 67 | 68 | === TEST 3: Non-chunked response larger than cache_max_memory. 69 | --- http_config eval: $::HttpConfig 70 | --- config 71 | location /max_memory_3_prx { 72 | rewrite ^(.*)_prx$ $1 break; 73 | content_by_lua_block { 74 | require("ledge").create_handler():run() 75 | } 76 | } 77 | location /max_memory_3 { 78 | chunked_transfer_encoding off; 79 | content_by_lua_block { 80 | ngx.header["Cache-Control"] = "max-age=3600" 81 | local body = "RESPONSE IS TOO LARGE TEST 3\n" 82 | ngx.header["Content-Length"] = string.len(body) 83 | ngx.print(body) 84 | } 85 | } 86 | --- request 87 | GET /max_memory_3_prx 88 | --- response_body 89 | RESPONSE IS TOO LARGE TEST 3 90 | --- response_headers_like 91 | X-Cache: MISS from .* 92 | --- no_error_log 93 | 94 | 95 | === TEST 4: Test we did not store in previous test. 96 | --- http_config eval: $::HttpConfig 97 | --- config 98 | location /max_memory_3_prx { 99 | rewrite ^(.*)_prx$ $1 break; 100 | content_by_lua_block { 101 | require("ledge").create_handler():run() 102 | } 103 | } 104 | location /max_memory_3 { 105 | content_by_lua_block { 106 | ngx.header["Cache-Control"] = "max-age=3600" 107 | ngx.say("TEST 4") 108 | } 109 | } 110 | --- request 111 | GET /max_memory_3_prx 112 | --- response_body 113 | TEST 4 114 | --- response_headers_like 115 | X-Cache: MISS from .* 116 | --- no_error_log 117 | 118 | 119 | === TEST 5a: Prime cache with ok size 120 | --- http_config eval: $::HttpConfig 121 | --- config 122 | location /max_memory_5_prx { 123 | rewrite ^(.*)_prx$ $1 break; 124 | content_by_lua_block { 125 | require("ledge").create_handler():run() 126 | } 127 | } 128 | location /max_memory_5 { 129 | content_by_lua_block { 130 | ngx.header["Cache-Control"] = "max-age=3600" 131 | ngx.say("OK") 132 | } 133 | } 134 | --- request 135 | GET /max_memory_5_prx 136 | --- response_body 137 | OK 138 | --- response_headers_like 139 | X-Cache: MISS from .* 140 | --- no_error_log 141 | [error] 142 | 143 | 144 | === TEST 5b: Try to replace with a large response 145 | --- http_config eval: $::HttpConfig 146 | --- config 147 | location /max_memory_5_prx { 148 | rewrite ^(.*)_prx$ $1 break; 149 | content_by_lua_block { 150 | require("ledge").create_handler():run() 151 | } 152 | } 153 | location /max_memory_5 { 154 | content_by_lua_block { 155 | ngx.header["Cache-Control"] = "max-age=3600" 156 | ngx.say("RESPONSE IS TOO LARGE") 157 | } 158 | } 159 | --- more_headers 160 | Cache-Control: no-cache 161 | --- request 162 | GET /max_memory_5_prx 163 | --- response_body 164 | RESPONSE IS TOO LARGE 165 | --- response_headers_like 166 | X-Cache: MISS from .* 167 | --- error_log 168 | larger than 8 bytes 169 | 170 | 171 | === TEST 5c: Confirm original cache is still ok 172 | --- http_config eval: $::HttpConfig 173 | --- config 174 | location /max_memory_5_prx { 175 | rewrite ^(.*)_prx$ $1 break; 176 | content_by_lua_block { 177 | require("ledge").create_handler():run() 178 | } 179 | } 180 | --- request 181 | GET /max_memory_5_prx 182 | --- response_body 183 | OK 184 | --- response_headers_like 185 | X-Cache: HIT from .* 186 | --- no_error_log 187 | [error] 188 | -------------------------------------------------------------------------------- /t/02-integration/multiple_headers.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Multiple cache-control response headers, miss 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /multiple_cache_headers_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler():run() 20 | } 21 | } 22 | 23 | location /multiple_cache_headers { 24 | content_by_lua_block { 25 | ngx.header["Cache-Control"] = { "public", "max-age=3600"} 26 | ngx.say("TEST 1") 27 | } 28 | } 29 | --- request 30 | GET /multiple_cache_headers_prx 31 | --- response_headers_like 32 | X-Cache: MISS from .* 33 | --- response_headers 34 | Cache-Control: public, max-age=3600 35 | --- response_body 36 | TEST 1 37 | 38 | 39 | === TEST 1b: Multiple cache-control response headers, hit 40 | --- http_config eval: $::HttpConfig 41 | --- config 42 | location /multiple_cache_headers_prx { 43 | rewrite ^(.*)_prx$ $1 break; 44 | content_by_lua_block { 45 | require("ledge").create_handler():run() 46 | } 47 | } 48 | 49 | location /multiple_cache_headers { 50 | content_by_lua_block { 51 | ngx.header["Cache-Control"] = { "public", "max-age=3600"} 52 | ngx.say("TEST 2") 53 | } 54 | } 55 | --- request 56 | GET /multiple_cache_headers_prx 57 | --- response_headers_like 58 | X-Cache: HIT from .* 59 | --- response_headers 60 | Cache-Control: public, max-age=3600 61 | --- response_body 62 | TEST 1 63 | 64 | === TEST 2: Multiple Date response headers, miss 65 | --- http_config eval: $::HttpConfig 66 | --- config 67 | location /multiple_date_headers_prx { 68 | rewrite ^(.*)_prx$ $1 break; 69 | content_by_lua_block { 70 | require("ledge").create_handler({ 71 | upstream_port = 12345 72 | }):run() 73 | } 74 | } 75 | --- request 76 | GET /multiple_date_headers_prx 77 | --- tcp_listen: 12345 78 | --- tcp_reply 79 | HTTP/1.1 200 OK 80 | Date: Mon, 24 Sep 2018 00:47:20 GMT 81 | Server: Apache/2 82 | Date: Mon, 24 Sep 2018 01:47:20 GMT 83 | Cache-Control: public, max-age=300 84 | 85 | TEST 2 86 | --- response_headers_like 87 | X-Cache: MISS from .* 88 | --- response_headers_unlike 89 | Date: Mon, 24 Sep 2018 00:47:20 GMT 90 | Date: Mon, 24 Sep 2018 01:47:20 GMT 91 | --- response_body 92 | TEST 2 93 | 94 | === TEST 2b: Multiple Date response headers, hit 95 | --- http_config eval: $::HttpConfig 96 | --- config 97 | location /multiple_date_headers_prx { 98 | rewrite ^(.*)_prx$ $1 break; 99 | content_by_lua_block { 100 | require("ledge").create_handler():run() 101 | } 102 | } 103 | --- request 104 | GET /multiple_date_headers_prx 105 | --- response_headers_like 106 | X-Cache: HIT from .* 107 | --- response_headers_unlike 108 | Date: Mon, 24 Sep 2018 00:47:20 GMT 109 | Date: Mon, 24 Sep 2018 01:47:20 GMT 110 | --- response_body 111 | TEST 2 112 | -------------------------------------------------------------------------------- /t/02-integration/on_abort.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(extra_nginx_config => qq{ 7 | lua_check_client_abort on; 8 | 9 | upstream test-upstream { 10 | server 127.0.0.1:1984; 11 | keepalive 16; 12 | } 13 | }, run_worker => 1); 14 | 15 | no_long_string(); 16 | no_diff(); 17 | run_tests(); 18 | 19 | __DATA__ 20 | === TEST 1: Warning when unable to set client abort handler 21 | --- http_config eval: $::HttpConfig 22 | --- config 23 | location /abort_prx { 24 | rewrite ^(.*)_prx$ $1 break; 25 | lua_check_client_abort off; 26 | content_by_lua_block { 27 | require("ledge").create_handler():run() 28 | } 29 | } 30 | location /abort { 31 | echo "foo"; 32 | } 33 | --- request 34 | GET /abort_prx 35 | --- error_log 36 | on_abort handler could not be set: lua_check_client_abort is off 37 | 38 | 39 | === TEST 2a: Client abort mid save should still save to cache (run and abort) 40 | --- http_config eval: $::HttpConfig 41 | --- config 42 | location /abort_prx { 43 | rewrite ^(.*)_prx$ $1 break; 44 | content_by_lua_block { 45 | require("ledge").create_handler():run() 46 | } 47 | } 48 | location /abort { 49 | content_by_lua_block { 50 | ngx.status = 200 51 | ngx.header["Cache-Control"] = "public, max-age=3600" 52 | ngx.say("START") 53 | ngx.flush(true) 54 | ngx.sleep(2) 55 | ngx.say("FINISH") 56 | } 57 | } 58 | --- request 59 | GET /abort_prx 60 | --- timeout: 1 61 | --- wait: 1.5 62 | --- abort 63 | --- ignore_response 64 | --- no_error_log 65 | [error] 66 | 67 | 68 | === TEST 2b: Prove we have a complete cache entry 69 | --- http_config eval: $::HttpConfig 70 | --- config 71 | location /abort_prx { 72 | rewrite ^(.*)_prx$ $1 break; 73 | content_by_lua_block { 74 | require("ledge").create_handler():run() 75 | } 76 | } 77 | --- request 78 | GET /abort_prx 79 | --- response_body 80 | START 81 | FINISH 82 | --- error_code: 200 83 | --- no_error_log 84 | [error] 85 | 86 | 87 | === TEST 3a: Client abort before save aborts fetching 88 | --- http_config eval: $::HttpConfig 89 | --- config 90 | location /abort_prx { 91 | rewrite ^(.*)_prx$ $1 break; 92 | content_by_lua_block { 93 | require("ledge").create_handler():run() 94 | } 95 | } 96 | location /abort { 97 | content_by_lua_block { 98 | ngx.sleep(2) 99 | ngx.status = 200 100 | ngx.header["Cache-Control"] = "public, max-age=3600" 101 | ngx.say("START 2") 102 | ngx.say("FINISH 2") 103 | } 104 | } 105 | --- request 106 | GET /abort_prx 107 | --- more_headers 108 | Cache-Control: max-age=0 109 | --- timeout: 1 110 | --- wait: 1.5 111 | --- abort 112 | --- ignore_response 113 | --- no_error_log 114 | [error] 115 | 116 | 117 | === TEST 3b: Prove we still have the previous cache entry 118 | --- http_config eval: $::HttpConfig 119 | --- config 120 | location /abort_prx { 121 | rewrite ^(.*)_prx$ $1 break; 122 | content_by_lua_block { 123 | require("ledge").create_handler():run() 124 | } 125 | } 126 | --- request 127 | GET /abort_prx 128 | --- response_body 129 | START 130 | FINISH 131 | --- error_code: 200 132 | --- no_error_log 133 | [error] 134 | 135 | 136 | === TEST 4a: Prime immediately expiring cache item 137 | --- http_config eval: $::HttpConfig 138 | --- config 139 | location /abort_prx { 140 | rewrite ^(.*)_prx$ $1 break; 141 | content_by_lua_block { 142 | local handler = require("ledge").create_handler() 143 | handler:bind("before_save", function(res) 144 | -- immediately expire cache entries 145 | res.header["Cache-Control"] = "max-age=0" 146 | end) 147 | handler:run() 148 | } 149 | } 150 | location /abort { 151 | content_by_lua_block { 152 | ngx.header["Cache-Control"] = "max-age=3600" 153 | ngx.say("OK") 154 | } 155 | } 156 | --- more_headers 157 | Cache-Control: no-cache 158 | --- request 159 | GET /abort_prx 160 | --- response_body 161 | OK 162 | --- error_code: 200 163 | --- no_error_log 164 | [error] 165 | 166 | 167 | === TEST 4b: Client abort before fetch with collapsed forwarding on cancels abort 168 | --- http_config eval: $::HttpConfig 169 | --- config 170 | location /abort_prx { 171 | rewrite ^(.*)_prx$ $1 break; 172 | content_by_lua_block { 173 | local handler = require("ledge").create_handler({ 174 | enable_collapsed_forwarding = true, 175 | }) 176 | handler:bind("before_upstream_request", function(res) 177 | ngx.sleep(2) 178 | end) 179 | handler:run() 180 | } 181 | } 182 | location /abort { 183 | content_by_lua_block { 184 | ngx.status = 200 185 | ngx.header["Cache-Control"] = "public, max-age=3600" 186 | ngx.say("START") 187 | ngx.say("FINISH") 188 | } 189 | } 190 | --- request 191 | GET /abort_prx 192 | --- timeout: 1 193 | --- wait: 1.5 194 | --- abort 195 | --- ignore_response 196 | --- no_error_log 197 | [error] 198 | 199 | 200 | === TEST 4c: Prove we have the previous cache entry 201 | --- http_config eval: $::HttpConfig 202 | --- config 203 | location /abort_prx { 204 | rewrite ^(.*)_prx$ $1 break; 205 | content_by_lua_block { 206 | require("ledge").create_handler():run() 207 | } 208 | } 209 | --- request 210 | GET /abort_prx 211 | --- response_body 212 | START 213 | FINISH 214 | --- error_code: 200 215 | --- no_error_log 216 | [error] 217 | 218 | 219 | === TEST 5: No error when keepalive_requests exceeded 220 | --- http_config eval: $::HttpConfig 221 | --- config 222 | location = /abort_top { 223 | content_by_lua_block { 224 | local http = require "resty.http" 225 | local httpc = http.new() 226 | local res, err = httpc:request_uri( 227 | "http://" .. 228 | ngx.var.server_addr .. ":" .. ngx.var.server_port .. 229 | "/abort_ngx" 230 | ) 231 | if not res then 232 | ngx.log(ngx.ERR, err) 233 | end 234 | 235 | local res, err = httpc:request_uri( 236 | "http://" .. 237 | ngx.var.server_addr .. ":" .. ngx.var.server_port .. 238 | "/abort_ngx" 239 | ) 240 | if not res then 241 | ngx.log(ngx.ERR, err) 242 | end 243 | 244 | ngx.say("OK") 245 | } 246 | } 247 | location = /abort_ngx { 248 | rewrite ^ /abort_prx break; 249 | proxy_pass http://test-upstream; 250 | } 251 | location = /abort_prx { 252 | rewrite ^(.*)_prx$ $1 break; 253 | keepalive_requests 1; 254 | content_by_lua_block { 255 | require("ledge").create_handler():run() 256 | } 257 | } 258 | location = /abort { 259 | content_by_lua_block { 260 | ngx.status = 200 261 | ngx.header["Cache-Control"] = "public, max-age=3600" 262 | ngx.say("START") 263 | ngx.say("FINISH") 264 | } 265 | } 266 | --- request 267 | GET /abort_top 268 | --- response_body 269 | OK 270 | --- error_code: 200 271 | --- no_error_log 272 | [error] 273 | -------------------------------------------------------------------------------- /t/02-integration/origin_mode.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: ORIGIN_MODE_NORMAL 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /origin_mode_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler({ 20 | origin_mode = require("ledge").ORIGIN_MODE_NORMAL 21 | }):run() 22 | } 23 | } 24 | location /origin_mode { 25 | content_by_lua_block { 26 | ngx.header["Cache-Control"] = "public, max-age=60" 27 | ngx.print("OK") 28 | } 29 | } 30 | --- request eval 31 | ["GET /origin_mode_prx", "GET /origin_mode_prx"] 32 | --- response_body eval 33 | ["OK", "OK"] 34 | --- response_headers_like eval 35 | ["X-Cache: MISS from .*", "X-Cache: HIT from .*"] 36 | --- no_error_log 37 | [error] 38 | 39 | 40 | === TEST 2: ORIGIN_MODE_AVOID (no-cache request) 41 | --- http_config eval: $::HttpConfig 42 | --- config 43 | location /origin_mode_prx { 44 | rewrite ^(.*)_prx$ $1 break; 45 | content_by_lua_block { 46 | require("ledge").create_handler({ 47 | origin_mode = require("ledge").ORIGIN_MODE_AVOID 48 | }):run() 49 | } 50 | } 51 | --- more_headers 52 | Cache-Control: no-cache 53 | --- request 54 | GET /origin_mode_prx 55 | --- response_body: OK 56 | --- response_headers_like 57 | X-Cache: HIT from .* 58 | --- no_error_log 59 | [error] 60 | 61 | 62 | === TEST 2a: ORIGIN_MODE_AVOID (max-age=0 request) 63 | --- http_config eval: $::HttpConfig 64 | --- config 65 | location /origin_mode_prx { 66 | rewrite ^(.*)_prx$ $1 break; 67 | content_by_lua_block { 68 | require("ledge").create_handler({ 69 | origin_mode = require("ledge").ORIGIN_MODE_AVOID 70 | }):run() 71 | } 72 | } 73 | --- more_headers 74 | Cache-Control: max-age=0 75 | --- request 76 | GET /origin_mode_prx 77 | --- response_body: OK 78 | --- response_headers_like 79 | X-Cache: HIT from .* 80 | --- no_error_log 81 | [error] 82 | 83 | 84 | === TEST 2b: ORIGIN_MODE_AVOID (expired cache) 85 | --- http_config eval: $::HttpConfig 86 | --- config 87 | location /origin_mode_2b_prx { 88 | rewrite ^(.*)_prx$ $1 break; 89 | content_by_lua_block { 90 | local handler = require("ledge").create_handler({ 91 | origin_mode = require("ledge").ORIGIN_MODE_AVOID 92 | }) 93 | 94 | handler:bind("before_save", function(res) 95 | -- immediately expire 96 | res.header["Cache-Control"] = "max-age=0" 97 | end) 98 | handler:run() 99 | } 100 | } 101 | location /origin_mode_2b { 102 | content_by_lua_block { 103 | ngx.header["Cache-Control"] = "public, max-age=60" 104 | ngx.print("OK") 105 | } 106 | } 107 | --- request eval 108 | ["GET /origin_mode_2b_prx", "GET /origin_mode_2b_prx"] 109 | --- response_body eval 110 | ["OK", "OK"] 111 | --- response_headers_like eval 112 | ["X-Cache: MISS from .*", "X-Cache: HIT from .*"] 113 | --- no_error_log 114 | [error] 115 | 116 | 117 | === TEST 3: ORIGIN_MODE_BYPASS when cached with 112 warning 118 | --- http_config eval: $::HttpConfig 119 | --- config 120 | location /origin_mode_prx { 121 | rewrite ^(.*)_prx$ $1 break; 122 | content_by_lua_block { 123 | require("ledge").create_handler({ 124 | origin_mode = require("ledge").ORIGIN_MODE_BYPASS 125 | }):run() 126 | } 127 | } 128 | --- more_headers 129 | Cache-Control: no-cache 130 | --- request 131 | GET /origin_mode_prx 132 | --- response_headers_like 133 | Warning: 112 .* 134 | --- response_body: OK 135 | --- no_error_log 136 | [error] 137 | 138 | 139 | === TEST 4: ORIGIN_MODE_BYPASS when we have nothing 140 | --- http_config eval: $::HttpConfig 141 | --- config 142 | location /origin_mode_bypass_prx { 143 | rewrite ^(.*)_prx$ $1 break; 144 | content_by_lua_block { 145 | require("ledge").create_handler({ 146 | origin_mode = require("ledge").ORIGIN_MODE_BYPASS 147 | }):run() 148 | } 149 | } 150 | --- more_headers 151 | Cache-Control: no-cache 152 | --- request 153 | GET /origin_mode_bypass_prx 154 | --- error_code: 503 155 | --- no_error_log 156 | [error] 157 | -------------------------------------------------------------------------------- /t/02-integration/req_body.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Should pass through request body 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /cached_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler():run() 20 | } 21 | } 22 | location /cached { 23 | content_by_lua_block { 24 | ngx.req.read_body() 25 | ngx.say({ngx.req.get_body_data()}) 26 | } 27 | } 28 | --- request 29 | POST /cached_prx 30 | requestbody 31 | --- response_body 32 | requestbody 33 | -------------------------------------------------------------------------------- /t/02-integration/req_method.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: GET 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /req_method_1_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler():run() 20 | } 21 | } 22 | location /req_method_1 { 23 | content_by_lua_block { 24 | ngx.header["Cache-Control"] = "max-age=3600" 25 | ngx.header["Etag"] = "req_method_1" 26 | ngx.say(ngx.req.get_method()) 27 | } 28 | } 29 | --- request 30 | GET /req_method_1_prx 31 | --- response_body 32 | GET 33 | --- no_error_log 34 | [error] 35 | 36 | 37 | === TEST 2: HEAD gets GET request 38 | --- http_config eval: $::HttpConfig 39 | --- config 40 | location /req_method_1 { 41 | rewrite ^(.*)_prx$ $1 break; 42 | content_by_lua_block { 43 | require("ledge").create_handler():run() 44 | } 45 | } 46 | --- request 47 | GET /req_method_1 48 | --- response_headers 49 | Etag: req_method_1 50 | --- no_error_log 51 | [error] 52 | 53 | 54 | === TEST 3: HEAD revalidate 55 | --- http_config eval: $::HttpConfig 56 | --- config 57 | location /req_method_1_prx { 58 | rewrite ^(.*)_prx$ $1 break; 59 | content_by_lua_block { 60 | require("ledge").create_handler():run() 61 | } 62 | } 63 | location /req_method_1 { 64 | content_by_lua_block { 65 | ngx.header["Cache-Control"] = "max-age=3600" 66 | ngx.header["Etag"] = "req_method_1" 67 | } 68 | } 69 | --- more_headers 70 | Cache-Control: max-age=0 71 | --- request 72 | HEAD /req_method_1_prx 73 | --- response_headers 74 | Etag: req_method_1 75 | --- no_error_log 76 | [error] 77 | 78 | 79 | === TEST 4: GET still has body 80 | --- http_config eval: $::HttpConfig 81 | --- config 82 | location /req_method_1 { 83 | content_by_lua_block { 84 | require("ledge").create_handler():run() 85 | } 86 | } 87 | --- request 88 | GET /req_method_1 89 | --- response_headers 90 | Etag: req_method_1 91 | --- response_body 92 | GET 93 | --- no_error_log 94 | [error] 95 | 96 | 97 | === TEST 5: POST does not get cached copy 98 | --- http_config eval: $::HttpConfig 99 | --- config 100 | location /req_method_1_prx { 101 | rewrite ^(.*)_prx$ $1 break; 102 | content_by_lua_block { 103 | require("ledge").create_handler():run() 104 | } 105 | } 106 | location /req_method_1 { 107 | content_by_lua_block { 108 | ngx.header["Cache-Control"] = "max-age=3600" 109 | ngx.header["Etag"] = "req_method_posted" 110 | ngx.say(ngx.req.get_method()) 111 | } 112 | } 113 | --- request 114 | POST /req_method_1_prx 115 | --- response_headers 116 | Etag: req_method_posted 117 | --- response_body 118 | POST 119 | --- no_error_log 120 | [error] 121 | 122 | 123 | === TEST 6: GET uses cached POST response. 124 | --- http_config eval: $::HttpConfig 125 | --- config 126 | location /req_method_1 { 127 | content_by_lua_block { 128 | require("ledge").create_handler():run() 129 | } 130 | } 131 | --- request 132 | GET /req_method_1 133 | --- response_headers 134 | Etag: req_method_posted 135 | --- response_body 136 | POST 137 | --- no_error_log 138 | [error] 139 | 140 | 141 | === TEST 7: 501 on unrecognised method 142 | --- http_config eval: $::HttpConfig 143 | --- config 144 | location /req_method_1 { 145 | content_by_lua_block { 146 | require("ledge").create_handler():run() 147 | } 148 | } 149 | --- request 150 | FOOBAR /req_method_1 151 | --- error_code: 501 152 | --- no_error_log 153 | [error] 154 | -------------------------------------------------------------------------------- /t/02-integration/request_leak.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(extra_nginx_config => qq{ 7 | if_modified_since off; 8 | lua_check_client_abort on; 9 | }); 10 | 11 | no_long_string(); 12 | no_diff(); 13 | run_tests(); 14 | 15 | __DATA__ 16 | === TEST 1: Aborted request does not leak body into subsequent request 17 | --- http_config eval 18 | "$::HttpConfig" 19 | 20 | --- config 21 | location = /trigger { 22 | content_by_lua_block { 23 | 24 | -- Send broken request and close socket 25 | local broken_sock = ngx.socket.tcp() 26 | broken_sock:settimeout(5000) 27 | local ok, err = broken_sock:connect("127.0.0.1", ngx.var.server_port) 28 | broken_sock:send("POST /target?id=1 HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 16\r\n\r\n123\r\n") 29 | broken_sock:close() 30 | 31 | -- Send valid request and leave socket open 32 | local valid_sock = ngx.socket.tcp() 33 | valid_sock:settimeout(1000) 34 | local ok, err = valid_sock:connect("127.0.0.1", ngx.var.server_port) 35 | valid_sock:send("GET /target?id=2 HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n") 36 | 37 | -- Wait and read until end of headers 38 | local header_reader = valid_sock:receiveuntil("\r\n\r\n") 39 | local headers 40 | repeat 41 | headers = header_reader() 42 | until headers 43 | 44 | ngx.log(ngx.INFO, "HEADERS: ", headers) 45 | 46 | -- We're expecting chunked encoding 47 | if not headers:find("chunked") then 48 | ngx.log(ngx.ERR, "Expected chunked response but no header indicating such, failed!") 49 | ngx.exit(400) 50 | end 51 | 52 | -- Read chunk length as base16 53 | local chunk_len = tonumber(valid_sock:receive('*l'), 16) 54 | 55 | -- Read full chunk off wire 56 | local body, err, partial 57 | repeat 58 | body, err, partial = valid_sock:receive(chunk_len) 59 | until body or err 60 | 61 | valid_sock:close() 62 | 63 | if err then 64 | ngx.exit(400) 65 | end 66 | 67 | ngx.print(body) 68 | } 69 | } 70 | 71 | location /target { 72 | rewrite /target$ /origin break; 73 | content_by_lua_block { 74 | ngx.req.set_header("Host", "127.0.0.2") 75 | 76 | require("ledge").create_handler():run() 77 | } 78 | } 79 | 80 | location = /origin { 81 | content_by_lua_block { 82 | ngx.req.read_body() 83 | local args, err = ngx.req.get_uri_args() 84 | local data = ngx.req.get_body_data() or '' 85 | local method = ngx.req.get_method() or '' 86 | ngx.print("ORIGIN-", args['id'], "-", method, ":", data) 87 | ngx.exit(200) 88 | } 89 | } 90 | 91 | --- request 92 | GET /trigger 93 | --- response_body: ORIGIN-2-GET: 94 | -------------------------------------------------------------------------------- /t/02-integration/response.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Header case insensitivity 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /response_1_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | local handler = require("ledge").create_handler() 20 | handler:bind("after_upstream_request", function(res) 21 | if res.header["X-tesT"] == "1" then 22 | res.header["x-TESt"] = "2" 23 | end 24 | 25 | if res.header["X-TEST"] == "2" then 26 | res.header["x-test"] = "3" 27 | end 28 | end) 29 | handler:run() 30 | } 31 | } 32 | location /response_1 { 33 | content_by_lua_block { 34 | ngx.header["X-Test"] = "1" 35 | ngx.say("OK") 36 | } 37 | } 38 | --- request 39 | GET /response_1_prx 40 | --- response_headers 41 | X-Test: 3 42 | --- no_error_log 43 | [error] 44 | 45 | 46 | === TEST 2: TTL from s-maxage (overrides max-age / Expires) 47 | --- http_config eval: $::HttpConfig 48 | --- config 49 | location /response_2_prx { 50 | rewrite ^(.*)_prx$ $1 break; 51 | content_by_lua_block { 52 | local handler = require("ledge").create_handler() 53 | handler:bind("before_serve", function(res) 54 | res.header["X-TTL"] = res:ttl() 55 | end) 56 | handler:run() 57 | } 58 | } 59 | location /response_2 { 60 | content_by_lua_block { 61 | ngx.header["Expires"] = ngx.http_time(ngx.time() + 300) 62 | ngx.header["Cache-Control"] = "max-age=600, s-maxage=1200" 63 | ngx.say("OK") 64 | } 65 | } 66 | --- more_headers 67 | Cache-Control: no-cache 68 | --- request 69 | GET /response_2_prx 70 | --- response_headers 71 | X-TTL: 1200 72 | --- no_error_log 73 | [error] 74 | 75 | 76 | === TEST 3: TTL from max-age (overrides Expires) 77 | --- http_config eval: $::HttpConfig 78 | --- config 79 | location /response_3_prx { 80 | rewrite ^(.*)_prx$ $1 break; 81 | content_by_lua_block { 82 | local handler = require("ledge").create_handler() 83 | handler:bind("before_serve", function(res) 84 | res.header["X-TTL"] = res:ttl() 85 | end) 86 | handler:run() 87 | } 88 | } 89 | location /response_3 { 90 | content_by_lua_block { 91 | ngx.header["Expires"] = ngx.http_time(ngx.time() + 300) 92 | ngx.header["Cache-Control"] = "max-age=600" 93 | ngx.say("OK") 94 | } 95 | } 96 | --- more_headers 97 | Cache-Control: no-cache 98 | --- request 99 | GET /response_3_prx 100 | --- response_headers 101 | X-TTL: 600 102 | --- no_error_log 103 | [error] 104 | 105 | 106 | === TEST 4: TTL from Expires 107 | --- http_config eval: $::HttpConfig 108 | --- config 109 | location /response_4_prx { 110 | rewrite ^(.*)_prx$ $1 break; 111 | content_by_lua_block { 112 | local handler = require("ledge").create_handler() 113 | handler:bind("before_serve", function(res) 114 | res.header["X-TTL"] = res:ttl() 115 | end) 116 | handler:run() 117 | } 118 | } 119 | location /response_4 { 120 | content_by_lua_block { 121 | ngx.header["Expires"] = ngx.http_time(ngx.time() + 300) 122 | ngx.say("OK") 123 | } 124 | } 125 | --- more_headers 126 | Cache-Control: no-cache 127 | --- request 128 | GET /response_4_prx 129 | --- response_headers 130 | X-TTL: 300 131 | --- no_error_log 132 | [error] 133 | 134 | 135 | === TEST 4b: TTL from Expires, when there are multiple Expires headers 136 | --- http_config eval: $::HttpConfig 137 | --- config 138 | location /response_4b_prx { 139 | rewrite ^(.*)_prx$ $1 break; 140 | content_by_lua_block { 141 | local handler = require("ledge").create_handler() 142 | handler:bind("before_serve", function(res) 143 | res.header["X-TTL"] = res:ttl() 144 | end) 145 | handler:run() 146 | } 147 | } 148 | location /response_4b { 149 | set $ttl_1 0; 150 | set $ttl_2 0; 151 | access_by_lua_block { 152 | ngx.var.ttl_1 = ngx.http_time(ngx.time() + 300) 153 | ngx.var.ttl_2 = ngx.http_time(ngx.time() + 100) 154 | } 155 | add_header Expires $ttl_1; 156 | add_header Expires $ttl_2; 157 | echo "OK"; 158 | } 159 | --- more_headers 160 | Cache-Control: no-cache 161 | --- request 162 | GET /response_4b_prx 163 | --- response_headers 164 | X-TTL: 100 165 | --- no_error_log 166 | [error] 167 | -------------------------------------------------------------------------------- /t/02-integration/stale-if-error.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Prime cache for subsequent tests 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /stale_if_error_1_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler():run() 20 | } 21 | } 22 | location /stale_if_error_1 { 23 | content_by_lua_block { 24 | ngx.header["Cache-Control"] = 25 | "max-age=3600, s-maxage=60, stale-if-error=60" 26 | ngx.say("TEST 1") 27 | } 28 | } 29 | --- more_headers 30 | Cache-Control: no-cache 31 | --- request 32 | GET /stale_if_error_1_prx 33 | --- response_body 34 | TEST 1 35 | --- response_headers_like 36 | X-Cache: MISS from .* 37 | --- no_error_log 38 | [error] 39 | 40 | 41 | === TEST 1b: Assert standard non-stale behaviours are unaffected. 42 | --- http_config eval: $::HttpConfig 43 | --- config 44 | location /stale_if_error_1_prx { 45 | rewrite ^(.*)_prx$ $1 break; 46 | content_by_lua_block { 47 | require("ledge").create_handler():run() 48 | } 49 | } 50 | location /stale_if_error_1 { 51 | return 500; 52 | } 53 | --- more_headers eval 54 | [ 55 | "Cache-Control: no-cache", 56 | "Cache-Control: no-store", 57 | "Pragma: no-cache", 58 | "" 59 | ] 60 | --- request eval 61 | [ 62 | "GET /stale_if_error_1_prx", 63 | "GET /stale_if_error_1_prx", 64 | "GET /stale_if_error_1_prx", 65 | "GET /stale_if_error_1_prx" 66 | ] 67 | --- error_code eval 68 | [ 69 | 500, 70 | 500, 71 | 500, 72 | 200 73 | ] 74 | --- raw_response_headers_unlike eval 75 | [ 76 | "Warning: .*", 77 | "Warning: .*", 78 | "Warning: .*", 79 | "Warning: .*" 80 | ] 81 | --- no_error_log 82 | [error] 83 | 84 | 85 | === TEST 2: Prime cache and expire it 86 | --- http_config eval: $::HttpConfig 87 | --- config 88 | location /stale_if_error_2_prx { 89 | rewrite ^(.*)_prx$ $1 break; 90 | content_by_lua_block { 91 | local handler = require("ledge").create_handler() 92 | handler:bind("before_save", function(res) 93 | res.header["Cache-Control"] = 94 | "max-age=0, s-maxage=0, stale-if-error=60" 95 | end) 96 | handler:run() 97 | } 98 | } 99 | location /stale_if_error_2 { 100 | content_by_lua_block { 101 | ngx.header["Cache-Control"] = 102 | "max-age=3600, s-maxage=60, stale-if-error=60" 103 | ngx.print("TEST 2") 104 | } 105 | } 106 | --- more_headers 107 | Cache-Control: no-cache 108 | --- request 109 | GET /stale_if_error_2_prx 110 | --- response_body: TEST 2 111 | --- response_headers_like 112 | X-Cache: MISS from .* 113 | --- wait: 2 114 | --- no_error_log 115 | [error] 116 | 117 | 118 | === TEST 2b: Request does not accept stale, for different reasons 119 | --- http_config eval: $::HttpConfig 120 | --- config 121 | location /stale_if_error_2_prx { 122 | rewrite ^(.*)_prx$ $1 break; 123 | content_by_lua_block { 124 | require("ledge").create_handler():run() 125 | } 126 | } 127 | location /stale_if_error_2 { 128 | return 500; 129 | } 130 | --- more_headers eval 131 | [ 132 | "Cache-Control: min-fresh=5", 133 | "Cache-Control: max-age=1", 134 | "Cache-Control: max-stale=1" 135 | ] 136 | --- request eval 137 | [ 138 | "GET /stale_if_error_2_prx", 139 | "GET /stale_if_error_2_prx", 140 | "GET /stale_if_error_2_prx" 141 | ] 142 | --- error_code eval 143 | [ 144 | 500, 145 | 500, 146 | 500 147 | ] 148 | --- raw_response_headers_unlike eval 149 | [ 150 | "Warning: .*", 151 | "Warning: .*", 152 | "Warning: .*" 153 | ] 154 | --- response_body_unlike eval 155 | [ 156 | "TEST 2", 157 | "TEST 2", 158 | "TEST 2", 159 | ] 160 | --- no_error_log 161 | [error] 162 | 163 | 164 | === TEST 2c: Request accepts stale 165 | --- http_config eval: $::HttpConfig 166 | --- config 167 | location /stale_if_error_2_prx { 168 | rewrite ^(.*)_prx$ $1 break; 169 | content_by_lua_block { 170 | require("ledge").create_handler():run() 171 | } 172 | } 173 | location /stale_if_error_2 { 174 | return 500; 175 | } 176 | --- more_headers eval 177 | [ 178 | "Cache-Control: max-age=99999", 179 | "" 180 | ] 181 | --- request eval 182 | [ 183 | "GET /stale_if_error_2_prx", 184 | "GET /stale_if_error_2_prx" 185 | ] 186 | --- response_body eval 187 | [ 188 | "TEST 2", 189 | "TEST 2" 190 | ] 191 | --- response_headers_like eval 192 | [ 193 | "X-Cache: HIT from .*", 194 | "X-Cache: HIT from .*" 195 | ] 196 | --- raw_response_headers_like eval 197 | [ 198 | "Warning: 112 .*", 199 | "Warning: 112 .*" 200 | ] 201 | --- no_error_log 202 | [error] 203 | 204 | 205 | === TEST 4: Prime cache and expire it 206 | --- http_config eval: $::HttpConfig 207 | --- config 208 | location /stale_if_error_4_prx { 209 | rewrite ^(.*)_prx$ $1 break; 210 | content_by_lua_block { 211 | local handler = require("ledge").create_handler() 212 | handler:bind("before_save", function(res) 213 | res.header["Cache-Control"] = 214 | "max-age=0, s-maxage=0, stale-if-error=60, must-revalidate" 215 | end) 216 | handler:run() 217 | } 218 | } 219 | location /stale_if_error_4 { 220 | content_by_lua_block { 221 | ngx.header["Cache-Control"] = 222 | "max-age=3600, s-maxage=60, stale-if-error=60, must-revalidate" 223 | ngx.say("TEST 2") 224 | } 225 | } 226 | --- more_headers 227 | Cache-Control: no-cache 228 | --- request 229 | GET /stale_if_error_4_prx 230 | --- response_body 231 | TEST 2 232 | --- response_headers_like 233 | X-Cache: MISS from .* 234 | --- no_error_log 235 | [error] 236 | 237 | 238 | === TEST 4b: Response cannot be served stale (must-revalidate) 239 | --- http_config eval: $::HttpConfig 240 | --- config 241 | location /stale_if_error_4_prx { 242 | rewrite ^(.*)_prx$ $1 break; 243 | content_by_lua_block { 244 | require("ledge").create_handler():run() 245 | } 246 | } 247 | location /stale_if_error_4 { 248 | return 500; 249 | } 250 | --- request 251 | GET /stale_if_error_4_prx 252 | --- error_code: 500 253 | --- raw_response_headers_unlike 254 | Warning: .* 255 | --- no_error_log 256 | [error] 257 | 258 | 259 | === TEST 4c: Prime cache (with valid stale config + proxy-revalidate) and expire 260 | --- http_config eval: $::HttpConfig 261 | --- config 262 | location /stale_if_error_4_prx { 263 | rewrite ^(.*)_prx$ $1 break; 264 | content_by_lua_block { 265 | local handler = require("ledge").create_handler() 266 | handler:bind("before_save", function(res) 267 | res.header["Cache-Control"] = 268 | "max-age=0, s-maxage=0, stale-if-error=60, proxy-revalidate" 269 | end) 270 | handler:run() 271 | } 272 | } 273 | location /stale_if_error_4 { 274 | content_by_lua_block { 275 | ngx.header["Cache-Control"] = 276 | "max-age=3600, s-maxage=60, stale-if-error=60, proxy-revalidate" 277 | ngx.say("TEST 2") 278 | } 279 | } 280 | --- more_headers 281 | Cache-Control: no-cache 282 | --- request 283 | GET /stale_if_error_4_prx 284 | --- response_body 285 | TEST 2 286 | --- response_headers_like 287 | X-Cache: MISS from .* 288 | --- no_error_log 289 | [error] 290 | 291 | 292 | === TEST 4d: Response cannot be served stale (proxy-revalidate) 293 | --- http_config eval: $::HttpConfig 294 | --- config 295 | location /stale_if_error_4_prx { 296 | rewrite ^(.*)_prx$ $1 break; 297 | content_by_lua_block { 298 | require("ledge").create_handler():run() 299 | } 300 | } 301 | location /stale_if_error_4 { 302 | return 500; 303 | } 304 | --- request 305 | GET /stale_if_error_4_prx 306 | --- error_code: 500 307 | --- raw_response_headers_unlike 308 | Warning: .* 309 | --- no_error_log 310 | [error] 311 | -------------------------------------------------------------------------------- /t/02-integration/upstream.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | $ENV{TEST_NGINX_HTML_DIR} ||= html_dir(); 7 | $ENV{TEST_NGINX_SOCKET_DIR} ||= $ENV{TEST_NGINX_HTML_DIR}; 8 | 9 | our $HttpConfig = LedgeEnv::http_config(); 10 | 11 | no_long_string(); 12 | no_diff(); 13 | run_tests(); 14 | 15 | __DATA__ 16 | === TEST 1: Short read timeout results in error 524. 17 | --- http_config eval: $::HttpConfig 18 | --- config 19 | location /upstream_prx { 20 | rewrite ^(.*)_prx$ $1 break; 21 | content_by_lua_block { 22 | require("ledge").create_handler({ 23 | upstream_send_timeout = 5000, 24 | upstream_connect_timeout = 5000, 25 | upstream_read_timeout = 100, 26 | }):run() 27 | } 28 | } 29 | location /upstream { 30 | content_by_lua_block { 31 | ngx.sleep(1) 32 | ngx.say("OK") 33 | } 34 | } 35 | --- request 36 | GET /upstream_prx 37 | --- error_code: 524 38 | --- response_body 39 | --- error_log 40 | timeout 41 | 42 | 43 | === TEST 2: No upstream results in a 503. 44 | --- http_config eval: $::HttpConfig 45 | --- config 46 | location /upstream_prx { 47 | rewrite ^(.*)_prx$ $1 break; 48 | content_by_lua_block { 49 | require("ledge").create_handler({ 50 | upstream_host = "", 51 | }):run() 52 | } 53 | } 54 | --- request 55 | GET /upstream_prx 56 | --- error_code: 503 57 | --- response_body 58 | --- error_log 59 | upstream connection failed: 60 | 61 | 62 | === TEST 3: No port results in 503 63 | --- http_config eval: $::HttpConfig 64 | --- config 65 | location /upstream_prx { 66 | rewrite ^(.*)_prx$ $1 break; 67 | content_by_lua_block { 68 | require("ledge").create_handler({ 69 | upstream_host = "127.0.0.1", 70 | upstream_port = "", 71 | }):run() 72 | } 73 | } 74 | --- request 75 | GET /upstream_prx 76 | --- error_code: 503 77 | --- response_body 78 | --- error_log 79 | upstream connection failed: 80 | 81 | 82 | === TEST 4: No port with unix socket works 83 | --- http_config eval: $::HttpConfig 84 | --- config 85 | listen unix:$TEST_NGINX_SOCKET_DIR/nginx.sock; 86 | location /upstream_prx { 87 | rewrite ^(.*)_prx$ $1 break; 88 | content_by_lua_block { 89 | require("ledge").create_handler({ 90 | upstream_host = "unix:$TEST_NGINX_SOCKET_DIR/nginx.sock", 91 | upstream_port = "", 92 | }):run() 93 | } 94 | } 95 | location /upstream { 96 | echo "OK"; 97 | } 98 | --- request 99 | GET /upstream_prx 100 | --- error_code: 200 101 | --- response_body 102 | OK 103 | --- no_error_log 104 | [error] 105 | -------------------------------------------------------------------------------- /t/02-integration/upstream_client.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(extra_nginx_config => qq{ 7 | lua_shared_dict test_upstream_dict 1m; 8 | }, extra_lua_config => qq{ 9 | function create_upstream_client(config) 10 | -- Defaults 11 | config = config or {} 12 | config["timeout"] = config["timeout"] or 100 13 | config["read_timeout"] = config["read_timeout"] or 500 14 | config["host"] = config["host"] or "$LedgeEnv::nginx_host" 15 | config["port"] = config["port"] or $LedgeEnv::nginx_port 16 | 17 | return function(handler) 18 | local httpc = require("resty.http").new() 19 | httpc:set_timeout(config.timeout) 20 | 21 | local ok, err = httpc:connect( 22 | config.host, 23 | config.port 24 | ) 25 | 26 | if not ok then 27 | ngx.log(ngx.ERR, "upstream client connection failed: ", err) 28 | return nil 29 | end 30 | 31 | httpc:set_timeout(config.read_timeout) 32 | 33 | handler.upstream_client = httpc 34 | end 35 | end 36 | 37 | require("ledge").bind("before_upstream_connect", function(handler) 38 | if ngx.req.get_uri_args()["skip_init"] then 39 | -- do nothing 40 | else 41 | -- create handler and pass through res 42 | create_upstream_client()(handler) 43 | end 44 | end) 45 | 46 | require("ledge").set_handler_defaults({ 47 | upstream_host = "", 48 | upstream_port = 9999, 49 | }) 50 | }, run_worker => 1); 51 | 52 | no_long_string(); 53 | no_diff(); 54 | run_tests(); 55 | 56 | __DATA__ 57 | === TEST 1: Sanity, response returned with upstream_client configured 58 | --- http_config eval: $::HttpConfig 59 | --- config 60 | location /upstream_client_prx { 61 | rewrite ^(.*)_prx$ $1 break; 62 | content_by_lua_block { 63 | require("ledge").create_handler():run() 64 | } 65 | } 66 | location /upstream_client { 67 | content_by_lua_block { 68 | ngx.say("OK") 69 | } 70 | } 71 | --- request 72 | GET /upstream_client_prx 73 | --- no_error_log 74 | [error] 75 | --- error_code: 200 76 | --- response_body 77 | OK 78 | 79 | === TEST 1b: Sanity, response returned with upstream_client configured at runtime 80 | --- http_config eval: $::HttpConfig 81 | --- config 82 | location /upstream_client_prx { 83 | rewrite ^(.*)_prx$ $1 break; 84 | content_by_lua_block { 85 | local h = require("ledge").create_handler() 86 | h:bind("before_upstream_connect", create_upstream_client() ) 87 | h:run() 88 | } 89 | } 90 | location /upstream_client { 91 | content_by_lua_block { 92 | ngx.say("OK") 93 | } 94 | } 95 | --- request 96 | GET /upstream_client_prx?skip_init=true 97 | --- no_error_log 98 | [error] 99 | --- error_code: 200 100 | --- response_body 101 | OK 102 | 103 | === TEST 2: Short read timeout results in error 524. 104 | --- http_config eval: $::HttpConfig 105 | --- config 106 | location /upstream_client_prx { 107 | rewrite ^(.*)_prx$ $1 break; 108 | content_by_lua_block { 109 | require("ledge").create_handler():run() 110 | } 111 | } 112 | location /upstream_client { 113 | content_by_lua_block { 114 | ngx.sleep(1) 115 | ngx.say("OK") 116 | } 117 | } 118 | --- request 119 | GET /upstream_client_prx 120 | --- error_code: 524 121 | --- response_body 122 | --- error_log 123 | timeout 124 | 125 | 126 | === TEST 3: No upstream results in a 503. 127 | --- http_config eval: $::HttpConfig 128 | --- config 129 | location /upstream_client_prx { 130 | rewrite ^(.*)_prx$ $1 break; 131 | content_by_lua_block { 132 | local h = require("ledge").create_handler() 133 | h:bind("before_upstream_connect", function(handler) 134 | handler.upstream_client = {} 135 | end) 136 | h:run() 137 | } 138 | } 139 | --- request 140 | GET /upstream_client_prx 141 | --- error_code: 503 142 | --- response_body 143 | --- error_log 144 | upstream connection failed 145 | -------------------------------------------------------------------------------- /t/02-integration/via_header.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use FindBin; 3 | use lib "$FindBin::Bin/.."; 4 | use LedgeEnv; 5 | 6 | our $HttpConfig = LedgeEnv::http_config(); 7 | 8 | no_long_string(); 9 | no_diff(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | === TEST 1: Ledge version advertised by default 14 | --- http_config eval: $::HttpConfig 15 | --- config 16 | location /t_prx { 17 | rewrite ^(.*)_prx$ $1 break; 18 | content_by_lua_block { 19 | require("ledge").create_handler():run() 20 | } 21 | } 22 | location /t { 23 | echo "ORIGIN"; 24 | } 25 | --- request 26 | GET /t_prx 27 | --- response_headers_like 28 | Via: \d+\.\d+ .+ \(ledge/\d+\.\d+[\.\d]*\) 29 | --- no_error_log 30 | [error] 31 | 32 | 33 | === TEST 2: Ledge version not advertised 34 | --- http_config eval: $::HttpConfig 35 | --- config 36 | location /t_prx { 37 | rewrite ^(.*)_prx$ $1 break; 38 | content_by_lua_block { 39 | require("ledge").create_handler({ 40 | advertise_ledge = false, 41 | }):run() 42 | } 43 | } 44 | location /t { 45 | echo "ORIGIN"; 46 | } 47 | --- request 48 | GET /t_prx 49 | --- raw_response_headers_unlike: Via: \d+\.\d+ .+ \(ledge/\d+\.\d+[\.\d]*\) 50 | --- no_error_log 51 | [error] 52 | 53 | 54 | === TEST 3: Via header uses visible_hostname config 55 | --- http_config eval: $::HttpConfig 56 | --- config 57 | location /t_prx { 58 | rewrite ^(.*)_prx$ $1 break; 59 | content_by_lua_block { 60 | require("ledge").create_handler({ 61 | visible_hostname = "ledge.example.com" 62 | }):run() 63 | } 64 | } 65 | location /t { 66 | echo "ORIGIN"; 67 | } 68 | --- request 69 | GET /t_prx 70 | --- response_headers_like 71 | Via: \d+\.\d+ ledge.example.com:\d+ \(ledge/\d+\.\d+[\.\d]*\) 72 | --- no_error_log 73 | [error] 74 | 75 | 76 | === TEST 4: Via header from upstream 77 | --- http_config eval: $::HttpConfig 78 | --- config 79 | location /t_prx { 80 | rewrite ^(.*)_prx$ $1 break; 81 | content_by_lua_block { 82 | require("ledge").create_handler():run() 83 | } 84 | } 85 | location /t { 86 | content_by_lua_block { 87 | ngx.header["Via"] = "1.1 foo" 88 | } 89 | } 90 | --- request 91 | GET /t_prx 92 | --- more_headers 93 | Cache-Control: no-cache 94 | --- response_headers_like 95 | Via: \d+\.\d+ .+ \(ledge/\d+\.\d+[\.\d]*\), \d+\.\d+ foo 96 | --- no_error_log 97 | [error] 98 | 99 | 100 | === TEST 5: Erroneous multiple Via headers from upstream 101 | --- http_config eval: $::HttpConfig 102 | --- config 103 | location /t_prx { 104 | rewrite ^(.*)_prx$ $1 break; 105 | content_by_lua_block { 106 | require("ledge").create_handler({ 107 | upstream_port = 1985, 108 | }):run() 109 | } 110 | } 111 | --- tcp_listen: 1985 112 | --- tcp_reply 113 | HTTP/1.1 200 OK 114 | Content-Length: 2 115 | Content-Type: text/plain 116 | Via: 1.1 foo 117 | Via: 1.1 foo.bar 118 | 119 | OK 120 | 121 | --- request 122 | GET /t_prx 123 | --- more_headers 124 | Cache-Control: no-cache 125 | --- response_body: OK 126 | --- response_headers_like 127 | Via: 1.1 .+ \(ledge/\d+\.\d+[\.\d]*\), 1.1 foo, 1.1 foo.bar 128 | --- no_error_log 129 | [error] 130 | -------------------------------------------------------------------------------- /t/03-sentinel/01-master_up.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use Cwd qw(cwd); 3 | 4 | my $pwd = cwd(); 5 | 6 | $ENV{TEST_NGINX_PORT} ||= 1984; 7 | $ENV{TEST_LEDGE_REDIS_DATABASE} ||= 2; 8 | $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE} ||= 3; 9 | $ENV{TEST_LEDGE_SENTINEL_MASTER_NAME} ||= 'mymaster'; 10 | $ENV{TEST_LEDGE_SENTINEL_PORT} ||= 6381; 11 | $ENV{TEST_COVERAGE} ||= 0; 12 | 13 | our $HttpConfig = qq{ 14 | lua_package_path "./lib/?.lua;../lua-resty-redis-connector/lib/?.lua;../lua-resty-qless/lib/?.lua;../lua-resty-http/lib/?.lua;../lua-ffi-zlib/lib/?.lua;;"; 15 | 16 | init_by_lua_block { 17 | if $ENV{TEST_COVERAGE} == 1 then 18 | require("luacov.runner").init() 19 | end 20 | 21 | local db = $ENV{TEST_LEDGE_REDIS_DATABASE} 22 | local qless_db = $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE} 23 | local master_name = '$ENV{TEST_LEDGE_SENTINEL_MASTER_NAME}' 24 | local sentinel_port = $ENV{TEST_LEDGE_SENTINEL_PORT} 25 | 26 | local redis_connector_params = { 27 | url = "sentinel://" .. master_name .. ":m/" .. tostring(db), 28 | sentinels = { 29 | { host = "127.0.0.1", port = sentinel_port }, 30 | }, 31 | } 32 | 33 | require("ledge").configure({ 34 | redis_connector_params = redis_connector_params, 35 | qless_db = $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE}, 36 | }) 37 | 38 | require("ledge").set_handler_defaults({ 39 | upstream_host = "127.0.0.1", 40 | upstream_port = $ENV{TEST_NGINX_PORT}, 41 | storage_driver_config = { 42 | redis_connector_params = redis_connector_params, 43 | } 44 | }) 45 | } 46 | 47 | init_worker_by_lua_block { 48 | require("ledge").create_worker():run() 49 | } 50 | 51 | }; 52 | 53 | no_long_string(); 54 | run_tests(); 55 | 56 | __DATA__ 57 | === TEST 1: Prime cache 58 | --- http_config eval: $::HttpConfig 59 | --- config 60 | location /sentinel_1_prx { 61 | rewrite ^(.*)_prx$ $1 break; 62 | content_by_lua_block { 63 | require("ledge").create_handler():run() 64 | } 65 | } 66 | location /sentinel_1 { 67 | content_by_lua_block { 68 | ngx.header["Cache-Control"] = "max-age=3600" 69 | ngx.say("OK") 70 | } 71 | } 72 | --- request 73 | GET /sentinel_1_prx 74 | --- response_body 75 | OK 76 | --- no_error_log 77 | [error] 78 | 79 | 80 | === TEST 2: create_redis_slave_connection 81 | --- http_config eval: $::HttpConfig 82 | --- config 83 | location /sentinel_1_prx { 84 | rewrite ^(.*)_prx$ $1 break; 85 | content_by_lua_block { 86 | local slave, err = require("ledge").create_redis_slave_connection() 87 | assert(slave and not err, 88 | "create_redis_slave_connection should return positively") 89 | 90 | assert(slave:role()[1] == "slave", "role should be slave") 91 | } 92 | } 93 | --- request 94 | GET /sentinel_1_prx 95 | --- no_error_log 96 | [error] 97 | 98 | 99 | === TEST 4a: Prime cache 100 | --- http_config eval: $::HttpConfig 101 | --- config 102 | location /sentinel_3_prx { 103 | rewrite ^(.*)_prx$ $1 break; 104 | content_by_lua_block { 105 | require("ledge").create_handler():run() 106 | } 107 | } 108 | location /sentinel_3 { 109 | content_by_lua_block { 110 | ngx.header["Cache-Control"] = "max-age=3600" 111 | ngx.say("OK") 112 | } 113 | } 114 | --- request 115 | GET /sentinel_3_prx 116 | --- response_body 117 | OK 118 | --- no_error_log 119 | [error] 120 | 121 | 122 | === TEST 3: Wildcard Purge (scan on slave) 123 | --- http_config eval: $::HttpConfig 124 | --- config 125 | location /sentinel_3 { 126 | content_by_lua_block { 127 | require("ledge").create_handler({ 128 | keyspace_scan_count = 1, 129 | }):run() 130 | } 131 | } 132 | --- request 133 | PURGE /sentinel_3* 134 | --- wait: 1 135 | --- no_error_log 136 | [error] 137 | --- error_code: 200 138 | -------------------------------------------------------------------------------- /t/03-sentinel/02-master_down.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use Cwd qw(cwd); 3 | 4 | my $pwd = cwd(); 5 | 6 | $ENV{TEST_NGINX_PORT} ||= 1984; 7 | $ENV{TEST_LEDGE_REDIS_DATABASE} ||= 2; 8 | $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE} ||= 3; 9 | $ENV{TEST_LEDGE_SENTINEL_MASTER_NAME} ||= 'mymaster'; 10 | $ENV{TEST_LEDGE_SENTINEL_PORT} ||= 6381; 11 | $ENV{TEST_COVERAGE} ||= 0; 12 | 13 | our $HttpConfig = qq{ 14 | lua_package_path "./lib/?.lua;../lua-resty-redis-connector/lib/?.lua;../lua-resty-qless/lib/?.lua;../lua-resty-http/lib/?.lua;../lua-ffi-zlib/lib/?.lua;;"; 15 | 16 | lua_socket_log_errors Off; 17 | init_by_lua_block { 18 | 19 | if $ENV{TEST_COVERAGE} == 1 then 20 | require("luacov.runner").init() 21 | end 22 | 23 | local db = $ENV{TEST_LEDGE_REDIS_DATABASE} 24 | local qless_db = $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE} 25 | local master_name = '$ENV{TEST_LEDGE_SENTINEL_MASTER_NAME}' 26 | local sentinel_port = $ENV{TEST_LEDGE_SENTINEL_PORT} 27 | 28 | local redis_connector_params = { 29 | url = "sentinel://" .. master_name .. ":s/" .. tostring(db), 30 | sentinels = { 31 | { host = "127.0.0.1", port = sentinel_port }, 32 | }, 33 | } 34 | 35 | require("ledge").configure({ 36 | redis_connector_params = redis_connector_params, 37 | qless_db = $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE}, 38 | }) 39 | 40 | require("ledge").set_handler_defaults({ 41 | upstream_host = "127.0.0.1", 42 | upstream_port = $ENV{TEST_NGINX_PORT}, 43 | storage_driver_config = { 44 | redis_connector_params = redis_connector_params, 45 | } 46 | }) 47 | } 48 | 49 | }; 50 | 51 | no_long_string(); 52 | run_tests(); 53 | 54 | __DATA__ 55 | === TEST 1: Read from cache (primed in previous test file) 56 | --- http_config eval: $::HttpConfig 57 | --- config 58 | location /sentinel_1_prx { 59 | rewrite ^(.*)_prx$ $1 break; 60 | content_by_lua_block { 61 | require("ledge").create_handler():run() 62 | } 63 | } 64 | location /sentinel_1 { 65 | echo "ORIGIN"; 66 | } 67 | --- request 68 | GET /sentinel_1_prx 69 | --- response_body 70 | OK 71 | --- no_error_log 72 | [error] 73 | 74 | 75 | === TEST 2: The write will fail, but well still get a 200 with our new content. 76 | --- http_config eval: $::HttpConfig 77 | --- config 78 | location /sentinel_2_prx { 79 | rewrite ^(.*)_prx$ $1 break; 80 | content_by_lua_block { 81 | require("ledge").create_handler():run() 82 | } 83 | } 84 | location /sentinel_2 { 85 | content_by_lua_block { 86 | ngx.header["Cache-Control"] = "max-age=3600" 87 | ngx.say("TEST 2") 88 | } 89 | } 90 | --- request 91 | GET /sentinel_2_prx 92 | --- response_body 93 | TEST 2 94 | --- error_log 95 | READONLY You can't write against a read only slave. 96 | 97 | 98 | === TEST 2b: The write will fail, but we still get a 200 with our content. 99 | --- http_config eval: $::HttpConfig 100 | --- config 101 | location /sentinel_2_prx { 102 | rewrite ^(.*)_prx$ $1 break; 103 | content_by_lua_block { 104 | require("ledge").create_handler():run() 105 | } 106 | } 107 | location /sentinel_2 { 108 | content_by_lua_block { 109 | ngx.header["Cache-Control"] = "max-age=3600" 110 | ngx.say("TEST 2b") 111 | } 112 | } 113 | --- request 114 | GET /sentinel_2_prx 115 | --- response_body 116 | TEST 2b 117 | --- error_log 118 | READONLY You can't write against a read only slave. 119 | -------------------------------------------------------------------------------- /t/03-sentinel/03-slave_promoted.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use Cwd qw(cwd); 3 | 4 | my $pwd = cwd(); 5 | 6 | $ENV{TEST_NGINX_PORT} ||= 1984; 7 | $ENV{TEST_LEDGE_REDIS_DATABASE} ||= 2; 8 | $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE} ||= 3; 9 | $ENV{TEST_LEDGE_SENTINEL_MASTER_NAME} ||= 'mymaster'; 10 | $ENV{TEST_LEDGE_SENTINEL_PORT} ||= 6381; 11 | $ENV{TEST_COVERAGE} ||= 0; 12 | 13 | our $HttpConfig = qq{ 14 | lua_package_path "./lib/?.lua;../lua-resty-redis-connector/lib/?.lua;../lua-resty-qless/lib/?.lua;../lua-resty-http/lib/?.lua;../lua-ffi-zlib/lib/?.lua;;"; 15 | 16 | init_by_lua_block { 17 | if $ENV{TEST_COVERAGE} == 1 then 18 | require("luacov.runner").init() 19 | end 20 | 21 | local db = $ENV{TEST_LEDGE_REDIS_DATABASE} 22 | local qless_db = $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE} 23 | local master_name = '$ENV{TEST_LEDGE_SENTINEL_MASTER_NAME}' 24 | local sentinel_port = $ENV{TEST_LEDGE_SENTINEL_PORT} 25 | 26 | local redis_connector_params = { 27 | url = "sentinel://" .. master_name .. ":a/" .. tostring(db), 28 | sentinels = { 29 | { host = "127.0.0.1", port = sentinel_port }, 30 | }, 31 | } 32 | 33 | require("ledge").configure({ 34 | redis_connector_params = redis_connector_params, 35 | qless_db = $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE}, 36 | }) 37 | 38 | require("ledge").set_handler_defaults({ 39 | upstream_host = "127.0.0.1", 40 | upstream_port = $ENV{TEST_NGINX_PORT}, 41 | storage_driver_config = { 42 | redis_connector_params = redis_connector_params, 43 | } 44 | }) 45 | } 46 | 47 | }; 48 | 49 | no_long_string(); 50 | run_tests(); 51 | 52 | __DATA__ 53 | === TEST 1: Read from cache (primed in previous test file) 54 | --- http_config eval: $::HttpConfig 55 | --- config 56 | location /sentinel_1 { 57 | content_by_lua_block { 58 | require("ledge").create_handler():run() 59 | } 60 | } 61 | --- request 62 | GET /sentinel_1 63 | --- response_body 64 | OK 65 | --- no_error_log 66 | [error] 67 | 68 | 69 | === TEST 2: The write will succeed, as our slave has been promoted. 70 | --- http_config eval: $::HttpConfig 71 | --- config 72 | location /sentinel_2_prx { 73 | rewrite ^(.*)_prx$ $1 break; 74 | content_by_lua_block { 75 | require("ledge").create_handler():run() 76 | } 77 | } 78 | location /sentinel_2 { 79 | content_by_lua_block { 80 | ngx.header["Cache-Control"] = "max-age=3600" 81 | ngx.say("TEST 2") 82 | } 83 | } 84 | --- request 85 | GET /sentinel_2_prx 86 | --- response_body 87 | TEST 2 88 | 89 | 90 | === TEST 2b: Test for cache hit. 91 | --- http_config eval: $::HttpConfig 92 | --- config 93 | location /sentinel_2 { 94 | content_by_lua_block { 95 | require("ledge").create_handler():run() 96 | } 97 | } 98 | --- request 99 | GET /sentinel_2 100 | --- response_body 101 | TEST 2 102 | -------------------------------------------------------------------------------- /t/LedgeEnv.pm: -------------------------------------------------------------------------------- 1 | package LedgeEnv; 2 | use strict; 3 | use warnings; 4 | use Exporter; 5 | 6 | our $nginx_host = $ENV{TEST_NGINX_HOST} || '127.0.0.1'; 7 | our $nginx_port = $ENV{TEST_NGINX_PORT} || 1984; 8 | our $test_coverage = $ENV{TEST_COVERAGE} || 0; 9 | 10 | our $redis_host = $ENV{TEST_LEDGE_REDIS_HOST} || '127.0.0.1'; 11 | our $redis_port = $ENV{TEST_LEDGE_REDIS_PORT} || 6379; 12 | our $redis_database = $ENV{TEST_LEDGE_REDIS_DATABASE} || 2; 13 | our $redis_qless_database = $ENV{TEST_LEDGE_REDIS_QLESS_DATABASE} || 3; 14 | 15 | sub http_config { 16 | my $extra_nginx_config = ""; 17 | my $extra_lua_config = ""; 18 | my $worker_config = ""; 19 | 20 | my (%args) = @_; 21 | 22 | if (defined $args{extra_nginx_config}) { 23 | $extra_nginx_config = $args{extra_nginx_config}; 24 | } 25 | 26 | if (defined $args{extra_lua_config}) { 27 | $extra_lua_config = $args{extra_lua_config}; 28 | } 29 | 30 | if ($args{run_worker}) { 31 | $worker_config = qq{ 32 | init_worker_by_lua_block { 33 | require("ledge").create_worker():run() 34 | } 35 | }; 36 | } 37 | 38 | return qq{ 39 | $extra_nginx_config 40 | 41 | lua_package_path "./lib/?.lua;./extlib/?.lua;;"; 42 | resolver local=on ipv6=off; 43 | 44 | init_by_lua_block { 45 | if $test_coverage == 1 then 46 | require("luacov.runner").init() 47 | end 48 | 49 | local REDIS_URL = "redis://$redis_host:$redis_port/$redis_database" 50 | 51 | require("ledge").configure({ 52 | redis_connector_params = { url = REDIS_URL }, 53 | qless_db = $redis_qless_database, 54 | }) 55 | 56 | require("ledge").set_handler_defaults({ 57 | upstream_host = "$nginx_host", 58 | upstream_port = $nginx_port, 59 | storage_driver_config = { 60 | redis_connector_params = { url = REDIS_URL }, 61 | }, 62 | }) 63 | 64 | $extra_lua_config; 65 | } 66 | 67 | $worker_config 68 | } 69 | } 70 | 71 | our @EXPORT = qw( http_config ); 72 | 73 | 1; 74 | -------------------------------------------------------------------------------- /t/cert/example.com.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZDCCAkwCCQC9pPAJEKdAJTANBgkqhkiG9w0BAQsFADCBiTELMAkGA1UEBhMC 3 | VUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9uMREwDwYDVQQKDAhT 4 | cXVpeiBVSzEQMA4GA1UECwwHSG9zdGluZzESMBAGA1UEAwwJRWRnZSBUZXN0MR8w 5 | HQYJKoZIhvcNAQkBFhBlZGdlQHNxdWl6LmNvLnVrMCAXDTE5MTExMjIyNDA0MFoY 6 | DzIxMTkxMDE5MjI0MDQwWjBcMQswCQYDVQQGEwJVSzETMBEGA1UECAwKU29tZS1T 7 | dGF0ZTEPMA0GA1UEBwwGTG9uZG9uMREwDwYDVQQKDAhTcXVpeiBVSzEUMBIGA1UE 8 | AwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDr 9 | jlyu9bPMemMkqfe0fi+HaLfXsYaMguJyzaOKIf11RAHG4Ptl3XHk4a6OrKR3MFuC 10 | MnGmOuPOAJPRwGJ2PMNX6g3dI0UsEqMxdOEadJsaP/kcV22OmVTDpErdbADItk8h 11 | tgCvo+QWUIIFCUMbd8t2nJpgusnhyfyhipwBiBTaiflANYFfINty8D39ohgHzg6j 12 | tTiOLf2jBEurFJkekb8bu2kWcDxFv0lpR4VurMpvaguxuAM2XhpQVjHzqhJ28AlG 13 | BJY8KV4OPZb3Qz+rZnojat3QKVoDIJc42pFRAUFTyanBF/m8ayZqOOZdH90t8bcj 14 | dLEKMgUBHB9vnDDOOVfnAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHbAH10eDzlq 15 | B3QXMHidAwZhaYEwnfqFINSYThLir9o8/WtgiMvhOaD5BXZNvuoePsYjZZyYIx14 16 | NF+xJe0Ijnh15RnAtNBDuw+NkGKVqtszdW1SNBkU9bH4rJXJYJOkHORkIRLRWlwl 17 | /YR/YpXOwPSKCIgl9K8H3FsuAbjNB+sjUsaSsbyTKbOVUB67BvjDSb0e0UZwSBtc 18 | 9wWLAdHL2gZQD+tsX/vEv1F0QdaKsBEjMfYCuLfk0Ov51hKLXfyrIG08T7Csm9Mf 19 | qvIku5Itl4AWNbGQZpXnbqtUHOF1OaWBe9i5xNoMtvv+WWqJcun9NQBsdMnpNMpE 20 | MF3NccQR5iA= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /t/cert/example.com.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA645crvWzzHpjJKn3tH4vh2i317GGjILics2jiiH9dUQBxuD7 3 | Zd1x5OGujqykdzBbgjJxpjrjzgCT0cBidjzDV+oN3SNFLBKjMXThGnSbGj/5HFdt 4 | jplUw6RK3WwAyLZPIbYAr6PkFlCCBQlDG3fLdpyaYLrJ4cn8oYqcAYgU2on5QDWB 5 | XyDbcvA9/aIYB84Oo7U4ji39owRLqxSZHpG/G7tpFnA8Rb9JaUeFbqzKb2oLsbgD 6 | Nl4aUFYx86oSdvAJRgSWPCleDj2W90M/q2Z6I2rd0ClaAyCXONqRUQFBU8mpwRf5 7 | vGsmajjmXR/dLfG3I3SxCjIFARwfb5wwzjlX5wIDAQABAoIBADOdBgHwJG1xg7fM 8 | 5lHONGvfLik85NZ091lgZa0mtXq0ZA9HzM4NL5+PM8hfW8oh9msY0n4x+ShyR/F1 9 | zh1KQyNITbFewRFfJBL6ITjCxBmEWvkyzvan8kLMBPtvZtyT1dL1JkFWD+wzx8mC 10 | tgmWviZHOixnwUSQFaLv1C8hujAID82wkoSiEhCgl+B69JxHG9zwsTiKmBnt8iTb 11 | URD38MPGxJkpGdzkWScb0nlSsbm8IZpPbEll0HU5UYjB0vkuv4tt5Ou23AqxTde0 12 | psC1WZYa4qyomycATX9+PykBFEA0Qkd56/oHnFMZTmPQKUevqtq3axMOeUc6vqjJ 13 | MEf5+8ECgYEA+MFDdhX5leLh0CxZf4PuHgaHTP9EITL9obU3Nh87fZYRlFVDjwpn 14 | 7ZMsAJw4f7uTXYH6j5e0oQ6KmCCk65Ak89/8roq+sbymm5AGLs07PGtUYKF1goq6 15 | JqhMBslWPD8rkWMiUzRtlf7yQq6iA2gOQTecRKKfbjYT9vNFPqkB51sCgYEA8mqv 16 | 3Y4ZUX6cCkAwsA+TEOqKOh52VCgJ3xtrL7iUjJ2hsfT8avM1TsvklSGob4/LWqPQ 17 | KN+EExaM3vULydxZ+WgSvOD73OyFSa7J25NdHdqFkyde2DC+f2aPa7zcRanuD1DD 18 | h6e6/Z3OGa2pZq7Ed5cW7gAljSLQJZIFNN65A2UCgYEAo+VSOX+JDoSKG8rcvPOD 19 | 9CyBAO4/SVB7ZAwt8G7rl3dE5eK3vIsypomNOGm1oBNKqRV2rR1bWbJnBoybnMlA 20 | T56IsceglSKi82QVbsix+sEMuw4ming05juEvAPz2YYVgpk6iG/GtElh/SVqgawR 21 | mE63m1E6kjb3OIJYYUyhgHkCgYEAr9USmvFnC+V56TWGGy4wziRQ/rb5vTENd/a7 22 | WHHZzeTIU/wO2sRt9imOM12mfsUeCzCm2/7EHdRNearkUhaybGVAsh++kBA+3aMa 23 | Z1oMQIswN/xmnwk8I8yQWuUyIJWRRyqdqNfQmgTMaXO9W+2IM/Yze44/ro+Byr6P 24 | aDnkmMECgYEAsHbqYEwD54neIKyNxlmYemybqqh63JVFp84VomJxMfHumVVW/UxQ 25 | Pt7jKgbsvO7ayBzETtSpaQ3ajCiiKPKlrupqJDb9yXwSpdfbf6fRamQLDvVWJNkD 26 | ZjLBiOUX5dS4Il0kEvUCeikgyFRssEFP6J5c2+Cbr5Gt12EnIQhu//I= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /t/cert/rootCA.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAy6v3eQFGXrSe97M/9ullfby4bg2jF+9h9p5P/wiv+Y9dTdk8 3 | DRR7M40laCqnmoXSSndAvHLb+lqeHs0GDkp+ofgBg8iP8p8IX0RQ33lqcrU65FJu 4 | ImMksswkYZLak78YpR1wqF4EMpHH6t7SKkPlFgULkg+vTpml83fFdnq2WVk8UOSx 5 | AsrllvVtxys8c8ny0hmBXqOWtISPQFYBNAy7N7uHLF8D/4/YMBHM2Vgp5oaMkvFX 6 | nsDyL6JWvnqTnz1kKjL9lbYHbxP7oziI74MIRvRzsw8Ou+OfWiYKTRZDlxfl+7Jr 7 | zOhxjbTpP/cOCw3ngBC6rrpdflDVuyf6r7rv2wIDAQABAoIBABtC1Uj5BrY+btiw 8 | wWsHKnJ+BCGW6bGWdQJRhluYihVZPx/gZ81IZIUt60faDbz9FHyrIZsXtKH55xgw 9 | URMwnWqIi4tcGQhciP5XYovG8JyR7WQKNHud0ZetA2GcCm2kMmRHYIDotJ8gLCYf 10 | 1PmbRNqBql7OgqR+pFvGOEP3gNjMf66m+A0jnSrXk2zLBGeo3hfEBpsWsm2fay5T 11 | Z8r5PlUn8rXSBkVCK9AGGf2Ngg07Z+2wj08Htn+hq3+kTNf65D/buVpkg9kNqrxm 12 | cDy1v182KBiM/jXjydoBt33GKxNkG4Ds6eH0W2hFzP8oOmYq4DCZ1BryUGhR+U6c 13 | izj6/SECgYEA8iw2e0blUsd2Esi5iD/sDr3ZmQONEeWCTv9Twwvl0cu3h5BRjxH5 14 | ypq/F1B1matjTFPxXY+PpVSj1dbYRGydyAfzaRwcox/ox/P+0h76zQeb3BMSnpmK 15 | g7dpNCARJVW9NbtDam9Y6UBgNZU3giBBaE8pKlrwwnhRTcrlfLmQIk0CgYEA10z+ 16 | U/cXWh9SlGqz+XcRq4+Tp3oZ+gBVdartwJjjSLl2DXMOnxchhSw0pkCkAURvGEps 17 | +mvopXn7fmpENOyXFN/TrnH09qY1exDl/xkzPvR5akCy0HkkAERtpYMfPfYzYYrI 18 | G+W5olDMM5SmSU7rmKeQsTlVtNyavbx9+TB4XscCgYEAl20D6BONgzRLZTVzpXlq 19 | zlDxxdbNl9otn93Rb016N7OtH6wjA1XXHlOilx5tWlgrb+exLbJ9vIBvLV/4vNg5 20 | 1ID8N8YnNezW7mhn9tT+N8PBNlwKsXcKgI/nzXsbnX++HuHoJp5XNwpU3kxeeBRZ 21 | MbMF54ETuFXpaL4svs99C6UCgYAuw7d+T25QEfui5yZeakF5TT9aIkhgKBBn9Y+c 22 | xNihZD9DHpmvbpvGTFrHPcUhzVaAJTJUlnm676rzw2s7P6R1UUSuYGw/4sw9Beef 23 | KD8cTofMz27Hn3h1YmeaisePctmoNzfN73EJ05j3HzObOrwrtUHVbMmz9jLaQYXv 24 | SVrr4wKBgQDfpZKufKQEX4q/L5OUs03hxRVEkSwtWwvIjXWFqbqvhkSEqB04+bns 25 | 3PhhsqVGa4nGmgua6f4/GPy2OAiniIbVupoyz/i3co8usixihH6U0/JhRJO6HHU2 26 | te+2zL1GIHHBAA4fmBIaHXtBC7kta5Ck8RUJ2eSMQ4TFl091wvJxfg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /t/cert/rootCA.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDkjCCAnoCCQCRNvMmzZMQezANBgkqhkiG9w0BAQsFADCBiTELMAkGA1UEBhMC 3 | VUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9uMREwDwYDVQQKDAhT 4 | cXVpeiBVSzEQMA4GA1UECwwHSG9zdGluZzESMBAGA1UEAwwJRWRnZSBUZXN0MR8w 5 | HQYJKoZIhvcNAQkBFhBlZGdlQHNxdWl6LmNvLnVrMCAXDTE5MTExMjIyMzcxNloY 6 | DzIxMTkxMDE5MjIzNzE2WjCBiTELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRv 7 | bjEPMA0GA1UEBwwGTG9uZG9uMREwDwYDVQQKDAhTcXVpeiBVSzEQMA4GA1UECwwH 8 | SG9zdGluZzESMBAGA1UEAwwJRWRnZSBUZXN0MR8wHQYJKoZIhvcNAQkBFhBlZGdl 9 | QHNxdWl6LmNvLnVrMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy6v3 10 | eQFGXrSe97M/9ullfby4bg2jF+9h9p5P/wiv+Y9dTdk8DRR7M40laCqnmoXSSndA 11 | vHLb+lqeHs0GDkp+ofgBg8iP8p8IX0RQ33lqcrU65FJuImMksswkYZLak78YpR1w 12 | qF4EMpHH6t7SKkPlFgULkg+vTpml83fFdnq2WVk8UOSxAsrllvVtxys8c8ny0hmB 13 | XqOWtISPQFYBNAy7N7uHLF8D/4/YMBHM2Vgp5oaMkvFXnsDyL6JWvnqTnz1kKjL9 14 | lbYHbxP7oziI74MIRvRzsw8Ou+OfWiYKTRZDlxfl+7JrzOhxjbTpP/cOCw3ngBC6 15 | rrpdflDVuyf6r7rv2wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAuBBRjAQ/ZEwNa 16 | UXkCZPc+3QxzWrKQCXcf8WoQYb73KaQKRd5gl8QQNWRkmiFdBngDwitd1xGVn0S3 17 | d0jHQAvreWUztiv3fu/Uf/fGv0BWJw9ve9+Wuw4ENINR6rQbRpecXW9Ia/4Jep0w 18 | pYFvNLBFUqPzukrdkf8UdCLyyl4H/gWENtgjgvURAxKCDJGkd3XiiBirT2837mNT 19 | oRweVDY8gxECd+Os2OIDL4B6mon2m3oSEiJpL72bxsX0rwwc7dKdsuOrjuKyg+Jb 20 | okTqY3oO5UzwEDVuKwuOOdvpO11LhtQ7SfjZiMQW2NAHeBtJqNgxcYIbJPeU1Zli 21 | aSxKnsds 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /t/cert/rootCA.srl: -------------------------------------------------------------------------------- 1 | BDA4F00910A74025 2 | -------------------------------------------------------------------------------- /util/lua-releng: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Getopt::Std; 7 | 8 | my (@luas, @tests); 9 | 10 | my %opts; 11 | getopts('Lse', \%opts) or die "Usage: lua-releng [-L] [-s] [-e] [files]\n"; 12 | 13 | my $silent = $opts{s}; 14 | my $stop_on_error = $opts{e}; 15 | my $no_long_line_check = $opts{L}; 16 | 17 | my $check_lua_ver = "luac -v | awk '{print\$2}'| grep 5.1"; 18 | my $output = `$check_lua_ver`; 19 | if ($output eq '') { 20 | die "ERROR: lua-releng ONLY supports Lua 5.1!\n"; 21 | } 22 | 23 | if ($#ARGV != -1) { 24 | @luas = @ARGV; 25 | 26 | } else { 27 | @luas = map glob, qw{ *.lua lib/*.lua lib/*/*.lua lib/*/*/*.lua lib/*/*/*/*.lua lib/*/*/*/*/*.lua }; 28 | if (-d 't') { 29 | @tests = map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }; 30 | } 31 | } 32 | 33 | for my $f (sort @luas) { 34 | process_file($f); 35 | } 36 | 37 | for my $t (@tests) { 38 | blank(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $t}); 39 | } 40 | # p: prints a string to STDOUT appending \n 41 | # w: prints a string to STDERR appending \n 42 | # Both respect the $silent value 43 | sub p { print "$_[0]\n" if (!$silent) } 44 | sub w { warn "$_[0]\n" if (!$silent) } 45 | 46 | # blank: runs a command and looks at the output. If the output is not 47 | # blank it is printed (and the program dies if stop_on_error is 1) 48 | sub blank { 49 | my ($command) = @_; 50 | if ($stop_on_error) { 51 | my $output = `$command`; 52 | if ($output ne '') { 53 | die $output; 54 | } 55 | } else { 56 | system($command); 57 | } 58 | } 59 | 60 | my $version; 61 | sub process_file { 62 | my $file = shift; 63 | # Check the sanity of each .lua file 64 | open my $in, $file or 65 | die "ERROR: Can't open $file for reading: $!\n"; 66 | my $found_ver; 67 | while (<$in>) { 68 | my ($ver, $skipping); 69 | if (/(?x) (?:_VERSION|version) \s* = .*? ([\d\.]*\d+) (.*? SKIP)?/) { 70 | my $orig_ver = $ver = $1; 71 | $found_ver = 1; 72 | $skipping = $2; 73 | $ver =~ s{^(\d+)\.(\d{3})(\d{3})$}{join '.', int($1), int($2), int($3)}e; 74 | w("$file: $orig_ver ($ver)"); 75 | last; 76 | 77 | } elsif (/(?x) (?:_VERSION|version) \s* = \s* ([a-zA-Z_]\S*)/) { 78 | w("$file: $1"); 79 | $found_ver = 1; 80 | last; 81 | } 82 | 83 | if ($ver and $version and !$skipping) { 84 | if ($version ne $ver) { 85 | die "$file: $ver != $version\n"; 86 | } 87 | } elsif ($ver and !$version) { 88 | $version = $ver; 89 | } 90 | } 91 | if (!$found_ver) { 92 | w("WARNING: No \"_VERSION\" or \"version\" field found in `$file`."); 93 | } 94 | close $in; 95 | 96 | p("Checking use of Lua global variables in file $file..."); 97 | p("\top no.\tline\tinstruction\targs\t; code"); 98 | blank("luac -p -l $file | grep -E '[GS]ETGLOBAL' | grep -vE '\\<(require|type|tostring|error|ngx|ndk|jit|setmetatable|getmetatable|string|table|io|os|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|rawget|rawset|rawlen|select|loadstring)\\>'"); 99 | unless ($no_long_line_check) { 100 | p("Checking line length exceeding 80..."); 101 | blank("grep -H -n -E --color '.{81}' $file"); 102 | } 103 | } 104 | --------------------------------------------------------------------------------