Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
haltdos-pro-waf / usr / local / share / lua / 5.1 / resty / icap.lua
Size: Mime:
local _M = {_VERSION = '1.0' }
local ngx = ngx
local ngx_socket_tcp = ngx.socket.tcp
local ngx_log = ngx.log
local ngx_DEBUG = ngx.DEBUG
local ngx_ERR = ngx.ERR
local str_lower = string.lower
local str_upper = string.upper
local str_find = string.find
local str_sub = string.sub
local str_gsub = string.gsub
local str_len = string.len
local str_format = string.format
local tbl_concat = table.concat
local tbl_insert = table.insert
local ngx_encode_args = ngx.encode_args
local ngx_re_match = ngx.re.match
local ngx_re_gmatch = ngx.re.gmatch
local ngx_re_sub = ngx.re.sub
local ngx_re_gsub = ngx.re.gsub
local ngx_re_find = ngx.re.find

local ICAP = {
           ['1.0'] = " ICAP/1.0",
}
local HTTP_REQ_HEADER ={
           'Authorization',
           'Allow',
           'From',
           'Host',
           'Referer',
           'User-Agent',
           'Preview'
}

local HTTP_RES_HEADER ={
           'Server',
           'ISTag'
}

local COMMON_HEADER_REQ_RES = {
           'Cache-Control',
           'Connection',
           'Date',   
           'Expires',    
           'Pragma',   
           'Trailer',    
           'Upgrade',    
           'Encapsulated'
}

local NEWLINE = "\r\n"
local SPACE = " "
local EXTENSION_HEADER_PREFIX = 'X-';
local REQMOD = 'REQMOD'
local RESPMOD = 'RESPMOD'
local OPTIONS = 'OPTIONS'
local REQUEST = { REQMOD, RESPMOD, OPTIONS }
local _USER_AGENT = "Lua-ICAP-Client/1.1"
    -- http method
local EXPECTING_BODY = {
        POST  = true,
        PUT   = true,
        PATCH = true,
}

local DEFAULT_PREVIEW = 1024
local DEFAULT_PORT = 1344;
local DEFAULT_TIMEOUT = 2000;
local mt = { __index = _M }

function _M.new(_)
    local sock, err = ngx_socket_tcp()
    if not sock then
        return nil, err
    end

    return setmetatable({ sock = sock, keepalive = true }, mt)
end

function _M.set_timeout(self, timeout)
    local sock = self.sock
    if not sock then
    return nil, "not initialized"
    end

    return sock:settimeout(timeout)
end

function _M.set_timeouts(self, connect_timeout, send_timeout, read_timeout)
   local sock = self.sock
   if not sock then
    return nil, "not initialized"
    end

    return sock:settimeouts(connect_timeout, send_timeout, read_timeout)
end

function _M.set_keepalive(self, ...)
   local sock = self.sock
   if not sock then
    return nil, "not initialized"
    end

    if self.keepalive == true then
        return sock:setkeepalive(...)
    else
        -- The server said we must close the connection, so we cannot setkeepalive.
        -- If close() succeeds we return 2 instead of 1, to differentiate between
        -- a normal setkeepalive() failure and an intentional close().
        local res, err = sock:close()
        if res then
            return 2, "connection must be closed"
        else
            return res, err
        end

    end

end

local function trim(s)
    s =  str_gsub(s, "%s+$", "")
    s=  str_gsub(s,"^%s+", "")
    return s
end

-- Integer value to hexadecimal
local function number_to_hex(input)
    return str_format("%x", input)
end


local function file_exist(path)
    return os.rename(path, path) 
end

local function open_file(path,mode)
    if not file_exist(path) then
        return nil, "FILE NOT FOUND ".. path
    end

    local file = io.open(path,mode)
    if not file then
        return nil, "Failed to open file"
    end

    return file
end

local function close_file(file)
    if not file then
        return nil, "File is empty "..type(file)
    end

    return pcall(io.close,file)
end


local function get_file_size(path)
    local file, err  = file_exist(path)
    if not  file then
        return nil, err
    end

    file, err = open_file(path,"rb")
    if not file then
        return nil, err
    end

    local file_size = file:seek("end")
    file, err = close_file(file)
    if not file then
        print("File closing error ".. tostring(err))
    end

    return file_size
end

local function close(self)
   local sock = self.sock
   if not sock then
    return nil, "not initialized"
    end

    return sock:close()
end

local function get_bytes_from_file(path,offset,limit)
    local file, err
    if type(path) == "string" then
        file, err  = open_file(path,"rb")
        if not file then
            print("Failed to open file")
            return nil, err
        end
    else 
        file = path
    end

    if offset then
        file:seek("set",offset)
    end

    local read = file:read(limit)
    if type(path) == "string" then -- If we opened file, then close
        close_file(path)
    end

    return read
end

local function _formatRequest(options)
    local req  = {
             options.method.. SPACE.. 'icap://'..options.host.."/"..options.service.. SPACE.. ICAP['1.0']
    }
    -- Append headers
    if options.headers["Allow"] == nil then 
        options.headers["Allow"] = "204"
    end

    if options.headers["X-Client-IP"] == nil  then
        options.headers["X-Client-IP"] = options.client_ip
    end

    if options.headers["X-Client-Port"] == nil  then
        options.headers["X-Client-Port"] = options.client_port
    end

    -- Start Adding Header
    for key, values in pairs(options.headers) do
        tbl_insert(req, key .. ": " .. tostring(values))
    end

    options.http_body = ""
    if options.method ~= OPTIONS and EXPECTING_BODY[str_upper(ngx.var.request_method)] then
        options.http_body = options.body or ""
        options.preview = 0 -- Important field to send request in chunk or single bulk, must be -1
        if type(options.body) == "string" then
            if options.preview >=0 and str_len(options.http_body) < options.preview then
                options.preview = str_len(options.http_body)
            end

        elseif type(options.body) == "table" and options.body["TYPE"] == "FILE" then
            local path = options.body["VALUE"]
            if not file_exist(path) then
                return nil, "FILE NOT FOUND : ".. path
            end

            local size, err = get_file_size(path)
            if not size then
                return nil, "Unable to get file size ".. err
            end

            options.body["SIZE"] = size
            if size > 0 then
                if options.preview == 0 then
                    options.preview = DEFAULT_PREVIEW
                end

                if options.preview > options.body["SIZE"] then
                    options.preview = options.body["SIZE"]
                end
            
            end

            options.http_body = get_bytes_from_file(path,nil,options.preview) or ""
            options.preview = str_len(options.http_body)
        end

        if options.preview >=0 then        
          tbl_insert(req,"Preview: "..options.preview)
      end

    end

    local encapsulate = {}
    tbl_insert(encapsulate,"req-hdr=0")
    if options.method ~= OPTIONS then --res-hdr is not implemented, still pending
        if EXPECTING_BODY[str_upper(ngx.var.request_method)] and str_len(options.http_body) > 0 then
            tbl_insert(encapsulate,"req-body="..str_len(options.http_header))
        else
            tbl_insert(encapsulate,"null-body="..str_len(options.http_header))
        end

    else
        tbl_insert(encapsulate,"null-body=0")
    end


  tbl_insert(req,"Encapsulated: "..tbl_concat(encapsulate,", "))
  tbl_insert(req,NEWLINE)
  return tbl_concat(req,NEWLINE)
end

local function parse_http_response_line(str)
    if not str then
        return nil, "No data"
    end

    local m, err =  ngx_re_match(str,"([^\\s]+)\\s(\\d{3})\\s(.+)","jio")
    if not m then
        return nil, nil, nil, err
    end

    return m[1], tonumber(m[2]), m[3]
end

local function parse_response_line(response_line)

   local line, err = ngx_re_match(response_line,"^(?:ICAP\\/1\\.0)\\s(\\d{3})\\s(.+)","jio")
   if not line then
      return nil, "Unable to read header "..response_line.. " "..err
  end

  return line[1], line[2]
end

function _M.read_response(self)
    if not self.sock then
      return nil, nil, "Socket is not initalized"
    end

    local sock = self.sock
    local requestline, err, partial = sock:receive()
    if not requestline then
      return  nil, nil, "No Response "..err.. " "..partial
    end

    local status, status_msg = parse_response_line(requestline)
    return tonumber(status), status_msg
end

local function _parse_encapsulated(str)
    if type(str) ~= "string" then
        return nil
    end
    local enc_tbl
    local itr = ngx_re_gmatch(str,"([^\\s]+)=(\\d+)","ijo")
    if itr then
        enc_tbl = {}
        while true do
            local enc_head = itr()
            if enc_head then
                enc_tbl[enc_head[1]] = tonumber(enc_head[2])
            else
                break
            end

        end

        return enc_tbl 

    end

end

local function _parse_http_headers(str)
    if not str then
        return nil, "No HTTP header"
    end

    local itr, err = ngx_re_gmatch(str,"(([^\\s]+):\\s(.*))","jio")
    if not itr then
        return nil, err
    end

    local headers = {}
    while true do
        local header = itr()
        if header then
            headers[str_lower(header[1])] = tostring(header[2])
        else
            if err then
                print(err)
            end

            break
        end

    end

    return headers
end

local function _parse_header(sock)
    local headers = {}
    local line, err = sock:receiveuntil("\r\n\r\n") -- Extract Header end
    if not line then
        return nil, err
    end

    local data, err, partial = line()
    local data_len = str_len(data)
    local keys, err = ngx_re_gmatch(data,"([^\\s:]+):\\s","jo") -- Regex for header key
    if not keys then
        return nil, "Failed to read header"
    end

    local offset = 1
    local key1 = keys()
    local key2 
    while true do
        if key1 == nil then
            break
        end

        key2 = keys()
        local key1_from, key1_to = str_find(data,key1[0],offset,true)
        if  key1_to == nil then
            print("Key not found ")
            break
        end

        local key2_from, key2_to
        if key2 ~= nil then 
            key2_from, key2_to = str_find(data,key2[0],key1_from,true)
            if key2_from then
                key2_from = key2_from - 3 -- move back from => current | \r | \n
            end

        else 
            key2_from = str_len(data)
        end

        local value = str_sub(data,key1_to+1,key2_from)
        headers[str_lower(key1[1])] = trim(value)
        key1 = key2
        offset = key1_to
    end

    if headers["encapsulated"]  ~= nil then
        headers["encapsulated"] = _parse_encapsulated(headers["encapsulated"])
    end

    return headers
end

local function  _parse_body(sock,headers)  
    if not headers["encapsulated"] then
        return nil
    end

    local encapsulated = headers["encapsulated"]
    if type(encapsulated) == "string" then
        encapsulated = _parse_encapsulated(encapsulated)
    end

    local header_size, body_size = 0, 0
    local has_body= false
    if encapsulated["null-body"] then
        header_size = encapsulated["null-body"]
    elseif encapsulated["res-body"] then
        header_size = encapsulated["res-body"]
        has_body = true
    end

    header_size = tonumber(header_size)
    if header_size == 0 then
        return nil, nil
    end

    local http_header, err, partial = sock:receive(header_size)
    http_header = trim(http_header)
    local http_status_version , http_status , http_status_msg, err = parse_http_response_line(http_header)
    if err then
        return nil, err
    end

    local from, to = str_find(http_header, http_status_msg,1, true)
    local header = trim(str_sub(http_header,to))
    local http_headers, err = _parse_http_headers(header)
    if not http_headers then
        return nil, err
    end

    local body -- Can be problem with chunk response body, but not any case found
    if has_body then
        body = {}
        local size = sock:receive()
        local read_size = tonumber(size,16) -- hexa decimal input - File size
        local data , err,partial = sock:receive(read_size)
        if data then
            tbl_insert(body,data)
        else
            if err then
                print(err)
            end

        end

        body = tbl_concat(body)
    end

    return {
        status = http_status,
        status_msg = http_status_mg,
        version = http_status_version,
        headers = http_headers,
        has_body = has_body,
        body= body
    }
end 

function _M.send_request(self,options)
    if not self.sock then
        return nil, "not initialized"
    end

    if not options.host or not options.port then
        return nil, "No Host/Port Found"
    end

    local sock = self.sock
    local icap_req_hdr, req_err = _formatRequest(options)
    -- print(icap_req_hdr)
    if not icap_req_hdr then
        return nil, "Failed to create request ".. req_err
    end

    self:set_timeout(options.timeout or DEFAULT_TIMEOUT)
    local ok, err = sock:connect(options.host,options.port)
    if not ok then
        return nil, "Unable to Connect ".. err
    end

    local bytes, err = sock:send(icap_req_hdr)
    if not bytes then
        return nil, "Unable to send request ".. err
    end

    local status, status_msg, response
    if options.method ~= OPTIONS then
        local body_req 
        local end_line
        if options.preview > 0 then
            if options.preview == options.body["SIZE"] then 
                end_line = "0; ieof"..NEWLINE..NEWLINE
            else 
                end_line = "0"..NEWLINE..NEWLINE
            end

        elseif options.preview == -1 then
            end_line ="0; ieof"..NEWLINE..NEWLINE
        else 
            end_line = "0"..NEWLINE..NEWLINE
        end

        body_req = {
                    options.http_header..number_to_hex(str_len(options.http_body)),
                    options.http_body,
                    end_line
                   }

        local http_hdr_body = tbl_concat(body_req,NEWLINE)
        -- print(http_hdr_body)
        local bytes, err = sock:send(http_hdr_body)
        if not bytes then
            return nil, "Invalid response ".. err
        end

        status, status_msg, err = self:read_response()
        -- print(status,status_msg,err)
    end

    if status == 100 then
        sock:receive() -- read remaining \r\n => 100 Continue\r\n[\r\n] 
        local req = {}
        if options.preview > 0 then
            if type(options.body) == "table" and options.body["TYPE"] == "FILE" then
                local offset = options.preview
                local size = options.body["SIZE"]
                local remaining  = size - offset
                local preview  = options.preview
                local file = open_file(options.body["VALUE"],"rb") -- Note: Must close file 
                if not file then
                    return nil, "Failed to read file "
                end

                file:seek("set",preview)
                while remaining > 0 do
                    if remaining < preview then
                        preview = remaining
                    end

                    local packet, err = get_bytes_from_file(file,nil,preview)
                    if not packet then
                        close_file(file)
                        return nil, err
                    end

                    -- print("Packet found ".. str_len(packet))
                    local packet_req = {
                        number_to_hex(str_len(packet)),
                        packet..NEWLINE
                    }
                    local packet_str = tbl_concat(packet_req,NEWLINE)
                    local bytes, err = sock:send(packet_str)
                    if not bytes then
                        close_file(file)
                        return nil, "Error in packet "..str_len(packet).. " offset ".. tostring(offset).. tostring(err)
                    end

                    remaining  = remaining - preview
                    offset = offset + preview
                end

                close_file(file)
                -- print("Ending Packet.....")
                local end_line =  "0"..NEWLINE..NEWLINE
                local bytes, err = sock:send(end_line)
                if not bytes then
                    return nil, "Error in packet"
                end

                status, status_msg, err = self:read_response()
                -- print("Final Packet response ".. tostring(status))
            end 

        else
            print("Unexcepted case") -- invalid chunk case
        end

    end

    if err then
        return nil, "No response to parse "..err
    end

    local headers = _parse_header(sock)
    local body, err =  _parse_body(sock,headers)

    local  has_body = false 
    if body then
        has_body = true
    elseif err then
        print(err)
    end

    return {
     status = status,
     status_msg = status_msg,
     headers = headers,
     has_body = has_body,
     encapsulate  = encap_tbl,
     body = body
 }
end

local function valid_request_method(method)
   if not method then
      return nil, "Request Method undefine"
    end

    method = str_upper(method)

    for _, v in pairs(REQUEST) do
        if v == method then
            return method
        end
    
    end

    return nil, "Invalid request method" 

end

function valid_request_header(headers)
    local req_header = {}
    if headers == nil then
        headers = {}
    end

    if type(headers) ~= "table" then
      return nil, "Invalid header"
    end

    for k, v in pairs(headers) do
        key = tostring(key)
        if type(values) == "table" then
            for _, value in pairs(values) do
                req[key] = tostring(value)
            end

        else
            req[key] =  tostring(values)
        end

    end

    return req_header
end


local function get_raw_header()
    local raw_header = {
        ngx.var.request_method,
        SPACE,
        ngx.unescape_uri(ngx.var.request_uri),
        SPACE,
        ngx.var.server_protocol,
        NEWLINE
    }

    for key, value in pairs(ngx.req.get_headers()) do
        tbl_insert(raw_header,key.. ": "..value.. NEWLINE)
    end

    tbl_insert(raw_header,NEWLINE)
    return tbl_concat(raw_header,"")
end

function _M.request(self,params)
    if not params then
      return nil, "No Request parameter define"
    end

    if not params.host then
      return nil, "ICAP Server Host not define"
    end

    if not params.port then
        params.port = DEFAULT_PORT
    elseif type(params.port) == "string" then
        params.port = tonumber(params.port)
    end

    if not params.service then
        return nil, "No Service Define"
    end

    local req_method, method_err = valid_request_method(params.method)

    if not req_method then
        return nil, "Invalid Request Method :  ".. method_err
    end

    params.method = req_method
    local service = params.service
    if not service then
        return nil, "No Service define"
    end

    if not params.headers then
        params.headers = {}
        local host = params.host
        if params.port ~= 1344 then
            host = host .. ':'.. params.port
        end

        params.headers["Host"] = host
        params.headers["User-Agent"] = _USER_AGENT
    end

    if not params.preview then
        params.preview  = -1
    end 

    params.client_ip = ngx.var.remote_addr
    params.client_port = ngx.var.remote_port

    if req_method ~= OPTIONS then 
        params.http_header = get_raw_header()
        if req_method == REQMOD then
            if EXPECTING_BODY[str_upper(ngx.var.request_method)] ~= nil then
                if ngx.var.content_length ~= nil and 
                tostring(ngx.var.content_length) ~= "" and
                tonumber(ngx.var.content_length) > 0  then
                    if params.body then
                        if type(params.body) == "table" then
                            if params.body["TYPE"] ~= "FILE" and params.body["TYPE"] ~= "STRING" then
                                return nil, "Unsupported body type ".. params.body["TYPE"]
                            end

                            if params.body["TYPE"] == "STRING" then
                                params.body = tostring(params.body["VALUE"])
                            end

                        else
                            params.body = tostring(params.body)
                        end

                    else
                        ngx.req.read_body()
                        params.body = ngx.req.get_body_data() 
                    end

                end

            end

        end
    end

    local data, err = self:send_request(params)
    self.sock:close()
    if not data then
    ngx_log(ngx_ERR," Not response found", err)
    return nil, err
    end

    return data;
end


return _M