The purpose of this project is to demonstrate that you cannot proxy websockets through Kong when using Chrome or Firefox.
Start a simple Spring Boot application with an embedded Tomcat and a Kong instance by executing
$ docker-compose build
$ docker-compose upOpen https://www.piesocket.com/websocket-tester, enter ws://localhost:8000/hello and press connect.
Expected behaviour The client should be connected to the server.
Actual behaviour Connection failed
The request and response data from the upstream service can be found at http://localhost:8080/actuator/httptrace
If we instead connect to ws://localhost:8080/hello which will bypass Kong and go directly to the upstream service you can see that it successfully connect to the websocket.
The difference between the two responses are that the one that goes directly to
the upstream service has the Upgrade: websocket header in the response, but it
is missing for the request that goes through Kong.
If we take a look at the request handler in Kong we can see that the upstream
Connection header is set to keep-alive, Upgrade
https://github.com/Kong/kong/blob/master/kong/runloop/handler.lua#L1244-L1247
-- Keep-Alive and WebSocket Protocol Upgrade Headers
if var.http_upgrade and lower(var.http_upgrade) == "websocket" then
var.upstream_connection = "keep-alive, Upgrade"
var.upstream_upgrade = "websocket"It was introduced in Kong/kong#5495 and was before set
to only "Upgrade"
If we continue our investigation and see why Kong drops the Upgrade header
we find the following lines where the response headers are filtered:
https://github.com/Kong/kong/blob/master/kong/runloop/handler.lua#L1398-L1401
-- clear hop-by-hop response headers:
for _, header_name in csv(var.upstream_http_connection) do
header[header_name] = nil
endThe csv function is called and all headers returned will be removed in the response. https://github.com/Kong/kong/blob/master/kong/runloop/handler.lua#L159-L170
local function csv(s)
if type(s) ~= "string" or s == "" then
return csv_iterator, s, -1
end
s = lower(s)
if s == "close" or s == "upgrade" or s == "keep-alive" then
return csv_iterator, s, -1
end
return csv_iterator, s, 1
endAs we can see above, the csv function will return an empty list in case s
is equal to upgrade. However, since our Connection header is keep-alive, Upgrade,
which is not equal to upgrade, the method will now return an array with
two elements; keep-alive and Upgrade. The headers keep-alive and upgrade
will be removed from the response, giving us our actual behaviour.
This can also be confirmed by the posted workaround in Kong/kong#5714 (comment)
where the Connection header is explicitly overridden and set to the single value Upgrade.
Since the header value now is equal to upgrade the csv function will return an empty array
and no headers will be dropped from the response, making the client successfully connect to the server.