Created
May 3, 2017 06:25
-
-
Save jsopenrb/15afde13f58fa884d2340e6beaa271d4 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 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('json') | |
local encdec = require('encdec') | |
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 | |
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, | |
source = ltn12.source.string(request.data), | |
sink = ltn12.sink.table(response_body), | |
redirect = request.allow_redirects, | |
proxy = request.proxy | |
} | |
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.pdecode(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 {} | |
request.headers['Content-Length'] = request.data:len() | |
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) | |
request.data = request.data or '' | |
if type(request.data) == "table" then | |
request.data = json.encode(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 = encdec.base64enc(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 = '' | |
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 encdec.md5(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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment