Packages
ace
0.19.0
0.19.0
0.18.10
0.18.9
0.18.8
0.18.7
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
retired
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
retired
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
HTTP web server and client, supports http1 and http2
Current section
Files
Jump to
Current section
Files
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