defmodule Mint.HTTP1.Request do @moduledoc false import Mint.HTTP1.Parse @user_agent "mint/" <> Mix.Project.config()[:version] def encode(method, target, host, headers, body) do headers = add_default_headers(headers, host, body) [ encode_request_line(method, target), encode_headers(headers), "\r\n", encode_body(body) ] end defp encode_request_line(method, target) do validate_target!(target) [method, ?\s, target, " HTTP/1.1\r\n"] end defp add_default_headers(headers, host, body) do headers |> add_content_length(body) |> put_new_header("user-agent", @user_agent) |> put_new_header("host", host) end defp add_content_length(headers, nil), do: headers defp add_content_length(headers, :stream), do: headers defp add_content_length(headers, body) do length = body |> IO.iodata_length() |> Integer.to_string() put_new_header(headers, "content-length", length) end defp put_new_header(headers, _name, nil) do headers end defp put_new_header(headers, name, value) do if List.keymember?(headers, name, 0) do headers else [{name, value} | headers] end end defp encode_headers(headers) do Enum.reduce(headers, "", fn {name, value}, acc -> validate_header_name!(name) validate_header_value!(name, value) [acc, name, ": ", value, "\r\n"] end) end defp encode_body(nil), do: "" defp encode_body(:stream), do: "" defp encode_body(body), do: body defp validate_target!(target) do _ = for <> do unless URI.char_unescaped?(char) do throw({:mint, :invalid_request_target}) end end :ok end defp validate_header_name!(name) do _ = for <> do unless is_tchar(char) do throw({:mint, {:invalid_header_name, name}}) end end :ok end defp validate_header_value!(name, value) do _ = for <> do unless is_vchar(char) or char in '\s\t' do throw({:mint, {:invalid_header_value, name, value}}) end end :ok end end