Created
April 12, 2019 02:59
-
-
Save ysugimoto/b2e130b469794916feb14f8df1d2f888 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
---------------------------------------------------------------- | |
-- 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 <[email protected]> | |
---------------------------------------------------------------- | |
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