| -- |
| -- Licensed to the Apache Software Foundation (ASF) under one or more |
| -- contributor license agreements. See the NOTICE file distributed with |
| -- this work for additional information regarding copyright ownership. |
| -- The ASF licenses this file to You under the Apache License, Version 2.0 |
| -- (the "License"); you may not use this file except in compliance with |
| -- the License. You may obtain a copy of the License at |
| -- |
| -- http://www.apache.org/licenses/LICENSE-2.0 |
| -- |
| -- Unless required by applicable law or agreed to in writing, software |
| -- distributed under the License is distributed on an "AS IS" BASIS, |
| -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| -- See the License for the specific language governing permissions and |
| -- limitations under the License. |
| -- |
| |
| local core = require("apisix.core") |
| local ngx_re = require("ngx.re") |
| local tab_concat = table.concat |
| local string = string |
| local io_open = io.open |
| local io_close = io.close |
| local ngx = ngx |
| local os = os |
| local ipairs = ipairs |
| local pairs = pairs |
| local tonumber = tonumber |
| |
| local plugin_name = "proxy-cache" |
| |
| local schema = { |
| type = "object", |
| properties = { |
| cache_zone = { |
| type = "string", |
| minLength = 1, |
| maxLength = 100, |
| default = "disk_cache_one", |
| }, |
| cache_key = { |
| type = "array", |
| minItems = 1, |
| items = { |
| description = "a key for caching", |
| type = "string", |
| pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]], |
| }, |
| default = {"$host", "$request_uri"} |
| }, |
| cache_http_status = { |
| type = "array", |
| minItems = 1, |
| items = { |
| description = "http response status", |
| type = "integer", |
| minimum = 200, |
| maximum = 599, |
| }, |
| uniqueItems = true, |
| default = {200, 301, 404}, |
| }, |
| cache_method = { |
| type = "array", |
| minItems = 1, |
| items = core.schema.method_schema, |
| uniqueItems = true, |
| default = {"GET", "HEAD"}, |
| }, |
| hide_cache_headers = { |
| type = "boolean", |
| default = false, |
| }, |
| cache_bypass = { |
| type = "array", |
| minItems = 1, |
| items = { |
| type = "string", |
| pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]] |
| }, |
| }, |
| no_cache = { |
| type = "array", |
| minItems = 1, |
| items = { |
| type = "string", |
| pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]] |
| }, |
| }, |
| }, |
| } |
| |
| local _M = { |
| version = 0.1, |
| priority = 1009, |
| name = plugin_name, |
| schema = schema, |
| } |
| |
| |
| function _M.check_schema(conf) |
| local ok, err = core.schema.check(schema, conf) |
| if not ok then |
| return false, err |
| end |
| |
| for _, key in ipairs(conf.cache_key) do |
| if key == "$request_method" then |
| return false, "cache_key variable " .. key .. " unsupported" |
| end |
| end |
| |
| local found = false |
| local local_conf = core.config.local_conf() |
| if local_conf.apisix.proxy_cache then |
| for _, cache in ipairs(local_conf.apisix.proxy_cache.zones) do |
| if cache.name == conf.cache_zone then |
| found = true |
| end |
| end |
| |
| if found == false then |
| return false, "cache_zone " .. conf.cache_zone .. " not found" |
| end |
| end |
| return true |
| end |
| |
| |
| local tmp = {} |
| local function generate_complex_value(data, ctx) |
| core.table.clear(tmp) |
| |
| core.log.info("proxy-cache complex value: ", core.json.delay_encode(data)) |
| for i, value in ipairs(data) do |
| core.log.info("proxy-cache complex value index-", i, ": ", value) |
| |
| if string.byte(value, 1, 1) == string.byte('$') then |
| tmp[i] = ctx.var[string.sub(value, 2)] |
| else |
| tmp[i] = value |
| end |
| end |
| |
| return tab_concat(tmp, "") |
| end |
| |
| |
| -- check whether the request method and response status |
| -- match the user defined. |
| local function match_method_and_status(conf, ctx) |
| local match_method, match_status = false, false |
| |
| -- Maybe there is no need for optimization here. |
| for _, method in ipairs(conf.cache_method) do |
| if method == ctx.var.request_method then |
| match_method = true |
| break |
| end |
| end |
| |
| for _, status in ipairs(conf.cache_http_status) do |
| if status == ngx.status then |
| match_status = true |
| break |
| end |
| end |
| |
| if match_method and match_status then |
| return true |
| end |
| |
| return false |
| end |
| |
| |
| local function file_exists(name) |
| local f = io_open(name, "r") |
| if f ~= nil then |
| io_close(f) |
| return true |
| end |
| return false |
| end |
| |
| |
| local function generate_cache_filename(cache_path, cache_levels, cache_key) |
| local md5sum = ngx.md5(cache_key) |
| local levels = ngx_re.split(cache_levels, ":") |
| local filename = "" |
| |
| local index = #md5sum |
| for k, v in pairs(levels) do |
| local length = tonumber(v) |
| index = index - length |
| filename = filename .. md5sum:sub(index+1, index+length) .. "/" |
| end |
| if cache_path:sub(-1) ~= "/" then |
| cache_path = cache_path .. "/" |
| end |
| filename = cache_path .. filename .. md5sum |
| return filename |
| end |
| |
| |
| local function cache_purge(conf, ctx) |
| local cache_zone_info = ngx_re.split(ctx.var.upstream_cache_zone_info, ",") |
| |
| local filename = generate_cache_filename(cache_zone_info[1], cache_zone_info[2], |
| ctx.var.upstream_cache_key) |
| if file_exists(filename) then |
| os.remove(filename) |
| return nil |
| end |
| |
| return "Not found" |
| end |
| |
| |
| function _M.rewrite(conf, ctx) |
| core.log.info("proxy-cache plugin rewrite phase, conf: ", core.json.delay_encode(conf)) |
| |
| ctx.var.upstream_cache_zone = conf.cache_zone |
| |
| local value = generate_complex_value(conf.cache_key, ctx) |
| ctx.var.upstream_cache_key = value |
| core.log.info("proxy-cache cache key value:", value) |
| |
| if ctx.var.request_method == "PURGE" then |
| local err = cache_purge(conf, ctx) |
| if err ~= nil then |
| return 404 |
| end |
| |
| return 200 |
| end |
| |
| if conf.cache_bypass ~= nil then |
| local value = generate_complex_value(conf.cache_bypass, ctx) |
| ctx.var.upstream_cache_bypass = value |
| core.log.info("proxy-cache cache bypass value:", value) |
| end |
| end |
| |
| |
| function _M.header_filter(conf, ctx) |
| core.log.info("proxy-cache plugin header filter phase, conf: ", core.json.delay_encode(conf)) |
| |
| local no_cache = "1" |
| |
| if match_method_and_status(conf, ctx) then |
| no_cache = "0" |
| end |
| |
| if conf.no_cache ~= nil then |
| local value = generate_complex_value(conf.no_cache, ctx) |
| core.log.info("proxy-cache no-cache value:", value) |
| |
| if value ~= nil and value ~= "" and value ~= "0" then |
| no_cache = "1" |
| end |
| end |
| |
| local upstream_hdr_cache_control |
| local upstream_hdr_expires |
| |
| if conf.hide_cache_headers == true then |
| upstream_hdr_cache_control = "" |
| upstream_hdr_expires = "" |
| else |
| upstream_hdr_cache_control = ctx.var.upstream_http_cache_control |
| upstream_hdr_expires = ctx.var.upstream_http_expires |
| end |
| |
| core.response.set_header("Cache-Control", upstream_hdr_cache_control, |
| "Expires", upstream_hdr_expires, |
| "Apisix-Cache-Status", ctx.var.upstream_cache_status) |
| |
| ctx.var.upstream_no_cache = no_cache |
| core.log.info("proxy-cache no cache:", no_cache) |
| end |
| |
| |
| return _M |