Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions http-scm-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ build = {
["http.websocket"] = "http/websocket.lua";
["http.zlib"] = "http/zlib.lua";
["http.compat.prosody"] = "http/compat/prosody.lua";
["http.compat.socket"] = "http/compat/socket.lua";
};
}
179 changes: 179 additions & 0 deletions http/compat/socket.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
--[[
Compatibility layer with luasocket's socket.http module
Documentation: http://w3.impa.br/~diego/software/luasocket/http.html

This module a few key differences:
- The `.create` member is not supported
- The user-agent will be from lua-http
- lua-http features (such as HTTPS and HTTP2) will be used where possible
- trailers are currently discarded
- error messages are differents
]]

local monotime = require "cqueues".monotime
local ce = require "cqueues.errno"
local request = require "http.request"
local version = require "http.version"
local reason_phrases = require "http.h1_reason_phrases"

local _M = {
PROXY = nil; -- default proxy used for connections;
TIMEOUT = 60; -- timeout for all I/O operations;
-- default user agent reported to server.
USERAGENT = string.format("%s/%s (luasocket compatibility layer)",
version.name, version.version);
}

local function ltn12_pump_step(src, snk)
local chunk, src_err = src()
local ret, snk_err = snk(chunk, src_err)
if chunk and ret then return 1
else return nil, src_err or snk_err end
end

local function get_body_as_string(stream, deadline)
local body, err = stream:get_body_as_string(deadline and deadline-monotime())
if not body then
if err == ce.EPIPE then
return nil
else
return nil, err
end
end
return body
end

local function returns_1()
return 1
end

function _M.request(reqt, b)
local deadline = _M.TIMEOUT and (monotime()+_M.TIMEOUT)
local req, proxy, user_headers, get_body
if type(reqt) == "string" then
req = request.new_from_uri(reqt)
proxy = _M.PROXY
if b ~= nil then
assert(type(b) == "string", "body must be nil or string")
req.headers:upsert(":method", "POST")
req:set_body(b)
req.headers:upsert("content-type", "application/x-www-form-urlencoded")
end
get_body = get_body_as_string
else
assert(reqt.create == nil, "'create' option not supported")
req = request.new_from_uri(reqt.url)
proxy = reqt.proxy or _M.PROXY
if reqt.host ~= nil then
req.host = reqt.host
end
if reqt.port ~= nil then
req.port = reqt.port
end
if reqt.method ~= nil then
assert(type(reqt.method) == "string", "'method' option must be nil or string")
req.headers:upsert(":method", reqt.method)
end
if reqt.redirect == false then
req.follow_redirects = false
else
req.max_redirects = 5 - (reqt.nredirects or 0)
end
user_headers = reqt.headers
local step = reqt.step or ltn12_pump_step
local src = reqt.source
if src ~= nil then
local co = coroutine.create(function()
while true do
assert(step(src, coroutine.yield))
end
end)
req:set_body(function()
-- Pass true through to coroutine to indicate success of last write
local ok, chunk, err = coroutine.resume(co, true)
if not ok then
error(chunk)
elseif err then
error(err)
else
return chunk
end
end)
end
local sink = reqt.sink
-- luasocket returns `1` when using a request table
if sink ~= nil then
get_body = function(stream, deadline) -- luacheck: ignore 431
local function res_body_source()
local chunk, err = stream:get_next_chunk(deadline and deadline-monotime())
if not chunk then
if err == ce.EPIPE then
return nil
else
return nil, err
end
end
return chunk
end
-- This loop is the same as ltn12.pump.all
while true do
local ok, err = step(res_body_source, sink)
if not ok then
if err then
return nil, err
else
return 1
end
end
end
end
else
get_body = returns_1
end
end
req.headers:upsert("user-agent", _M.USERAGENT)
if proxy then
error("PROXYs are not currently supported by lua-http")
end
if user_headers then
for name, field in pairs(user_headers) do
if name == "host" then
req.headers:upsert(":authority", field)
else
req.headers:append(name:lower(), field)
end
end
end
local res_headers, stream = req:go(deadline and deadline-monotime())
if not res_headers then
if stream == ce.EPIPE then
return nil, "closed"
elseif stream == ce.ETIMEDOUT then
return nil, "timeout"
else
return nil, stream
end
end
local code = res_headers:get(":status")
local status = reason_phrases[code]
-- In luasocket, status codes are returned as numbers
code = tonumber(code, 10) or code
local headers = {}
for name in res_headers:each() do
if name ~= ":status" and headers[name] == nil then
headers[name] = res_headers:get_comma_separated(name)
end
end
local body, err = get_body(stream, deadline)
stream:shutdown()
if not body then
if err == ce.ETIMEDOUT then
return nil, "timeout"
else
return nil, err
end
end
return body, code, headers, status
end

return _M
147 changes: 147 additions & 0 deletions spec/compat_socket_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
local TEST_TIMEOUT = 2
describe("http.compat.socket module", function()
local http = require "http.compat.socket"
local new_headers = require "http.headers".new
local server = require "http.server"
local util = require "http.util"
local cqueues = require "cqueues"
local function assert_loop(cq, timeout)
local ok, err, _, thd = cq:loop(timeout)
if not ok then
if thd then
err = debug.traceback(thd, err)
end
error(err, 2)
end
end
it("fails safely on an invalid host", function()
-- in the luasocket example they use 'wrong.host', but 'host' is now a valid TLD.
-- use 'wrong.invalid' instead for this test.
local r, e = http.request("http://wrong.invalid/")
assert.same(r, nil)
-- in luasocket, the error is documented as "host not found", but we allow something else
assert.same("string", type(e))
end)
it("works against builtin server with GET request", function()
local cq = cqueues.new()
local s = server.listen {
host = "localhost";
port = 0;
}
assert(s:listen())
local _, host, port = s:localname()
local authority = util.to_authority(host, port, "http")
cq:wrap(function()
s:run(function (stream)
local request_headers = assert(stream:get_headers())
s:pause()
assert.same("http", request_headers:get ":scheme")
assert.same("GET", request_headers:get ":method")
assert.same("/foo", request_headers:get ":path")
assert.same(authority, request_headers:get ":authority")
local headers = new_headers()
headers:append(":status", "200")
headers:append("connection", "close")
assert(stream:write_headers(headers, false))
assert(stream:write_chunk("hello world", true))
end)
s:close()
end)
cq:wrap(function()
local r, e = http.request("http://"..authority.."/foo")
assert.same("hello world", r)
assert.same(200, e)
end)
assert_loop(cq, TEST_TIMEOUT)
assert.truthy(cq:empty())
end)
it("works against builtin server with POST request", function()
local cq = cqueues.new()
local s = server.listen {
host = "localhost";
port = 0;
}
assert(s:listen())
local _, host, port = s:localname()
local authority = util.to_authority(host, port, "http")
cq:wrap(function()
s:run(function (stream)
s:pause()
local request_headers = assert(stream:get_headers())
assert.same("http", request_headers:get ":scheme")
assert.same("POST", request_headers:get ":method")
assert.same("/foo", request_headers:get ":path")
assert.same(authority, request_headers:get ":authority")
local body = assert(stream:get_body_as_string())
assert.same("a body", body)
local headers = new_headers()
headers:append(":status", "201")
headers:append("connection", "close")
assert(stream:write_headers(headers, false))
assert(stream:write_chunk("hello world", true))
end)
s:close()
end)
cq:wrap(function()
local r, e = http.request("http://"..authority.."/foo", "a body")
assert.same("hello world", r)
assert.same(201, e)
end)
assert_loop(cq, TEST_TIMEOUT)
assert.truthy(cq:empty())
end)
it("works against builtin server with complex request", function()
local cq = cqueues.new()
local s = server.listen {
host = "localhost";
port = 0;
}
assert(s:listen())
local _, host, port = s:localname()
cq:wrap(function()
s:run(function (stream)
s:pause()
local request_headers = assert(stream:get_headers())
assert.same("http", request_headers:get ":scheme")
assert.same("PUT", request_headers:get ":method")
assert.same("/path?query", request_headers:get ":path")
assert.same("otherhost.com:8080", request_headers:get ":authority")
assert.same("fun", request_headers:get "myheader")
assert.same("normalised", request_headers:get "camelcase")
assert(stream:write_continue())
local body = assert(stream:get_body_as_string())
assert.same("a body", body)
local headers = new_headers()
headers:append(":status", "404")
headers:append("connection", "close")
assert(stream:write_headers(headers, false))
assert(stream:write_chunk("hello world", true))
end)
s:close()
end)
cq:wrap(function()
local r, e = assert(http.request {
url = "http://example.com/path?query";
host = host;
port = port;
method = "PUT";
headers = {
host = "otherhost.com:8080";
myheader = "fun";
CamelCase = "normalised";
};
source = coroutine.wrap(function()
coroutine.yield("a body")
end);
sink = coroutine.wrap(function(b)
assert.same("hello world", b)
assert.same(nil, coroutine.yield(true))
end);
})
assert.same(1, r)
assert.same(404, e)
end)
assert_loop(cq, TEST_TIMEOUT)
assert.truthy(cq:empty())
end)
end)