| -- |
| -- 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 bit = require 'bit' |
| local socket = require 'socket' |
| local sha1 = require 'sha1' |
| local md5 = require 'md5' |
| local mime = require 'mime' |
| local CRC32 = { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D } |
| |
| -- Helpers |
| local encode_param = function(str) |
| return tostring(str):gsub("\n", "\r\n"):gsub("([^%w_])", function (c) |
| return string.format("%%%02X", string.byte(c)) |
| end) |
| end |
| |
| local encode_params = function(tab) |
| local list = {} |
| for k, v in pairs(tab) do |
| table.insert(list, encode_param(k) .. "=" .. encode_param(v)) |
| end |
| return table.concat(list, "&") |
| end |
| |
| local function reverse_merge(src, defs) |
| local opts = {} |
| for k,v in pairs(src) do opts[k] = v end |
| for k,v in pairs(defs) do |
| if src[k] then v = src[k] end |
| opts[k] = v |
| end |
| return opts |
| end |
| |
| local function stub_options(opts, method) |
| opts = opts or {} |
| if method and opts["method"] == nil then opts["method"] = method end |
| if type(opts.args) == "table" then opts["args"] = encode_params(opts.args) end |
| return opts |
| end |
| |
| local function stub_response(res) |
| return reverse_merge(res or {}, { status = 200, headers = {}, body = "" }) |
| end |
| |
| local control_chars = { |
| ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", |
| ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\\"] = "\\\\" |
| } |
| |
| local function replace_control_char(c) |
| return control_chars[c] |
| end |
| |
| local function stub_format(uri, opts) |
| local pad = 0 |
| for k,_ in pairs(opts) do |
| if k ~= "method" and #k > pad then pad = #k end |
| end |
| |
| local msg = " " .. (opts.method or "GET") .. " " .. uri .. "\n" |
| for k,v in pairs(opts) do |
| if k ~= "method" then |
| k = k .. ":" .. string.rep(" ", pad + 1 - #k) |
| msg = msg .. " " .. k .. v:gsub("(%c)", replace_control_char) .. "\n" |
| end |
| end |
| return msg |
| end |
| |
| -- Capture Registry |
| local Captures = {} |
| |
| function Captures:new() |
| local this = { stubs = {} } |
| setmetatable(this, { __index = self }) |
| return this |
| end |
| |
| function Captures:length() |
| return #self.stubs |
| end |
| |
| function Captures:each(fun) |
| for i=self:length(),1,-1 do |
| local stub = self.stubs[i] |
| fun(stub) |
| end |
| end |
| |
| function Captures:find(uri, opts) |
| opts = stub_options(opts, "GET") |
| |
| for i=self:length(),1,-1 do |
| local stub = self.stubs[i] |
| if uri == stub.uri then |
| local is_match = true |
| |
| for k,v in pairs(stub.opts) do |
| if type(v) == 'function' then |
| is_match = v(opts[k]) |
| elseif type(v) == 'string' and v:sub(1, 2) == "~>" then |
| is_match = tostring(opts[k]):match(v:sub(3)) and true |
| elseif opts[k] ~= v then |
| is_match = false |
| end |
| if not is_match then break end |
| end |
| if is_match then return stub end |
| end |
| end |
| |
| return nil |
| end |
| |
| function Captures:stub(uri, opts, res) |
| local stub = { uri = uri, opts = stub_options(opts), res = stub_response(res), calls = {} } |
| table.insert(self.stubs, stub) |
| return stub |
| end |
| |
| -- TCP Proxy |
| local TCP = {} |
| |
| function TCP:new() |
| return setmetatable({ host = nil, port = 0, timeout = 0, keepalive = {-1, 0}, data = {} }, { __index = self }) |
| end |
| |
| function TCP:connect(host, port) |
| self.host = host |
| self.port = port |
| return true, nil |
| end |
| |
| function TCP:settimeout(value) |
| self.timeout = value |
| end |
| |
| function TCP:setkeepalive(...) |
| self.keepalive = {...} |
| end |
| |
| function TCP:send(msg) |
| table.insert(self.data, msg) |
| end |
| |
| -- UDP Proxy |
| local UDP = {} |
| |
| function UDP:new() |
| return setmetatable({ host = nil, port = 0, timeout = 0, data = {}, closed = false }, { __index = self }) |
| end |
| |
| function UDP:setpeername(host, port) |
| self.host = host |
| self.port = port |
| return true, nil |
| end |
| |
| function UDP:settimeout(value) |
| self.timeout = value |
| end |
| |
| function UDP:send(msg) |
| table.insert(self.data, msg) |
| return true, nil |
| end |
| |
| function UDP:close() |
| self.closed = true |
| return true, nil |
| end |
| |
| -- DICT Proxy |
| local SharedDict = {} |
| |
| function SharedDict:new() |
| return setmetatable({ data = {} }, { __index = self }) |
| end |
| |
| function SharedDict:get(key) |
| return self.data[key], 0 |
| end |
| |
| function SharedDict:set(key, value) |
| self.data[key] = value |
| return true, nil, false |
| end |
| |
| function SharedDict:add(key, value) |
| if self.data[key] ~= nil then |
| return false, "exists", false |
| end |
| |
| self.data[key] = value |
| return true, nil, false |
| end |
| |
| function SharedDict:replace(key, value) |
| if self.data[key] == nil then |
| return false, "not found", false |
| end |
| |
| self.data[key] = value |
| return true, nil, false |
| end |
| |
| function SharedDict:delete(key) |
| self.data[key] = nil |
| end |
| |
| function SharedDict:incr(key, value) |
| if not self.data[key] then |
| return nil, "not found" |
| elseif type(self.data[key]) ~= "number" then |
| return nil, "not a number" |
| end |
| |
| self.data[key] = self.data[key] + value |
| return self.data[key], nil |
| end |
| |
| -- NGX Prototype |
| local protoype = { |
| |
| -- Log constants |
| STDERR = 0, |
| EMERG = 1, |
| ALERT = 2, |
| CRIT = 3, |
| ERR = 4, |
| WARN = 5, |
| NOTICE = 6, |
| INFO = 7, |
| DEBUG = 8, |
| |
| -- HTTP Method Constants |
| HTTP_GET = "GET", |
| HTTP_HEAD = "HEAD", |
| HTTP_POST = "POST", |
| HTTP_PUT = "PUT", |
| HTTP_DELETE = "DELETE", |
| |
| -- HTTP Status Constants |
| HTTP_OK = 200, |
| HTTP_CREATED = 201, |
| HTTP_ACCEPTED = 202, |
| HTTP_NO_CONTENT = 204, |
| HTTP_PARTIAL_CONTENT = 206, |
| HTTP_SPECIAL_RESPONSE = 300, |
| HTTP_MOVED_PERMANENTLY = 301, |
| HTTP_MOVED_TEMPORARILY = 302, |
| HTTP_SEE_OTHER = 303, |
| HTTP_NOT_MODIFIED = 304, |
| HTTP_BAD_REQUEST = 400, |
| HTTP_UNAUTHORIZED = 401, |
| HTTP_FORBIDDEN = 403, |
| HTTP_NOT_FOUND = 404, |
| HTTP_NOT_ALLOWED = 405, |
| HTTP_REQUEST_TIME_OUT = 408, |
| HTTP_CONFLICT = 409, |
| HTTP_LENGTH_REQUIRED = 411, |
| HTTP_PRECONDITION_FAILED = 412, |
| HTTP_REQUEST_ENTITY_TOO_LARGE = 413, |
| HTTP_REQUEST_URI_TOO_LARGE = 414, |
| HTTP_UNSUPPORTED_MEDIA_TYPE = 415, |
| HTTP_RANGE_NOT_SATISFIABLE = 416, |
| HTTP_CLOSE = 444, |
| HTTP_NGINX_CODES = 494, |
| HTTP_REQUEST_HEADER_TOO_LARGE = 494, |
| HTTP_INTERNAL_SERVER_ERROR = 500, |
| HTTP_NOT_IMPLEMENTED = 501, |
| HTTP_BAD_GATEWAY = 502, |
| HTTP_SERVICE_UNAVAILABLE = 503, |
| HTTP_GATEWAY_TIME_OUT = 504, |
| HTTP_INSUFFICIENT_STORAGE = 507, |
| |
| } |
| |
| -- NGX Builder |
| local fakengx = {} |
| |
| -- Constructor |
| function fakengx.new() |
| local ngx = {} |
| for k, v in pairs(protoype) do |
| ngx[k] = v |
| end |
| setmetatable(ngx, getmetatable(protoype)) |
| |
| -- Create namespaces |
| ngx.req = {} |
| ngx.re = {} |
| ngx.socket = {} |
| ngx.thread = {} |
| ngx.location = {} |
| ngx.shared = {} |
| |
| -- Create shared dict API |
| setmetatable(ngx.shared, { |
| __index = function(t, k) |
| t[k] = SharedDict:new() |
| return t[k] |
| end |
| }) |
| |
| function ngx._reset() |
| ngx.status = 200 |
| ngx.var = {} |
| ngx.ctx = {} |
| ngx.header = {} |
| ngx.arg = {} |
| |
| -- Internal Registries |
| ngx._captures = Captures:new() |
| ngx._sockets = {} |
| ngx._body = "" |
| ngx._log = "" |
| ngx._exit = nil |
| |
| for k,_ in pairs(ngx.shared) do |
| ngx.shared[k] = nil |
| end |
| end |
| |
| -- Reset once |
| ngx._reset() |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.print |
| function ngx.print(s) |
| ngx._body = ngx._body .. s |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.say |
| function ngx.say(s) |
| ngx.print(s .. "\n") |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.log |
| function ngx.log(level, ...) |
| local args = {...} |
| for i=1,#args do args[i] = tostring(args[i]) or "nil" end |
| ngx._log = ngx._log .. "LOG(" .. tostring(level) .. "): " .. table.concat(args) .. "\n" |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.time |
| function ngx.time() |
| if not ngx._time then |
| ngx._time = os.time() |
| end |
| return ngx._time |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.update_time |
| function ngx.update_time() |
| ngx._time = nil |
| ngx._now = nil |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.now |
| function ngx.now() |
| if not ngx._now then |
| ngx._now = socket.gettime() |
| end |
| return ngx._now |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.cookie_time |
| function ngx.cookie_time(t) |
| return os.date('!%a, %d-%b-%Y %H:%M:%S GMT', t) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.exit |
| function ngx.exit(status) |
| if status > ngx.status then ngx.status = status end |
| ngx._exit = status |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.crc32_short |
| function ngx.crc32_short(s) |
| local crc, l, i = 0xFFFFFFFF, string.len(s) |
| for i = 1, l, 1 do |
| crc = bit.bxor(bit.rshift(crc, 8), CRC32[bit.band(bit.bxor(crc, string.byte(s, i)), 0xFF) + 1]) |
| end |
| return bit.bxor(crc, -1) % 2^32 |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.hmac_sha1 |
| function ngx.hmac_sha1(secret_key, str) |
| return sha1.hmac_sha1_binary(secret_key, str) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.sha1_bin |
| function ngx.sha1_bin(str) |
| return sha1.sha1_binary(str) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.md5 |
| function ngx.md5(str) |
| return md5.sumhexa(str) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.md5_bin |
| function ngx.md5_bin(str) |
| return md5.sum(str) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.escape_uri |
| function ngx.escape_uri(str) |
| return tostring(str):gsub("\n", "\r\n"):gsub("([^%w_ ])", function (c) |
| return string.format("%%%02X", string.byte(c)) |
| end):gsub(" ", "+") |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.unescape_uri |
| function ngx.unescape_uri(str) |
| return tostring(str):gsub("+", " "):gsub("\r\n", "\n"):gsub("%%(%x%x)", function(h) |
| return string.char(tonumber(h,16)) |
| end) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.encode_args |
| function ngx.encode_args(tab) |
| return encode_params(tab) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.location.capture |
| function ngx.location.capture(uri, opts) |
| local stub = ngx._captures:find(uri, opts) |
| if not stub then |
| local msg = "\n\nUnstubbed request:\n\n" .. stub_format(uri, opts or {}) .. "\nStubbed were:\n" |
| ngx._captures:each(function(stub) |
| msg = msg .. "\n" .. stub_format(stub.uri, stub.opts or {}) |
| end) |
| error(msg) |
| end |
| |
| table.insert(stub.calls, { uri = uri, opts = opts }) |
| return stub.res |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.location.capture_multi |
| function ngx.location.capture_multi(...) |
| local requests = ... |
| local responses = {} |
| for i, request in ipairs(requests) do |
| table.insert(responses, ngx.location.capture(request[1], request[2])) |
| end |
| return unpack(responses) |
| end |
| |
| -- Stub a capture |
| function ngx.location.stub(...) |
| return ngx._captures:stub(...) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.req.read_body |
| function ngx.req.read_body() |
| end |
| |
| function ngx.req.get_headers() |
| return {} |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.socket.tcp |
| function ngx.socket.tcp() |
| local sock = TCP:new() |
| table.insert(ngx._sockets, sock) |
| return sock |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.socket.udp |
| function ngx.socket.udp() |
| local sock = UDP:new() |
| table.insert(ngx._sockets, sock) |
| return sock |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.encode_base64 |
| function ngx.encode_base64(s) |
| return mime.b64(s) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.decode_base64 |
| function ngx.decode_base64(s) |
| return mime.unb64(s) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.thread.spawn |
| function ngx.thread.spawn(fun, ...) |
| return { fun = fun, args = {...} } |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.thread.wait |
| function ngx.thread.wait(thread) |
| return true, thread.fun(unpack(thread.args)) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.re.gmatch |
| function ngx.re.gmatch(s, pattern) |
| return string.gmatch(s, pattern) |
| end |
| |
| -- http://wiki.nginx.org/HttpLuaModule#ngx.re.match |
| function ngx.re.match(s, pattern) |
| return string.match(s, pattern) |
| end |
| |
| return ngx |
| end |
| |
| return fakengx |