Skip to content

Commit

Permalink
Introduce new HTTP1 option: skip_target_validation
Browse files Browse the repository at this point in the history
As discussed in #453

I tried to follow the guidance of `case_sensitive_headers` so
that these options are treated somewhat similarly.
  • Loading branch information
PragTob committed Nov 8, 2024
1 parent 5e9eac1 commit d5f397e
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 9 deletions.
12 changes: 10 additions & 2 deletions lib/mint/http1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ defmodule Mint.HTTP1 do
:mode,
:scheme_as_string,
:case_sensitive_headers,
:skip_target_validation,
requests: :queue.new(),
state: :closed,
buffer: "",
Expand Down Expand Up @@ -123,6 +124,10 @@ defmodule Mint.HTTP1 do
* `:case_sensitive_headers` - (boolean) if set to `true` the case of the supplied
headers in requests will be preserved. The default is to lowercase the headers
because HTTP/1.1 header names are case-insensitive. *Available since v1.6.0*.
* `:skip_target_validation` - (boolean) if set to `true` the target of a request
will not be validated. You might want this if you deal with non standard-
conform URIs but need to preserve them. The default is to validate the request
target. *Available since v1.?.?*
"""
@spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) ::
Expand Down Expand Up @@ -200,7 +205,8 @@ defmodule Mint.HTTP1 do
scheme_as_string: Atom.to_string(scheme),
state: :open,
log: log?,
case_sensitive_headers: Keyword.get(opts, :case_sensitive_headers, false)
case_sensitive_headers: Keyword.get(opts, :case_sensitive_headers, false),
skip_target_validation: Keyword.get(opts, :skip_target_validation, false)
}

{:ok, conn}
Expand Down Expand Up @@ -275,12 +281,14 @@ defmodule Mint.HTTP1 do
|> add_default_headers(conn)

with {:ok, headers, encoding} <- add_content_length_or_transfer_encoding(headers, body),
# does have the conn here
{:ok, iodata} <-
Request.encode(
method,
path,
Headers.to_raw(headers, conn.case_sensitive_headers),
body
body,
conn.skip_target_validation
),
:ok <- transport.send(socket, iodata) do
request_ref = make_ref()
Expand Down
8 changes: 4 additions & 4 deletions lib/mint/http1/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ defmodule Mint.HTTP1.Request do

import Mint.HTTP1.Parse

def encode(method, target, headers, body) do
def encode(method, target, headers, body, skip_target_validation \\ false) do
body = [
encode_request_line(method, target),
encode_request_line(method, target, skip_target_validation),
encode_headers(headers),
"\r\n",
encode_body(body)
Expand All @@ -16,8 +16,8 @@ defmodule Mint.HTTP1.Request do
{:mint, reason} -> {:error, reason}
end

defp encode_request_line(method, target) do
validate_target!(target)
defp encode_request_line(method, target, skip_target_validation) do
unless skip_target_validation, do: validate_target!(target)
[method, ?\s, target, " HTTP/1.1\r\n"]
end

Expand Down
32 changes: 32 additions & 0 deletions test/mint/http1/conn_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,38 @@ defmodule Mint.HTTP1Test do
""")
end

@invalid_target "%%"
test "targets are validated by default", %{port: port, server_ref: server_ref} do
assert {:ok, conn} = HTTP1.connect(:http, "localhost", port)

assert_receive {^server_ref, _server_socket}

assert {:error, %Mint.HTTP1{},
%Mint.HTTPError{reason: {:invalid_request_target, @invalid_target}}} =
HTTP1.request(conn, "GET", @invalid_target, [], "")
end

test "target validation may be skipped based on connection options", %{
port: port,
server_ref: server_ref
} do
assert {:ok, conn} = HTTP1.connect(:http, "localhost", port, skip_target_validation: true)

assert_receive {^server_ref, server_socket}

assert {:ok, _conn, _ref} = HTTP1.request(conn, "GET", @invalid_target, [], "body")

assert receive_request_string(server_socket) ==
request_string("""
GET %% HTTP/1.1
content-length: 4
host: localhost:#{port}
user-agent: mint/#{Mix.Project.config()[:version]}
body\
""")
end
end

describe "streaming requests" do
Expand Down
14 changes: 11 additions & 3 deletions test/mint/http1/request_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ defmodule Mint.HTTP1.RequestTest do
""")
end

@invalid_request_targets ["/ /", "/%foo", "/foo%x"]
test "validates request target" do
for invalid_target <- ["/ /", "/%foo", "/foo%x"] do
for invalid_target <- @invalid_request_targets do
assert Request.encode("GET", invalid_target, [], nil) ==
{:error, {:invalid_request_target, invalid_target}}
end
Expand All @@ -43,6 +44,13 @@ defmodule Mint.HTTP1.RequestTest do
assert String.starts_with?(request, request_string("GET /foo%20bar HTTP/1.1"))
end

test "can optionally skip validating the request target" do
for invalid_target <- @invalid_request_targets do
request = encode_request("GET", invalid_target, [], nil, true)
assert String.starts_with?(request, request_string("GET #{invalid_target} HTTP/1.1"))
end
end

test "invalid header name" do
assert Request.encode("GET", "/", [{"f oo", "bar"}], nil) ==
{:error, {:invalid_header_name, "f oo"}}
Expand Down Expand Up @@ -76,8 +84,8 @@ defmodule Mint.HTTP1.RequestTest do
end
end

defp encode_request(method, target, headers, body) do
assert {:ok, iodata} = Request.encode(method, target, headers, body)
defp encode_request(method, target, headers, body, skip_target_validation \\ false) do
assert {:ok, iodata} = Request.encode(method, target, headers, body, skip_target_validation)
IO.iodata_to_binary(iodata)
end

Expand Down

0 comments on commit d5f397e

Please sign in to comment.