mirror of
https://github.com/kenzok8/small-package.git
synced 2026-01-16 04:10:50 +08:00
304 lines
8.6 KiB
Lua
304 lines
8.6 KiB
Lua
|
|
-- Lua Requests library for http ease
|
||
|
|
|
||
|
|
local http_socket = require('socket.http')
|
||
|
|
local https_socket = require('ssl.https')
|
||
|
|
local url_parser = require('socket.url')
|
||
|
|
local ltn12 = require('ltn12')
|
||
|
|
local json = require('cjson.safe')
|
||
|
|
local md5sum = require('md5') -- TODO: Make modular?
|
||
|
|
local mime = require('mime')
|
||
|
|
|
||
|
|
local requests = {
|
||
|
|
_DESCRIPTION = 'Http requests made simpler',
|
||
|
|
http_socket = http_socket,
|
||
|
|
https_socket = https_socket
|
||
|
|
}
|
||
|
|
|
||
|
|
local _requests = {}
|
||
|
|
|
||
|
|
--User facing function the make a request use Digest Authentication
|
||
|
|
--TODO: Determine what else should live in authentication
|
||
|
|
function requests.HTTPDigestAuth(user, password)
|
||
|
|
return { _type = 'digest', user = user, password = password}
|
||
|
|
end
|
||
|
|
|
||
|
|
--User facing function the make a request use Basic Authentication
|
||
|
|
--TODO: Determine what else should live in authentication
|
||
|
|
function requests.HTTPBasicAuth(user, password)
|
||
|
|
return { _type = 'basic', user = user, password = password}
|
||
|
|
end
|
||
|
|
|
||
|
|
function requests.post(url, args)
|
||
|
|
return requests.request("POST", url, args)
|
||
|
|
end
|
||
|
|
|
||
|
|
function requests.get(url, args)
|
||
|
|
return requests.request("GET", url, args)
|
||
|
|
end
|
||
|
|
|
||
|
|
function requests.delete(url, args)
|
||
|
|
return requests.request("DELETE", url, args)
|
||
|
|
end
|
||
|
|
|
||
|
|
function requests.patch(url, args)
|
||
|
|
return requests.request("PATCH", url, args)
|
||
|
|
end
|
||
|
|
|
||
|
|
function requests.put(url, args)
|
||
|
|
return requests.request("PUT", url, args)
|
||
|
|
end
|
||
|
|
|
||
|
|
function requests.options(url, args)
|
||
|
|
return requests.request("OPTIONS", url, args)
|
||
|
|
end
|
||
|
|
|
||
|
|
function requests.head(url, args)
|
||
|
|
return requests.request("HEAD", url, args)
|
||
|
|
end
|
||
|
|
|
||
|
|
function requests.trace(url, args)
|
||
|
|
return requests.request("TRACE", url, args)
|
||
|
|
end
|
||
|
|
|
||
|
|
--Sets up all the data for a request and makes the request
|
||
|
|
function requests.request(method, url, args)
|
||
|
|
local request
|
||
|
|
|
||
|
|
if type(url) == "table" then
|
||
|
|
request = url
|
||
|
|
if not request.url and request[1] then
|
||
|
|
request.url = table.remove(request, 1)
|
||
|
|
end
|
||
|
|
else
|
||
|
|
request = args or {}
|
||
|
|
request.url = url
|
||
|
|
end
|
||
|
|
|
||
|
|
request.method = method
|
||
|
|
_requests.parse_args(request)
|
||
|
|
|
||
|
|
-- TODO: Find a better way to do this
|
||
|
|
if request.auth and request.auth._type == 'digest' then
|
||
|
|
local response = _requests.make_request(request)
|
||
|
|
return _requests.use_digest(response, request)
|
||
|
|
else
|
||
|
|
return _requests.make_request(request)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--Makes a request
|
||
|
|
function _requests.make_request(request)
|
||
|
|
local response_body = {}
|
||
|
|
local full_request = {
|
||
|
|
method = request.method,
|
||
|
|
url = request.url,
|
||
|
|
headers = request.headers,
|
||
|
|
sink = ltn12.sink.table(response_body),
|
||
|
|
redirect = request.allow_redirects,
|
||
|
|
proxy = request.proxy
|
||
|
|
}
|
||
|
|
if request.data then
|
||
|
|
full_request.source = ltn12.source.string(request.data)
|
||
|
|
end
|
||
|
|
|
||
|
|
local response = {}
|
||
|
|
local ok
|
||
|
|
local socket = string.find(full_request.url, '^https:') and not request.proxy and https_socket or http_socket
|
||
|
|
|
||
|
|
ok, response.status_code, response.headers, response.status = socket.request(full_request)
|
||
|
|
|
||
|
|
assert(ok, 'error in '..request.method..' request: '..response.status_code)
|
||
|
|
response.text = table.concat(response_body)
|
||
|
|
response.json = function () return json.decode(response.text) end
|
||
|
|
|
||
|
|
return response
|
||
|
|
end
|
||
|
|
|
||
|
|
--Parses through all the possible arguments for a request
|
||
|
|
function _requests.parse_args(request)
|
||
|
|
_requests.check_url(request)
|
||
|
|
_requests.check_data(request)
|
||
|
|
_requests.create_header(request)
|
||
|
|
_requests.check_timeout(request.timeout)
|
||
|
|
_requests.check_redirect(request.allow_redirects)
|
||
|
|
end
|
||
|
|
|
||
|
|
--Format the the url based on the params argument
|
||
|
|
function _requests.format_params(url, params) -- TODO: Clean
|
||
|
|
if not params or next(params) == nil then return url end
|
||
|
|
|
||
|
|
url = url..'?'
|
||
|
|
for key, value in pairs(params) do
|
||
|
|
if tostring(value) then
|
||
|
|
url = url..tostring(key)..'='
|
||
|
|
|
||
|
|
if type(value) == 'table' then
|
||
|
|
local val_string = ''
|
||
|
|
|
||
|
|
for _, val in ipairs(value) do
|
||
|
|
val_string = val_string..tostring(val)..','
|
||
|
|
end
|
||
|
|
|
||
|
|
url = url..val_string:sub(0, -2)
|
||
|
|
else
|
||
|
|
url = url..tostring(value)
|
||
|
|
end
|
||
|
|
|
||
|
|
url = url..'&'
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return url:sub(0, -2)
|
||
|
|
end
|
||
|
|
|
||
|
|
--Check that there is a URL given and append to it if params are passed in.
|
||
|
|
function _requests.check_url(request)
|
||
|
|
assert(request.url, 'No url specified for request')
|
||
|
|
request.url = _requests.format_params(request.url, request.params)
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Add to the HTTP header
|
||
|
|
function _requests.create_header(request)
|
||
|
|
request.headers = request.headers or {}
|
||
|
|
if request.data then
|
||
|
|
request.headers['Content-Length'] = request.data:len()
|
||
|
|
end
|
||
|
|
|
||
|
|
if request.cookies then
|
||
|
|
if request.headers.cookie then
|
||
|
|
request.headers.cookie = request.headers.cookie..'; '..request.cookies
|
||
|
|
else
|
||
|
|
request.headers.cookie = request.cookies
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if request.auth then
|
||
|
|
_requests.add_auth_headers(request)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--Makes sure that the data is in a format that can be sent
|
||
|
|
function _requests.check_data(request)
|
||
|
|
if type(request.data) == "table" then
|
||
|
|
request.data = json.encode(request.data)
|
||
|
|
elseif request.data then
|
||
|
|
request.data = tostring(request.data)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--Set the timeout
|
||
|
|
function _requests.check_timeout(timeout)
|
||
|
|
http_socket.TIMEOUT = timeout or 5
|
||
|
|
https_socket.TIMEOUT = timeout or 5
|
||
|
|
end
|
||
|
|
|
||
|
|
--Checks is allow_redirects parameter is set correctly
|
||
|
|
function _requests.check_redirect(allow_redirects)
|
||
|
|
if allow_redirects and type(allow_redirects) ~= "boolean" then
|
||
|
|
error("allow_redirects expects a boolean value. received type = "..type(allow_redirects))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--Create the Authorization header for Basic Auth
|
||
|
|
function _requests.basic_auth_header(request)
|
||
|
|
local encoded = mime.b64(request.auth.user..':'..request.auth.password)
|
||
|
|
request.headers.Authorization = 'Basic '..encoded
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Create digest authorization string for request header TODO: Could be better, but it should work
|
||
|
|
function _requests.digest_create_header_string(auth)
|
||
|
|
local authorization = 'Digest username="'..auth.user..'", realm="'..auth.realm..'", nonce="'..auth.nonce
|
||
|
|
authorization = authorization..'", uri="'..auth.uri..'", qop='..auth.qop..', nc='..auth.nc
|
||
|
|
authorization = authorization..', cnonce="'..auth.cnonce..'", response="'..auth.response..'"'
|
||
|
|
|
||
|
|
if auth.opaque then
|
||
|
|
authorization = authorization..', opaque="'..auth.opaque..'"'
|
||
|
|
end
|
||
|
|
|
||
|
|
return authorization
|
||
|
|
end
|
||
|
|
|
||
|
|
--MD5 hash all parameters
|
||
|
|
local function md5_hash(...)
|
||
|
|
return md5sum.sumhexa(table.concat({...}, ":"))
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Creates response hash TODO: Add functionality
|
||
|
|
function _requests.digest_hash_response(auth_table)
|
||
|
|
return md5_hash(
|
||
|
|
md5_hash(auth_table.user, auth_table.realm, auth_table.password),
|
||
|
|
auth_table.nonce,
|
||
|
|
auth_table.nc,
|
||
|
|
auth_table.cnonce,
|
||
|
|
auth_table.qop,
|
||
|
|
md5_hash(auth_table.method, auth_table.uri)
|
||
|
|
)
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Add digest authentication to the request header
|
||
|
|
function _requests.digest_auth_header(request)
|
||
|
|
if not request.auth.nonce then return end
|
||
|
|
|
||
|
|
request.auth.cnonce = request.auth.cnonce or string.format("%08x", os.time())
|
||
|
|
|
||
|
|
request.auth.nc_count = request.auth.nc_count or 0
|
||
|
|
request.auth.nc_count = request.auth.nc_count + 1
|
||
|
|
|
||
|
|
request.auth.nc = string.format("%08x", request.auth.nc_count)
|
||
|
|
|
||
|
|
local url = url_parser.parse(request.url)
|
||
|
|
request.auth.uri = url_parser.build{path = url.path, query = url.query}
|
||
|
|
request.auth.method = request.method
|
||
|
|
request.auth.qop = 'auth'
|
||
|
|
|
||
|
|
request.auth.response = _requests.digest_hash_response(request.auth)
|
||
|
|
|
||
|
|
request.headers.Authorization = _requests.digest_create_header_string(request.auth)
|
||
|
|
end
|
||
|
|
|
||
|
|
--Checks the resonse code and adds additional headers for Digest Auth
|
||
|
|
-- TODO: Rename this
|
||
|
|
function _requests.use_digest(response, request)
|
||
|
|
if response.status_code == 401 then
|
||
|
|
_requests.parse_digest_response_header(response,request)
|
||
|
|
_requests.create_header(request)
|
||
|
|
response = _requests.make_request(request)
|
||
|
|
response.auth = request.auth
|
||
|
|
response.cookies = request.headers.cookie
|
||
|
|
return response
|
||
|
|
else
|
||
|
|
response.auth = request.auth
|
||
|
|
response.cookies = request.headers.cookie
|
||
|
|
return response
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--Parse the first response from the host to make the Authorization header
|
||
|
|
function _requests.parse_digest_response_header(response, request)
|
||
|
|
for key, value in response.headers['www-authenticate']:gmatch('(%w+)="(%S+)"') do
|
||
|
|
request.auth[key] = value
|
||
|
|
end
|
||
|
|
|
||
|
|
if request.headers.cookie then
|
||
|
|
request.headers.cookie = request.headers.cookie..'; '..response.headers['set-cookie']
|
||
|
|
else
|
||
|
|
request.headers.cookie = response.headers['set-cookie']
|
||
|
|
end
|
||
|
|
|
||
|
|
request.auth.nc_count = 0
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Call the correct authentication header function
|
||
|
|
function _requests.add_auth_headers(request)
|
||
|
|
local auth_func = {
|
||
|
|
basic = _requests.basic_auth_header,
|
||
|
|
digest = _requests.digest_auth_header
|
||
|
|
}
|
||
|
|
|
||
|
|
auth_func[request.auth._type](request)
|
||
|
|
end
|
||
|
|
|
||
|
|
--Return public functions
|
||
|
|
requests._private = _requests
|
||
|
|
return requests
|