diff --git a/http/request.lua b/http/request.lua index 40f7a19c..f37a1aac 100644 --- a/http/request.lua +++ b/http/request.lua @@ -167,6 +167,146 @@ function request_methods:to_uri(with_userinfo) return scheme .. "://" .. authority .. path end +function request_methods:to_curl() + local cmd = { + "curl"; + } + local n = 1 + + if self.version then + if self.version == 1 then + cmd[n+1] = "-0" + elseif self.version == 1.1 then + cmd[n+1] = "--http1.1" + elseif self.version == 2 then + cmd[n+1] = "--http2" + else + error("invalid version") + end + n = n + 1 + end + + if self.proxy then + if type(self.proxy) ~= "string" then + error("NYI") + end + cmd[n+1] = "--proxy" + cmd[n+2] = self.proxy + n = n + 2 + elseif not self.proxies then + cmd[n+1] = "--noproxy" + cmd[n+2] = "*" + n = n + 2 + elseif self.proxies ~= default_proxies then + assert(getmetatable(self.proxies) == http_proxies.mt, "proxies property should be a http.proxies object") + error("NYI") + end + + if self.expect_100_timeout ~= 1 then + cmd[n+1] = "--expect100-timeout" + cmd[n+2] = string.format("%d", self.expect_100_timeout) + n = n + 2 + end + + if self.follow_redirects then + cmd[n+1] = "--location-trusted" + cmd[n+2] = "-e" + cmd[n+3] = ";auto" + n = n + 3 + + if self.max_redirects ~= 50 then -- curl default is 50 + cmd[n+1] = "--max-redirs" + cmd[n+2] = string.format("%d", self.max_redirects or -1) + n = n + 2 + end + + if self.post301 then + cmd[n+1] = "--post301" + n = n + 1 + end + + if self.post302 then + cmd[n+1] = "--post302" + n = n + 1 + end + end + + if self.tls and self.tls ~= true then + error("NYI") + end + + local scheme = self.headers:get(":scheme") + -- Unlike the ':to_uri' method, curl needs the authority in the URI to be the actual host/port + local authority = http_util.to_authority(self.host, self.port, scheme) + local path = self.headers:get(":path") + assert(path == "" or path:sub(1,1) == "/" or path:sub(1,1) == "?", "invalid path for cURL") + local url = scheme .. "://" .. authority .. path + if url:match("[%[%]%{%}]") then + -- Turn off curl URL globbing + cmd[n+1] = "-g" + n = n + 1 + end + cmd[n+1] = url + n = n + 1 + + for name, value in self.headers:each() do + if name:sub(1,1) == ":" then + if name == ":authority" then + if value ~= authority then + cmd[n+1] = "-H" + cmd[n+2] = "host: " .. value + n = n + 2 + end + elseif name == ":method" then + if value == "HEAD" then + cmd[n+1] = "-I" + n = n + 1 + elseif (value ~= "GET" or self.body ~= nil) and (value ~= "POST" or self.body == nil) then + cmd[n+1] = "-X" + cmd[n+2] = value + n = n + 2 + end + end + elseif name == "user-agent" then + cmd[n+1] = "-A" + cmd[n+2] = value + n = n + 2 + elseif name == "referer" then + cmd[n+1] = "-e" + assert(not value:match(";"), "cannot render referer") + if self.follow_redirects then + cmd[n+2] = value .. ";auto" + else + cmd[n+2] = value + end + n = n + 2 + else + cmd[n+1] = "-H" + cmd[n+2] = name .. ": " .. value + n = n + 2 + end + end + + if self.body then + if type(self.body) == "string" then + cmd[n+1] = "--data-raw" + cmd[n+2] = self.body + n = n + 2 + else + error("NYI") + end + end + + -- escape ready for a command line + for i=1, n do + local arg = cmd[i] + if arg:match("[^%w%_%:%/%@%^%.%-]") then + cmd[i] = "'" .. arg:gsub("'", "'\\''") .. "'" + end + end + return table.concat(cmd, " ", 1, n) +end + function request_methods:handle_redirect(orig_headers) local max_redirects = self.max_redirects if max_redirects <= 0 then diff --git a/spec/request_spec.lua b/spec/request_spec.lua index 63315a4a..8f7456ae 100644 --- a/spec/request_spec.lua +++ b/spec/request_spec.lua @@ -157,6 +157,131 @@ describe("http.request module", function() test("https://foo:bar@example.com:1234") assert.has.errors(function() test("https://example.com/path") end) end) + describe(":to_curl() #curl", function() + it("lua-http defaults", function() + local req = request.new_from_uri("http://example.com/") + req.headers:delete "user-agent" -- take variability out of tests + assert.same("curl --location-trusted -e ';auto' --max-redirs 5 http://example.com/", req:to_curl()) + end) + local req_template = request.new_from_uri("http://example.com/") + req_template.headers:delete "user-agent" -- take variability out of tests + req_template.follow_redirects = false -- closer to curl defaults + req_template.max_redirects = 50 + it("curl defaults", function() + local req = req_template:clone() + assert.same("curl http://example.com/", req:to_curl()) + end) + it("http version 1.0", function() + local req = req_template:clone() + req.version = 1 + assert.same("curl -0 http://example.com/", req:to_curl()) + end) + it("http version 1.1", function() + local req = req_template:clone() + req.version = 1.1 + assert.same("curl --http1.1 http://example.com/", req:to_curl()) + end) + it("http version 2", function() + local req = req_template:clone() + req.version = 2 + assert.same("curl --http2 http://example.com/", req:to_curl()) + end) + it("expect_100_timeout flag", function() + local req = req_template:clone() + req.expect_100_timeout = 2 + assert.same("curl --expect100-timeout 2 http://example.com/", req:to_curl()) + end) + it("post301 flag", function() + local req = req_template:clone() + req.follow_redirects = nil + req.post301 = true + assert.same("curl --location-trusted -e ';auto' --post301 http://example.com/", req:to_curl()) + end) + it("post302 flag", function() + local req = req_template:clone() + req.follow_redirects = nil + req.post302 = true + assert.same("curl --location-trusted -e ';auto' --post302 http://example.com/", req:to_curl()) + end) + it("path component", function() + local req = req_template:clone() + req.headers:upsert(":path", "/[complex]&path{component}") + assert.same("curl -g 'http://example.com/[complex]&path{component}'", req:to_curl()) + end) + it("query component", function() + local req = req_template:clone() + req.headers:upsert(":path", "/path?query") + assert.same("curl 'http://example.com/path?query'", req:to_curl()) + end) + it("scheme change", function() + local req = req_template:clone() + req.headers:upsert(":scheme", "https") + assert.same("curl https://example.com:80/ -H 'host: example.com'", req:to_curl()) + end) + it("scheme and port change", function() + local req = req_template:clone() + req.headers:upsert(":scheme", "https") + req.port = 443 + assert.same("curl https://example.com/", req:to_curl()) + end) + it("port change", function() + local req = req_template:clone() + req.port = 443 + assert.same("curl http://example.com:443/ -H 'host: example.com'", req:to_curl()) + end) + it("host change", function() + local req = req_template:clone() + req.headers:upsert(":authority", "foo.com") + assert.same("curl http://example.com/ -H 'host: foo.com'", req:to_curl()) + end) + it("proxy", function() + local req = req_template:clone() + req.proxy = "http://foo.com" + assert.same("curl --proxy http://foo.com http://example.com/", req:to_curl()) + end) + it("proxies", function() + local req = req_template:clone() + req.proxies = false + assert.same("curl --noproxy '*' http://example.com/", req:to_curl()) + end) + it("head method", function() + local req = req_template:clone() + req.headers:upsert(":method", "HEAD") + assert.same("curl http://example.com/ -I", req:to_curl()) + end) + it("post method with body", function() + local req = req_template:clone() + req.headers:upsert(":method", "POST") + req.body = "foo" + assert.same("curl http://example.com/ --data-raw foo", req:to_curl()) + end) + it("post method without body", function() + local req = req_template:clone() + req.headers:upsert(":method", "POST") + assert.same("curl http://example.com/ -X POST", req:to_curl()) + end) + it("referer header", function() + local req = req_template:clone() + req.headers:upsert("user-agent", "myuseragent") + assert.same("curl http://example.com/ -A myuseragent", req:to_curl()) + end) + it("referer header", function() + local req = req_template:clone() + req.headers:upsert("referer", "foo.com") + assert.same("curl http://example.com/ -e foo.com", req:to_curl()) + end) + it("referer header when follow_redirects is on", function() + local req = req_template:clone() + req.headers:upsert("referer", "foo.com") + req.follow_redirects = nil + assert.same("curl --location-trusted -e ';auto' http://example.com/ -e 'foo.com;auto'", req:to_curl()) + end) + it("custom headers", function() + local req = req_template:clone() + req.headers:append("foo", "bar") + assert.same("curl http://example.com/ -H 'foo: bar'", req:to_curl()) + end) + end) it(":set_body sets content-length for string arguments", function() local req = request.new_from_uri("http://example.com") assert.falsy(req.headers:has("content-length"))