Skip to content

Instantly share code, notes, and snippets.

@ysugimoto
Created April 12, 2019 02:59
Show Gist options
  • Save ysugimoto/b2e130b469794916feb14f8df1d2f888 to your computer and use it in GitHub Desktop.
Save ysugimoto/b2e130b469794916feb14f8df1d2f888 to your computer and use it in GitHub Desktop.
----------------------------------------------------------------
-- Lua common HTTP request library
--
-- This library is utility for common HTTP/HTTPS request usage,
-- and provide easy syntax like python requests module.
--
-- Dependencies
-- - [luasocket](https://github.com/diegonehab/luasocket)
-- - [luasec](https://github.com/brunoos/luasec)
-- - [net-url](https://github.com/golgote/neturl)
--
-- So you need to install above pakcages via luarocks:
-- luaroks install luasocket
-- luaroks install luasec
-- luaroks install net-url
--
-- This library provides request methods as function:
-- - request.get(url, headers)
-- - request.post(url, headers, body)
-- - request.put(url, headers, body)
-- - request.delete(url, headers)
--
-- All methods return response table and err in second argument.
--
-- Example
-- ```
-- local request = require("request")
-- local resp, err = request.get("https://www.google.com", {
-- ["X-Custom-Headers"]="foobar"
-- })
-- if err then
-- print("request error " .. err)
-- return
-- end
-- print(resp.status_code) -- Get status code
-- print(resp.body) - Get response body
-- for key, val in pairs(resp.headers) do -- Get response headers as lower-snake-cased
-- print(("%s: %s"):format(key, val))
-- end
-- ```
--
-- @lisence MIT
-- @author Yoshiaki Sugimoto <sugimoto@wnotes.net>
----------------------------------------------------------------
local socket = require('socket')
local ssl = require('ssl')
local url = require('net.url')
-- Create TCP or TLS connection to destination host
--
-- @param {table} u - URL table parsed by net-url
-- @return conn - connection object
local function create_connection(u)
local conn = socket.tcp()
if u.scheme == "https" then
conn:connect(u.host, u.port or 443)
conn = ssl.wrap(conn, {
mode = "client",
protocol = "tlsv1",
verify = "peer",
options = "all"
})
conn:dohandshake()
else
conn:connect(u.host, u.port or 80)
end
return conn
end
-- Create request lines for connection
--
-- @param {string} method - request method
-- @param {string} host - destination host
-- @param {string} path - request uri (can include query strings)
-- @param {table} headers - additional headers
-- @param {string|nil} body - request body
-- @return {string} request string
local function create_request(method, host, path, headers, body)
if path == "" then
path = "/"
end
local req = {("%s %s HTTP/1.1"):format(method, path)}
-- table.insert(req, "Host: " .. host)
-- Default User-Agent is Lua-Request-Client, you can override in custom header :-)
-- table.insert(req, "User-Agent: Lua-Request-Client")
for k, v in pairs(headers or {}) do
table.insert(req, ("%s: %s"):format(k, v))
end
table.insert(req, "")
if body then
table.insert(req, body)
end
print('-------request-------------')
print(table.concat(req, "\n"))
print('-------/request-------------')
return table.concat(req, "\n")
end
-- Parse first response line as status code
--
-- @param {table} conn - connection object
-- @return {int} status_code - response status code
-- @return {string|nil} err - string if error occured
local function receive_status_code(conn)
local line, err = conn:receive("*l")
if err then
return nil, err
end
local _, _, status_code = line:find("HTTP/[0-9%.]+ ([0-9]+)")
if not status_code then
return nil, "Unexpected response line: " .. line
end
return tonumber(status_code), nil
end
-- Parse respose headers
--
-- A first return value of this function is transformed to Lower-Snake case
-- in order to user should not confuse how header key case is, and also access via lua table access syntax.
--
-- For example,
-- - A "Content-Type" header is transformed to "content_type".
-- - A "X-Requested-With" header is transformed to "x_requested_with"
--
-- Of course you can retrieve raw response header from second return value.
--
-- @param {table} conn - connection object
-- @return {table} status_code - response status code
-- @return {string|nil} err - string if error occured
local function receive_headers(conn)
local key, value
local headers = {}
local raw_headers = {}
while true do
line, err = conn:receive("*l")
if err then
return nil, nil, err
end
if line == "" then
break
end
local p = line:find(":")
if p then
local key = line:sub(1, p-1)
local value = line:sub(p+1)
raw_headers[key] = value
headers[string.gsub(string.lower(key), "%-", "_")] = value
end
end
return headers, raw_headers, nil
end
-- Read response body
--
-- If Content-Length header is supplied (normaly good manner in HTTP), read as that bytes.
-- Otherwise, try to read until EOF with timeout...
--
-- @param {table} conn - connection object
-- @param {int} content_length - Content-Length value
-- @return {string} response body
-- @return {string|nil} err - string if error occured
function receive_body(conn, content_length)
if content_length > 0 then
return conn:receive(content_length)
end
local body = ""
conn:receive("*l")
while true do
conn:settimeout(0.3, "b")
line, err = conn:receive("*l")
if err then
return nil, err
end
if not line or line == "0" then
break
end
body = body .. line .. "\n"
end
return body, nil
end
-- HTTP(S) conversation process
--
-- @param {string} method - request method
-- @param {string} request_url - full request URL
-- @param {table} headers - additional headers
-- @param {string|nil} body - request body
-- @return {table} response data table
-- @return {string|nil} err - string if error occured
local function send_request(method, request_url, headers, body)
local u = url.parse(request_url)
local path = u.path
if path == "" then
path = "/"
end
local conn = create_connection(u)
local req = create_request(method, u.host, u.path, headers, body)
print(req)
conn:send(req)
local resp = {
status_code = 0,
raw_headers = {},
headers = {},
body = ""
}
local err
resp.status_code, err = receive_status_code(conn)
if err then
print("status_code")
conn:close()
return nil, err
end
resp.headers, resp.raw_headers, err = receive_headers(conn)
if err then
print("header")
conn:close()
return nil, err
end
resp.body, err = receive_body(conn, tonumber(resp.headers["content_length"] or 0))
if err then
print("body")
conn:close()
return nil, err
end
conn:close()
return resp, nil
end
-- Expose module
local _M = {}
_M.get = function(request_url, headers)
return send_request("GET", request_url, headers, nil)
end
_M.post = function(request_url, headers, body)
return send_request("POST", request_url, headers, body)
end
_M.put = function(request_url, headers, body)
return send_request("PUT", request_url, headers, body)
end
_M.delete = function(request_url, headers)
return send_request("DELETE", request_url, headers, nil)
end
return _M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment