defmodule Bandit.HTTP2.Adapter do @moduledoc false # Implements the Plug-facing `Plug.Conn.Adapter` behaviour. These functions provide the primary # mechanism for Plug applications to interact with a client, including functions to read the # client body (if sent) and send response information back to the client. @behaviour Plug.Conn.Adapter defstruct connection: nil, peer: nil, stream_id: nil, end_stream: false, uri: nil @typedoc "A struct for backing a Plug.Conn.Adapter" @type t :: %__MODULE__{ connection: Bandit.HTTP2.Connection.t(), peer: Plug.Conn.Adapter.peer_data(), stream_id: Bandit.HTTP2.Stream.stream_id(), end_stream: boolean(), uri: URI.t() } # As described in the header documentation for the `Bandit.HTTP2.StreamTask` module, we # purposefully use raw `receive` message patterns here in order to facilitate an imperatively # structured blocking interface. Comments inline. @impl Plug.Conn.Adapter def read_req_body(%__MODULE__{end_stream: true}, _opts), do: raise(Bandit.BodyAlreadyReadError) def read_req_body(%__MODULE__{} = adapter, opts) do timeout = Keyword.get(opts, :read_timeout, 15_000) length = Keyword.get(opts, :length, 8_000_000) # Repeatedly stream messages from the Connection process using the bare `receive` primitive # and reduce this stream down based on their content (or on a timeout condition on receive) Stream.repeatedly(fn -> receive do msg -> msg after timeout -> :timeout end end) |> Enum.reduce_while([], fn {:data, data}, acc -> if byte_size(data) + IO.iodata_length(acc) <= length do {:cont, [data | acc]} else {:halt, {:more, [data | acc], adapter}} end :end_stream, acc -> {:halt, {:ok, acc, %{adapter | end_stream: true}}} :timeout, acc -> {:halt, {:more, acc, adapter}} end) |> case do {:ok, body, adapter} -> {:ok, body |> Enum.reverse() |> IO.iodata_to_binary(), adapter} {:more, body, adapter} -> {:more, body |> Enum.reverse() |> IO.iodata_to_binary(), adapter} end end @impl Plug.Conn.Adapter def send_resp(%__MODULE__{} = adapter, status, headers, body) do if IO.iodata_length(body) == 0 do send_headers(adapter, status, headers, true) else send_headers(adapter, status, headers, false) send_data(adapter, body, true) end {:ok, nil, adapter} end @impl Plug.Conn.Adapter def send_file(%__MODULE__{} = adapter, status, headers, path, offset, length) do %File.Stat{type: :regular, size: size} = File.stat!(path) length = if length == :all, do: size - offset, else: length cond do offset + length == size && offset == 0 -> send_chunked(adapter, status, headers) # As per Plug documentation `chunk/2` always returns `:ok` # and webservers SHOULDN'T modify any state on sending a chunk, # so there is no need to check the return value # of `chunk/2` and the adapter doesn't need updating. path |> File.stream!([], 2048) |> Enum.each(&chunk(adapter, &1)) # Send empty chunk to indicate the end of stream chunk(adapter, "") {:ok, nil, adapter} offset + length < size -> with {:ok, fd} <- :file.open(path, [:raw, :binary]), {:ok, data} <- :file.pread(fd, offset, length) do send_headers(adapter, status, headers, false) send_data(adapter, data, true) {:ok, nil, adapter} end true -> raise "Cannot read #{length} bytes starting at #{offset} as #{path} is only #{size} octets in length" end end @impl Plug.Conn.Adapter def send_chunked(%__MODULE__{} = adapter, status, headers) do send_headers(adapter, status, headers, false) {:ok, nil, adapter} end @impl Plug.Conn.Adapter def chunk(%__MODULE__{} = adapter, chunk) do # Sending an empty chunk implicitly ends the stream. This is a bit of an undefined corner of # the Plug.Conn.Adapter behaviour (see https://github.com/elixir-plug/plug/pull/535 for # details) and closing the stream here carves closest to the underlying HTTP/1.1 behaviour # (RFC7230§4.1). The whole notion of chunked encoding is moot in HTTP/2 anyway (RFC7540§8.1) # so this entire section of the API is a bit slanty regardless. send_data(adapter, chunk, chunk == <<>>) :ok end @impl Plug.Conn.Adapter def inform(adapter, status, headers) do headers = split_cookies(headers) headers = [{":status", to_string(status)} | headers] GenServer.call(adapter.connection, {:send_headers, adapter.stream_id, headers, false}) end @impl Plug.Conn.Adapter def push(adapter, path, headers) do headers = split_cookies(headers) headers = [{":path", path} | headers] headers = [{":authority", adapter.uri.authority} | headers] headers = [{":scheme", adapter.uri.scheme} | headers] headers = [{":method", "GET"} | headers] GenServer.call(adapter.connection, {:send_push, adapter.stream_id, headers}) end @impl Plug.Conn.Adapter def get_peer_data(%__MODULE__{peer: peer}), do: peer @impl Plug.Conn.Adapter def get_http_protocol(%__MODULE__{}), do: :"HTTP/2" defp send_headers(adapter, status, headers, end_stream) do headers = split_cookies(headers) headers = [{":status", to_string(status)} | headers] GenServer.call(adapter.connection, {:send_headers, adapter.stream_id, headers, end_stream}) end defp send_data(adapter, data, end_stream) do GenServer.call( adapter.connection, {:send_data, adapter.stream_id, data, end_stream}, :infinity ) end defp split_cookies(headers) do headers |> Enum.flat_map(fn {"cookie", cookie} -> cookie |> String.split("; ") |> Enum.map(fn crumb -> {"cookie", crumb} end) {header, value} -> [{header, value}] end) end end