I want to use HAPROXY to terminate requests made with the HTTP CONNECT method, converting them into TCP connections. Similarly I’d like to do the reverse - receive a TCP connection and then turn it into an HTTP CONNECT.
I’ve tried having an mode http
frontend with a mode tcp
backend, but this is not supported. I thought potentially it might be a filter, but I don’t see one.
Similar questions have been asked before, but with no response: HTTP CONNECT method handling
I’ve figured out a way of doing this with lua and the Channel API:
local function _http_response(code, message)
return "HTTP/1.0 " .. code .. " \r\nContent-Length: " .. #message .. "\r\n\r\n" .. message
end
local function read_headers(chan)
local headers = {}
while true do
local line = chan:getline()
if line == "\r\n" then
break
end
local key, value = line:match("^([^:]+):%s*([^\r\n]+)\r\n$")
if key then
headers[key] = value
end
end
return headers
end
-- An action that parses and removes the HTTP CONNECT header from the
-- tcp-request. It exposes the CONNECT HTTP headers to haproxy in the
-- txn.req_headers variable. Usage:
--
-- tcp-request inspect-delay 5s
-- tcp-request content lua.terminate_http_connect
local function terminate_http_connect(txn)
local req_chan = txn.req
local res_chan = txn.res
local first_line = req_chan:getline()
local method, uri, version = first_line:match(
"^CONNECT%s+([^%s]+)%s+HTTP/([0-9.]+)\r\n$")
-- It's an error to not have a CONNECT header:
if not method then
txn.res:send(_http_response("400", "Bad Request: Missing CONNECT header"))
txn:done()
return
end
-- Read all headers and expose them to haproxy:
txn:set_var("txn.req_headers", read_headers(req_chan))
-- Send CONNECT response back down the pipe:
txn.res:send("HTTP/1.0 200 Connection established\r\n\r\n")
-- Two way communication is now established
end
core.register_action("terminate_http_connect", { "tcp-req" }, terminate_http_connect)
-- Two actions that convert a TCP request into an HTTP CONNECT request. Usage:
--
-- tcp-request content lua.add_http_connect str('my.example.com')
-- tcp-response inspect-delay 5s
-- tcp-response content lua.strip_http_connect_response
local function add_http_connect(txn, x_hostname)
local req_chan = txn.req
local c = core.concat()
c:add("CONNECT blooblah.ste:8080 HTTP/1.0\r\n")
c:add("X-Hostname: ")
c:add(x_hostname)
c:add("\r\n\r\n")
req_chan:send(c:dump())
end
core.register_action("add_http_connect", { "tcp-req" }, add_http_connect, 1)
-- An action that strips the HTTP CONNECT response header from the response:
local function strip_http_connect_response(txn)
local res_chan = txn.res
local first_line = res_chan:getline()
local version, code = first_line:match("^HTTP/([0-9.]+)%s+(%d+)")
if not version then
txn.res:send(_http_response("500", "Internal Server Error: Missing HTTP response"))
txn:done()
return
end
-- Read all headers and expose them to haproxy:
txn:set_var("txn.res_headers", read_headers(res_chan))
end
core.register_action("strip_http_connect_response", { "tcp-res" }, strip_http_connect_response)
I couldn’t figure out how to pass variables into the actions so it interfaces with the haproxy config via setting variables.
In my usecase I add a CONNECT header to a mode tcp
stream, and then have that connect on localhost back to haproxy to a mode http
frontend to be processed as HTTP. It would be nice to have a syntax for chaining backends to frontends without send the data out and back again over a socket. Maybe haproxy supports this?, but I’ve not been able to figure it out.