Current section

Files

Jump to
ace lib ace http1 parser.ex
Raw

lib/ace/http1/parser.ex

defmodule Ace.HTTP1.Parser do
@moduledoc """
Incrementally parse an HTTP/1.x request.
"""
@typedoc """
Available options when initializing a new parser
"""
@type option :: {:max_line_length, integer}
@typep buffer :: String.t()
@typep read_start_line :: {:start_line, buffer, map}
@typep read_headers :: {:headers, buffer, HttpRequest, [HttpHeader], map}
@typep read_body :: {:body, buffer, non_neg_integer, map}
@typep read_body_chunked :: {:body_chunked, buffer, map}
@typep read_trailers :: {:trailers, buffer, map}
@typep read_done :: {:done, buffer, map}
@typedoc """
State tracking the progress of the Parser.
"""
@opaque state ::
read_start_line
| read_headers
| read_body
| read_body_chunked
| read_trailers
| read_done
@doc """
Initial state for the incremental parser.
"""
@spec new([option]) :: state
def new(opts) do
max_line_length = Keyword.get(opts, :max_line_length)
{:start_line, "", %{max_line_length: max_line_length}}
end
@doc """
Run the parser aginst a some new input.
This parser returns a list of parts that are in the input and an updated state.
"""
@spec parse(String.t(), state, [Raxx.part()]) :: {:ok, {[Raxx.part()], state}} | {:error, term}
def parse(binary, state, parts \\ []) do
state = append_buffer(state, binary)
case pop_part(state) do
{:ok, {nil, new_state}} ->
{:ok, {parts, new_state}}
{:ok, {new_part, new_state}} ->
parse("", new_state, parts ++ [new_part])
{:error, reason} ->
{:error, reason}
end
end
@doc """
Has the parser consumed and entire HTTP message?
"""
@spec done?(state) :: boolean
def done?({:done, _, _}) do
true
end
def done?(_state) do
false
end
defp append_buffer({:start_line, buffer, options}, binary) do
{:start_line, buffer <> binary, options}
end
defp append_buffer({:headers, buffer, start_line, headers, options}, binary) do
{:headers, buffer <> binary, start_line, headers, options}
end
defp append_buffer({:body, buffer, remaining, options}, binary) do
{:body, buffer <> binary, remaining, options}
end
defp append_buffer({:body_chunked, buffer, options}, binary) do
{:body_chunked, buffer <> binary, options}
end
defp append_buffer({:trailers, buffer, options}, binary) do
{:trailers, buffer <> binary, options}
end
defp append_buffer({:done, overflow, options}, binary) do
{:done, overflow <> binary, options}
end
defp pop_part(state = {:start_line, buffer, options}) do
case :erlang.decode_packet(:http_bin, buffer, line_length: options.max_line_length) do
{:more, :undefined} ->
{:ok, {nil, state}}
{:ok, start_line = {:http_request, _, _, _}, rest} ->
pop_part({:headers, rest, start_line, [], options})
{:ok, {:http_error, line}, _rest} ->
{:error, {:invalid_line, line}}
{:error, :invalid} ->
{:error, {:line_length_limit_exceeded, :request_line}}
end
end
defp pop_part(state = {:headers, buffer, start_line, headers, options}) do
case :erlang.decode_packet(:httph_bin, buffer, line_length: options.max_line_length) do
{:more, :undefined} ->
{:ok, {nil, state}}
{:ok, {:http_header, _, key, _, value}, rest} ->
new_headers = headers ++ [{String.downcase("#{key}"), value}]
pop_part({:headers, rest, start_line, new_headers, options})
{:ok, :http_eoh, rest} ->
# NOTE: transfer-encoding is not part of HTTP/2 spec, header is removed before parsing to Ace.Worker.
{transfer_encoding, clean_headers} = resolve_transfer_encoding(headers)
clean_headers = :proplists.delete("connection", clean_headers)
case build_partial_request(start_line, :proplists.get_value("host", headers)) do
{:ok, initial_request} ->
clean_headers = :proplists.delete("host", clean_headers)
request_head =
Enum.reduce(
clean_headers,
initial_request,
fn {k, v}, %{headers: headers} = request ->
Map.put(request, :headers, [{k, v} | headers])
end
)
case transfer_encoding do
nil ->
case content_length(headers) do
length when length in [0, nil] ->
{:ok, {request_head, {:done, rest, options}}}
length ->
{:ok, {%{request_head | body: true}, {:body, rest, length, options}}}
end
"chunked" ->
{:ok, {%{request_head | body: true}, {:body_chunked, rest, options}}}
end
{:error, :not_implemented} ->
{:error, :not_implemented}
end
{:ok, {:http_error, line}, _rest} ->
{:error, {:invalid_line, line}}
{:error, :invalid} ->
{:error, {:line_length_limit_exceeded, :header_line}}
end
end
defp pop_part({:body, "", remaining, options}) do
{:ok, {nil, {:body, "", remaining, options}}}
end
defp pop_part({:body, buffer, remaining, options}) when byte_size(buffer) < remaining do
part = Raxx.data(buffer)
{:ok, {part, {:body, "", remaining - byte_size(buffer), options}}}
end
defp pop_part({:body, buffer, remaining, options}) when byte_size(buffer) >= remaining do
<<data::binary-size(remaining), rest::binary>> = buffer
{:ok, {Raxx.data(data), {:trailers, rest, options}}}
end
defp pop_part({:body_chunked, buffer, options}) do
{:ok, {chunk, rest}} = Raxx.HTTP1.parse_chunk(buffer)
case chunk do
nil ->
{:ok, {nil, {:body_chunked, buffer, options}}}
"" ->
{:ok, {Raxx.tail([]), {:done, rest, options}}}
chunk ->
{:ok, {Raxx.data(chunk), {:body_chunked, rest, options}}}
end
end
defp pop_part({:trailers, buffer, options}) do
{:ok, {Raxx.tail([]), {:done, buffer, options}}}
end
defp pop_part(state = {:done, _overflow, _options}) do
{:ok, {nil, state}}
end
defp resolve_transfer_encoding(headers) do
case :proplists.get_value("transfer-encoding", headers) do
:undefined ->
{nil, headers}
binary ->
{binary, :proplists.delete("transfer-encoding", headers)}
end
end
defp content_length(headers) do
case :proplists.get_value("content-length", headers) do
:undefined ->
nil
binary ->
{content_length, ""} = Integer.parse(binary)
content_length
end
end
defp build_partial_request({:http_request, method, http_uri, _version}, host) do
path_string =
case http_uri do
{:abs_path, path_string} ->
path_string
{:absoluteURI, _scheme, _host, _port, path_string} ->
# Throw away the rest of the absolute URI since we are not proxying
path_string
end
# NOTE scheme is ignored from message.
# It should therefore not be part of request but part of connection. same as client ip etc
# However scheme is a required header in HTTP/2
# Q? how often is a host header sent with http in place
# %{scheme: scheme} = URI.parse(host)
# NOTE add invalid scheme and authority so that parsing a path with leading `//` is handled correctly
case method do
method when is_atom(method) ->
{:ok, %{Raxx.request(method, "raxx://root.example" <> path_string) | authority: host}}
# :erlang.decode_packet doesn't support patch as a known method.
"PATCH" ->
{:ok, %{Raxx.request(:PATCH, "raxx://root.example" <> path_string) | authority: host}}
_ ->
{:error, :not_implemented}
end
end
end