Packages

A runtime-configurable HTTP reverse proxy based on Bandit.

Current section

Files

Jump to
wayfarer lib wayfarer server websocket_proxy.ex
Raw

lib/wayfarer/server/websocket_proxy.ex

defmodule Wayfarer.Server.WebSocketProxy do
@moduledoc """
When a connection is upgraded to a websocket, we switch from handing via
`Plug` to `WebSock` via `WebSockAdapter`.
The outgoing connection is made using `Mint.WebSocket`.
"""
@behaviour WebSock
alias Mint.WebSocket
alias Plug.Conn
alias Wayfarer.Telemetry
require Logger
@default_opts [extensions: [WebSocket.PerMessageDeflate]]
@doc false
@impl true
def init({mint, conn, proto}) when proto in [:ws, :wss] do
request_path =
case {conn.request_path, conn.query_string} do
{path, nil} -> path
{path, ""} -> path
{path, query} -> path <> "?" <> query
end
case WebSocket.upgrade(proto, mint, request_path, proxy_headers(conn), @default_opts) do
{:ok, mint, ref} -> {:ok, %{mint: mint, ref: ref, status: :init, buffer: [], conn: conn}}
{:error, _mint, reason} -> {:error, reason}
end
end
def init({mint, conn, :https}), do: init({mint, conn, :wss})
def init({mint, conn, :http}), do: init({mint, conn, :ws})
@doc false
@impl true
def handle_control({frame, [{:opcode, :ping}]}, state) do
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, {:ping, frame}),
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
conn = request_client_frame(state.conn, {:ping, frame})
{:ok, %{state | websocket: websocket, mint: mint, conn: conn}}
else
error -> handle_error(error, state)
end
end
def handle_control({frame, [{:opcode, frame_type}]}, state) do
conn = request_client_frame(state.conn, {frame_type, frame})
{:ok, %{state | conn: conn}}
end
@doc false
@impl true
def handle_in({payload, [{:opcode, frame_type}]}, state) when state.status == :init do
frame = {frame_type, payload}
buffer = [frame | state.buffer]
conn = request_client_frame(state.conn, frame)
{:ok, %{state | buffer: buffer, conn: conn}}
end
def handle_in({payload, [{:opcode, frame_type}]}, state) do
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, {frame_type, payload}),
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
conn = request_client_frame(state.conn, {frame_type, payload})
{:ok, %{state | websocket: websocket, mint: mint, conn: conn}}
else
error -> handle_error(error, state)
end
end
@doc false
@impl true
def handle_info(msg, state) when state.status == :init do
with {:ok, mint, result} <- WebSocket.stream(state.mint, msg),
{:ok, result} <- handle_upgrade_response(result, state.ref),
{:ok, mint, websocket} <- WebSocket.new(mint, state.ref, result.status, result.headers),
state <- Map.merge(state, %{status: :connected, websocket: websocket, mint: mint}),
{:ok, state} <- empty_buffer(state),
{:ok, messages, state} <- decode_frames(result.data, state) do
response_for_messages(messages, state)
else
error -> handle_error(error, state)
end
end
def handle_info(msg, state) when state.status == :connected do
with {:ok, mint, result} <- WebSocket.stream(state.mint, msg),
{:ok, frames} <- handle_websocket_data(result, state.ref),
{:ok, messages, state} <- decode_frames(frames, %{state | mint: mint}) do
response_for_messages(messages, state)
else
error -> handle_error(error, state)
end
end
@doc false
@impl true
def terminate(_reason, _state), do: :ok
defp handle_error({:error, _, %{reason: reason}, _}, state),
do: handle_error({:error, reason}, state)
defp handle_error({:error, reason, state}, _state),
do: handle_error({:error, reason}, state)
defp handle_error({:error, reason}, state) do
Logger.debug(fn ->
"Dropping WebSocket connection for reason: #{inspect(reason)}"
end)
{:stop, :normal, state}
end
defp proxy_headers(conn) do
listener =
conn.private.wayfarer.listener
|> case do
%{address: address, port: port} when tuple_size(address) == 8 ->
"[#{:inet.ntoa(address)}]:#{port}"
%{address: address, port: port} ->
"#{:inet.ntoa(address)}:#{port}"
end
client =
conn
|> Conn.get_peer_data()
|> case do
%{address: address, port: port} when tuple_size(address) == 8 ->
"[#{:inet.ntoa(address)}]:#{port}"
%{address: address, port: port} ->
"#{:inet.ntoa(address)}:#{port}"
end
req_headers =
conn.req_headers
|> Enum.reject(
&(elem(&1, 0) in [
"connection",
"upgrade",
"sec-websocket-extensions",
"sec-websocket-key",
"sec-websocket-version"
])
)
[
{"forwarded", "by=#{listener};for=#{client};host=#{conn.host};proto=#{conn.scheme}"}
| req_headers
]
end
defp empty_buffer(state) when state.buffer == [], do: {:ok, state}
defp empty_buffer(state), do: do_empty_buffer(Enum.reverse(state.buffer), %{state | buffer: []})
defp do_empty_buffer([], state), do: {:ok, state}
defp do_empty_buffer([head | tail], state) do
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, head),
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
conn = request_client_frame(state.conn, head)
do_empty_buffer(tail, %{state | websocket: websocket, mint: mint, conn: conn})
end
end
defp handle_upgrade_response(result, ref), do: handle_upgrade_response(result, ref, %{data: []})
defp handle_upgrade_response([{:done, ref}], ref, result), do: {:ok, result}
defp handle_upgrade_response([{:status, ref, status} | tail], ref, result) do
handle_upgrade_response(tail, ref, Map.put(result, :status, status))
end
defp handle_upgrade_response([{:headers, ref, headers} | tail], ref, result) do
handle_upgrade_response(tail, ref, Map.put(result, :headers, headers))
end
defp handle_upgrade_response([{:data, ref, data} | tail], ref, result) do
result = Map.update!(result, :data, &[data | &1])
handle_upgrade_response(tail, ref, result)
end
defp handle_websocket_data(data, ref),
do: handle_websocket_data(data, ref, [])
defp handle_websocket_data([], _ref, messages), do: {:ok, Enum.reverse(messages)}
defp handle_websocket_data([{:data, ref, data} | tail], ref, messages),
do: handle_websocket_data(tail, ref, [data | messages])
defp decode_frames(frames, state) do
frames
|> Enum.reduce_while({:ok, [], state}, fn frame, {:ok, messages, state} ->
case decode_frame(state, frame) do
{:ok, new_messages, state} -> {:cont, {:ok, [new_messages, messages], state}}
{:error, reason, state} -> {:halt, {:error, reason, state}}
end
end)
|> case do
{:ok, messages, state} -> {:ok, List.flatten(messages), state}
{:error, reason, state} -> {:error, reason, state}
end
end
defp decode_frame(state, frame) do
case WebSocket.decode(state.websocket, frame) do
{:ok, websocket, frames} when is_list(frames) ->
conn = Enum.reduce(frames, state.conn, &request_server_frame(&2, &1))
{:ok, frames, %{state | websocket: websocket, conn: conn}}
{:error, websocket, reason} ->
{:error, reason, %{state | websocket: websocket}}
end
end
# Handle all the frames coming from the target and decide how to respond to
# Bandit/WebSock. In the case of encountering a close frame, we terminate the
# client websocket with the same code, otherwise we just copy the frames over.
defp response_for_messages(messages, state, response \\ [])
defp response_for_messages([], state, []), do: {:ok, state}
defp response_for_messages([], state, response), do: {:push, Enum.reverse(response), state}
defp response_for_messages([{:close, code, _} | _], state, response),
do: {:stop, :normal, code, Enum.reverse(response), state}
defp response_for_messages([message | messages], state, response),
do: response_for_messages(messages, state, [message | response])
defp request_client_frame(conn, {frame_type, frame}) do
frame_size = byte_size(frame)
conn
|> Telemetry.increment_metrics(%{
client_frame_bytes: frame_size,
client_frame_count: 1
})
|> Telemetry.request_client_frame(frame_size, frame_type)
end
defp request_server_frame(conn, {frame_type, frame}) do
frame_size = byte_size(frame)
conn
|> Telemetry.increment_metrics(%{
server_frame_bytes: frame_size,
server_frame_count: 1
})
|> Telemetry.request_server_frame(frame_size, frame_type)
end
end