Repository URL to install this package:
|
Version:
1.0.6 ▾
|
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