Skip to content

Commit 8516bcd

Browse files
PragTobwhatyouhide
andauthored
Introduce new HTTP/1 :skip_target_validation option (#456)
Closes #453. Co-authored-by: Andrea Leopardi <[email protected]>
1 parent 6d727bb commit 8516bcd

File tree

4 files changed

+85
-46
lines changed

4 files changed

+85
-46
lines changed

lib/mint/http1.ex

+34-1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ defmodule Mint.HTTP1 do
9393
:mode,
9494
:scheme_as_string,
9595
:case_sensitive_headers,
96+
:skip_target_validation,
9697
requests: :queue.new(),
9798
state: :closed,
9899
buffer: "",
@@ -123,6 +124,10 @@ defmodule Mint.HTTP1 do
123124
* `:case_sensitive_headers` - (boolean) if set to `true` the case of the supplied
124125
headers in requests will be preserved. The default is to lowercase the headers
125126
because HTTP/1.1 header names are case-insensitive. *Available since v1.6.0*.
127+
* `:skip_target_validation` - (boolean) if set to `true` the target of a request
128+
will not be validated. You might want this if you deal with non standard-
129+
conforming URIs but need to preserve them. The default is to validate the request
130+
target. *Available since v1.7.0*.
126131
127132
"""
128133
@spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) ::
@@ -200,7 +205,8 @@ defmodule Mint.HTTP1 do
200205
scheme_as_string: Atom.to_string(scheme),
201206
state: :open,
202207
log: log?,
203-
case_sensitive_headers: Keyword.get(opts, :case_sensitive_headers, false)
208+
case_sensitive_headers: Keyword.get(opts, :case_sensitive_headers, false),
209+
skip_target_validation: Keyword.get(opts, :skip_target_validation, false)
204210
}
205211

206212
{:ok, conn}
@@ -275,6 +281,7 @@ defmodule Mint.HTTP1 do
275281
|> add_default_headers(conn)
276282

277283
with {:ok, headers, encoding} <- add_content_length_or_transfer_encoding(headers, body),
284+
:ok <- validate_request_target(path, conn.skip_target_validation),
278285
{:ok, iodata} <-
279286
Request.encode(
280287
method,
@@ -964,6 +971,32 @@ defmodule Mint.HTTP1 do
964971
{:ok, body}
965972
end
966973

974+
defp validate_request_target(target, skip_validation?)
975+
defp validate_request_target(target, false), do: validate_target(target)
976+
defp validate_request_target(_, true), do: :ok
977+
978+
# Percent-encoding is not case sensitive so we have to account for lowercase and uppercase.
979+
@hex_characters ~c"0123456789abcdefABCDEF"
980+
981+
defp validate_target(target), do: validate_target(target, target)
982+
983+
defp validate_target(<<?%, char1, char2, rest::binary>>, original_target)
984+
when char1 in @hex_characters and char2 in @hex_characters do
985+
validate_target(rest, original_target)
986+
end
987+
988+
defp validate_target(<<char, rest::binary>>, original_target) do
989+
if URI.char_unescaped?(char) do
990+
validate_target(rest, original_target)
991+
else
992+
{:error, {:invalid_request_target, original_target}}
993+
end
994+
end
995+
996+
defp validate_target(<<>>, _original_target) do
997+
:ok
998+
end
999+
9671000
defp new_request(ref, method, body, encoding) do
9681001
state =
9691002
if body == :stream do

lib/mint/http1/request.ex

-23
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ defmodule Mint.HTTP1.Request do
1717
end
1818

1919
defp encode_request_line(method, target) do
20-
validate_target!(target)
2120
[method, ?\s, target, " HTTP/1.1\r\n"]
2221
end
2322

@@ -46,28 +45,6 @@ defmodule Mint.HTTP1.Request do
4645
[Integer.to_string(length, 16), "\r\n", chunk, "\r\n"]
4746
end
4847

49-
# Percent-encoding is not case sensitive so we have to account for lowercase and uppercase.
50-
@hex_characters ~c"0123456789abcdefABCDEF"
51-
52-
defp validate_target!(target), do: validate_target!(target, target)
53-
54-
defp validate_target!(<<?%, char1, char2, rest::binary>>, original_target)
55-
when char1 in @hex_characters and char2 in @hex_characters do
56-
validate_target!(rest, original_target)
57-
end
58-
59-
defp validate_target!(<<char, rest::binary>>, original_target) do
60-
if URI.char_unescaped?(char) do
61-
validate_target!(rest, original_target)
62-
else
63-
throw({:mint, {:invalid_request_target, original_target}})
64-
end
65-
end
66-
67-
defp validate_target!(<<>>, _original_target) do
68-
:ok
69-
end
70-
7148
defp validate_header_name!(name) do
7249
_ =
7350
for <<char <- name>> do

test/mint/http1/conn_test.exs

+51-12
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ defmodule Mint.HTTP1Test do
548548
request_string("""
549549
GET / HTTP/1.1
550550
host: localhost:#{port}
551-
user-agent: mint/#{Mix.Project.config()[:version]}
551+
user-agent: #{mint_user_agent()}
552552
553553
\
554554
""")
@@ -570,7 +570,7 @@ defmodule Mint.HTTP1Test do
570570
request_string("""
571571
GET / HTTP/1.1
572572
host: localhost
573-
user-agent: mint/#{Mix.Project.config()[:version]}
573+
user-agent: #{mint_user_agent()}
574574
575575
\
576576
""")
@@ -591,7 +591,7 @@ defmodule Mint.HTTP1Test do
591591
GET / HTTP/1.1
592592
content-length: 4
593593
host: localhost:#{port}
594-
user-agent: mint/#{Mix.Project.config()[:version]}
594+
user-agent: #{mint_user_agent()}
595595
596596
body\
597597
""")
@@ -607,7 +607,7 @@ defmodule Mint.HTTP1Test do
607607
request_string("""
608608
GET / HTTP/1.1
609609
host: localhost:#{port}
610-
user-agent: mint/#{Mix.Project.config()[:version]}
610+
user-agent: #{mint_user_agent()}
611611
612612
\
613613
""")
@@ -626,7 +626,7 @@ defmodule Mint.HTTP1Test do
626626
request_string("""
627627
GET / HTTP/1.1
628628
host: localhost:#{port}
629-
user-agent: mint/#{Mix.Project.config()[:version]}
629+
user-agent: #{mint_user_agent()}
630630
content-length: 10
631631
632632
body\
@@ -760,6 +760,42 @@ defmodule Mint.HTTP1Test do
760760
761761
""")
762762
end
763+
764+
@invalid_request_targets ["/ /", "/%foo", "/foo%x"]
765+
test "targets are validated by default", %{port: port, server_ref: server_ref} do
766+
assert {:ok, conn} = HTTP1.connect(:http, "localhost", port)
767+
768+
assert_receive {^server_ref, _server_socket}
769+
770+
for invalid_target <- @invalid_request_targets do
771+
assert {:error, %Mint.HTTP1{},
772+
%Mint.HTTPError{reason: {:invalid_request_target, ^invalid_target}}} =
773+
HTTP1.request(conn, "GET", invalid_target, [], "")
774+
end
775+
end
776+
777+
test "target validation may be skipped based on connection options", %{
778+
port: port,
779+
server_ref: server_ref
780+
} do
781+
assert {:ok, conn} = HTTP1.connect(:http, "localhost", port, skip_target_validation: true)
782+
783+
assert_receive {^server_ref, server_socket}
784+
785+
for invalid_target <- @invalid_request_targets do
786+
assert {:ok, _conn, _ref} = HTTP1.request(conn, "GET", invalid_target, [], "body")
787+
788+
assert receive_request_string(server_socket) ==
789+
request_string("""
790+
GET #{invalid_target} HTTP/1.1
791+
content-length: 4
792+
host: localhost:#{port}
793+
user-agent: #{mint_user_agent()}
794+
795+
body\
796+
""")
797+
end
798+
end
763799
end
764800

765801
describe "streaming requests" do
@@ -772,7 +808,7 @@ defmodule Mint.HTTP1Test do
772808
GET / HTTP/1.1
773809
transfer-encoding: chunked
774810
host: localhost:#{port}
775-
user-agent: mint/#{Mix.Project.config()[:version]}
811+
user-agent: #{mint_user_agent()}
776812
777813
\
778814
""")
@@ -795,7 +831,7 @@ defmodule Mint.HTTP1Test do
795831
request_string("""
796832
GET / HTTP/1.1
797833
host: localhost:#{port}
798-
user-agent: mint/#{Mix.Project.config()[:version]}
834+
user-agent: #{mint_user_agent()}
799835
transfer-encoding: chunked
800836
801837
\
@@ -815,7 +851,7 @@ defmodule Mint.HTTP1Test do
815851
request_string("""
816852
GET / HTTP/1.1
817853
host: localhost:#{port}
818-
user-agent: mint/#{Mix.Project.config()[:version]}
854+
user-agent: #{mint_user_agent()}
819855
transfer-encoding: gzip,chunked
820856
821857
\
@@ -839,7 +875,7 @@ defmodule Mint.HTTP1Test do
839875
request_string("""
840876
GET / HTTP/1.1
841877
host: localhost:#{port}
842-
user-agent: mint/#{Mix.Project.config()[:version]}
878+
user-agent: #{mint_user_agent()}
843879
transfer-encoding: identity
844880
845881
\
@@ -859,7 +895,7 @@ defmodule Mint.HTTP1Test do
859895
request_string("""
860896
GET / HTTP/1.1
861897
host: localhost:#{port}
862-
user-agent: mint/#{Mix.Project.config()[:version]}
898+
user-agent: #{mint_user_agent()}
863899
content-length: 5
864900
865901
\
@@ -877,7 +913,7 @@ defmodule Mint.HTTP1Test do
877913
GET / HTTP/1.1
878914
transfer-encoding: chunked
879915
host: localhost:#{port}
880-
user-agent: mint/#{Mix.Project.config()[:version]}
916+
user-agent: #{mint_user_agent()}
881917
882918
\
883919
""")
@@ -897,7 +933,7 @@ defmodule Mint.HTTP1Test do
897933
POST / HTTP/1.1
898934
transfer-encoding: chunked
899935
host: localhost:#{port}
900-
user-agent: mint/#{Mix.Project.config()[:version]}
936+
user-agent: #{mint_user_agent()}
901937
902938
\
903939
""")
@@ -997,4 +1033,7 @@ defmodule Mint.HTTP1Test do
9971033
defp stream_message_bytewise(<<>>, conn, responses) do
9981034
{:ok, conn, responses}
9991035
end
1036+
1037+
@mint_user_agent "mint/#{Mix.Project.config()[:version]}"
1038+
defp mint_user_agent, do: @mint_user_agent
10001039
end

test/mint/http1/request_test.exs

-10
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,6 @@ defmodule Mint.HTTP1.RequestTest do
3333
""")
3434
end
3535

36-
test "validates request target" do
37-
for invalid_target <- ["/ /", "/%foo", "/foo%x"] do
38-
assert Request.encode("GET", invalid_target, [], nil) ==
39-
{:error, {:invalid_request_target, invalid_target}}
40-
end
41-
42-
request = encode_request("GET", "/foo%20bar", [], nil)
43-
assert String.starts_with?(request, request_string("GET /foo%20bar HTTP/1.1"))
44-
end
45-
4636
test "invalid header name" do
4737
assert Request.encode("GET", "/", [{"f oo", "bar"}], nil) ==
4838
{:error, {:invalid_header_name, "f oo"}}

0 commit comments

Comments
 (0)