Skip to content

Commit 57cb2cb

Browse files
authored
Merge pull request #7 from danschultzer/ipv6
Add option for IP address type
2 parents b5e563d + 14ca41d commit 57cb2cb

File tree

8 files changed

+100
-20
lines changed

8 files changed

+100
-20
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## v0.1.9 (TBA)
2+
3+
- `:httpd` server adapter now parses remote ip to tuple format
4+
- `:httpd` server adapter now parses host from host header
5+
- Specifying `:host` now also binds the hostname to IPv6 loopback
6+
- Added `:ipfamily` option to set IP address type to use
7+
18
## v0.1.8 (2023-02-10)
29

310
- Support Bandit and httpd web server

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ TestServer.start(http_server: {Bandit, []})
155155

156156
You can create your own plug based HTTP Server Adapter by using the `TestServer.HTTPServer` behaviour.
157157

158+
### IPv6
159+
160+
Use the `:ipfamily` option to test with IPv6 when you are starting the test server:
161+
162+
```elixir
163+
TestServer.start(ipfamily: :inet6)
164+
```
165+
158166
<!-- MDOC !-->
159167

160168
## LICENSE

lib/test_server.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ defmodule TestServer do
2626
* `:port` - integer of port number, defaults to random port that can be opened;
2727
* `:scheme` - an atom for the http scheme. Defaults to `:http`;
2828
* `:http_server` - HTTP server configuration. Defaults to `{TestServer.HTTPServer.Httpd, []}`;
29+
* `:tls` - Passthru options for TLS configuration handled by the webserver;
30+
* `:ipfamily` - The IP address type to use, either `:inet` or `:inet6`. Defaults to `:inet`;
2931
"""
3032
@spec start(keyword()) :: {:ok, pid()}
3133
def start(options \\ []) do
@@ -158,7 +160,7 @@ defmodule TestServer do
158160
Produces a URL for current test server.
159161
160162
## Options
161-
* `:host` - binary host value, it'll be added to inet for IP 127.0.0.1, defaults to `"localhost"`;
163+
* `:host` - binary host value, it'll be added to inet for IP `127.0.0.1` and `::1`, defaults to `"localhost"`;
162164
"""
163165
@spec url(binary(), keyword()) :: binary()
164166
def url(uri, opts) when is_binary(uri), do: url(fetch_instance!(), uri, opts)
@@ -235,6 +237,7 @@ defmodule TestServer do
235237
defp maybe_enable_host(host) do
236238
:inet_db.set_lookup([:file, :dns])
237239
:inet_db.add_host({127, 0, 0, 1}, [String.to_charlist(host)])
240+
:inet_db.add_host({0, 0, 0, 0, 0, 0, 0, 1}, [String.to_charlist(host)])
238241

239242
host
240243
end

lib/test_server/http_server.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ defmodule TestServer.HTTPServer do
2424
@type scheme :: :http | :https
2525
@type instance :: pid()
2626
@type port_number :: :inet.port_number()
27-
@type tls_options :: keyword()
27+
@type options :: [tls: keyword(), ipfamily: :inet | :inet6]
2828
@type server_options :: keyword()
2929

30-
@callback start(instance(), port_number(), scheme(), tls_options(), server_options()) :: {:ok, pid(), server_options()} | {:error, any()}
30+
@callback start(instance(), port_number(), scheme(), options(), server_options()) :: {:ok, pid(), server_options()} | {:error, any()}
3131
@callback stop(instance(), server_options()) :: :ok | {:error, any()}
3232
@callback get_socket_pid(Plug.Conn.t()) :: pid()
3333

@@ -46,9 +46,11 @@ defmodule TestServer.HTTPServer do
4646
port = open_port(options)
4747
scheme = parse_scheme(options)
4848
{tls_options, x509_options} = maybe_generate_x509_suite(options, scheme)
49+
ip_family = Keyword.get(options, :ipfamily, :inet)
50+
test_server_options = [tls: tls_options, ipfamily: ip_family]
4951
{mod, server_options} = Keyword.get(options, :http_server, Application.get_env(:test_server, :http_server, @default_http_server))
5052

51-
case mod.start(instance, port, scheme, tls_options, server_options) do
53+
case mod.start(instance, port, scheme, test_server_options, server_options) do
5254
{:ok, reference, server_options} ->
5355
options =
5456
options

lib/test_server/http_server/bandit.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ defmodule TestServer.HTTPServer.Bandit do
88
@behaviour WebSock
99

1010
@impl TestServer.HTTPServer
11-
def start(instance, port, scheme, tls_options, bandit_options) do
11+
def start(instance, port, scheme, options, bandit_options) do
12+
opts = [options[:ipfamily]] ++ options[:tls]
13+
1214
thousand_islands_options =
1315
bandit_options
1416
|> Keyword.get(:options, [])
1517
|> Keyword.put(:port, port)
16-
|> Keyword.put(:transport_options, tls_options)
18+
|> Keyword.update(:transport_options, opts, & &1 ++ opts)
1719

1820
bandit_options =
1921
bandit_options

lib/test_server/http_server/httpd.ex

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule TestServer.HTTPServer.Httpd do
55
@behaviour TestServer.HTTPServer
66

77
@impl TestServer.HTTPServer
8-
def start(instance, port, scheme, tls_options, httpd_options) do
8+
def start(instance, port, scheme, options, httpd_options) do
99
httpd_options =
1010
httpd_options
1111
|> Keyword.put(:port, port)
@@ -14,7 +14,8 @@ defmodule TestServer.HTTPServer.Httpd do
1414
|> Keyword.put_new(:document_root, '/tmp')
1515
|> Keyword.put_new(:server_root, '/tmp')
1616
|> Keyword.put_new(:handler_plug, {TestServer.Plug, {__MODULE__, [], instance}})
17-
|> put_tls_options(scheme, tls_options)
17+
|> Keyword.put_new(:ipfamily, options[:ipfamily])
18+
|> put_tls_options(scheme, options[:tls])
1819

1920
case :inets.start(:httpd, httpd_options) do
2021
{:ok, pid} -> {:ok, pid, httpd_options}
@@ -74,6 +75,7 @@ defmodule TestServer.HTTPServer.Httpd do
7475
{path, qs} = request_path(data)
7576
{port, host} = host(data)
7677
{_port, remote_ip} = peer(data)
78+
{:ok, remote_ip} = :inet.parse_address(remote_ip)
7779

7880
headers = Enum.map(parsed_header(data), &{to_string(elem(&1, 0)), to_string(elem(&1, 1))})
7981

@@ -84,7 +86,7 @@ defmodule TestServer.HTTPServer.Httpd do
8486
owner: self(),
8587
path_info: split_path(to_string(path)),
8688
port: port,
87-
remote_ip: to_string(remote_ip),
89+
remote_ip: remote_ip,
8890
query_string: qs,
8991
req_headers: headers,
9092
request_path: path,
@@ -106,9 +108,20 @@ defmodule TestServer.HTTPServer.Httpd do
106108
end
107109

108110
defp host(data) do
109-
{:init_data, _, host, _} = httpd(data, :init_data)
111+
data
112+
|> httpd(:parsed_header)
113+
|> Enum.find(&elem(&1, 0) == 'host')
114+
|> Kernel.||({'host', ''})
115+
|> elem(1)
116+
|> to_string()
117+
|> :binary.split(":")
118+
|> case do
119+
[host, port] ->
120+
{Integer.parse(port), host}
110121

111-
host
122+
[host] ->
123+
{nil, host}
124+
end
112125
end
113126

114127
defp peer(data) do

lib/test_server/http_server/plug_cowboy.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ defmodule TestServer.HTTPServer.Plug.Cowboy do
1616
]
1717

1818
@impl TestServer.HTTPServer
19-
def start(instance, port, scheme, tls_options, cowboy_options) do
19+
def start(instance, port, scheme, options, cowboy_options) do
2020
cowboy_options =
2121
cowboy_options
2222
|> Keyword.put_new(:protocol_options, @default_protocol_options)
2323
|> Keyword.put(:port, port)
2424
|> Keyword.put(:dispatch, dispatch(instance))
2525
|> Keyword.put(:ref, cowboy_ref(port))
26-
|> Keyword.merge(tls_options)
26+
|> Keyword.put(:net, options[:ipfamily])
27+
|> Keyword.merge(options[:tls])
2728

2829
case apply(Cowboy, scheme, [TestServer.Plug, {__MODULE__, %{}, instance}, cowboy_options]) do
2930
{:ok, pid} -> {:ok, pid, cowboy_options}

test/test_server_test.exs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ defmodule TestServerTest do
4747

4848
assert %X509.Test.Suite{} = options[:x509_suite]
4949

50-
httpc_opts = fn cacerts ->
50+
http_opts = fn cacerts ->
5151
[
5252
ssl: [
5353
verify: :verify_peer,
@@ -65,10 +65,27 @@ defmodule TestServerTest do
6565
invalid_cacerts = X509.Test.Suite.new().cacerts
6666

6767
assert {:error, {:failed_connect, _}} =
68-
request(TestServer.url("/"), httpc_opts: httpc_opts.(invalid_cacerts))
68+
request(TestServer.url("/"), http_opts: http_opts.(invalid_cacerts))
6969

7070
assert :ok = TestServer.add("/")
71-
assert {:ok, _} = request(TestServer.url("/"), httpc_opts: httpc_opts.(valid_cacerts))
71+
assert {:ok, _} = request(TestServer.url("/"), http_opts: http_opts.(valid_cacerts))
72+
end
73+
74+
test "starts in IPv6-only mode`" do
75+
{:ok, instance} = TestServer.start(ipfamily: :inet6)
76+
options = TestServer.Instance.get_options(instance)
77+
78+
assert options[:ipfamily] == :inet6
79+
80+
assert :ok = TestServer.add("/", to: fn conn ->
81+
assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1}
82+
83+
Plug.Conn.send_resp(conn, 200, "OK")
84+
end)
85+
86+
assert %{host: hostname} = URI.parse(TestServer.url("/"))
87+
assert {:ok, {0, 0, 0, 0, 0, 0, 0, 1}} == :inet.getaddr(String.to_charlist(hostname), :inet6)
88+
assert {:ok, _} = request(TestServer.url("/"))
7289
end
7390
end
7491

@@ -148,7 +165,7 @@ defmodule TestServerTest do
148165
end
149166
end
150167

151-
test "invalid `:host`" do
168+
test "with invalid `:host`" do
152169
TestServer.start()
153170

154171
assert_raise RuntimeError, ~r/Invalid host, got: :invalid/, fn ->
@@ -164,6 +181,32 @@ defmodule TestServerTest do
164181
refute TestServer.url("/") == TestServer.url("/", host: "bad-host")
165182
end
166183

184+
test "with `:host`" do
185+
TestServer.start()
186+
187+
assert :ok = TestServer.add("/", to: fn conn ->
188+
assert conn.remote_ip == {127, 0, 0, 1}
189+
assert conn.host == "custom-host"
190+
191+
Plug.Conn.send_resp(conn, 200, "OK")
192+
end)
193+
194+
assert {:ok, _} = request(TestServer.url("/", host: "custom-host"))
195+
end
196+
197+
test "with `:host` in IPv6-only mode" do
198+
TestServer.start(ipfamily: :inet6, http_server: {TestServer.HTTPServer.Httpd, []})
199+
200+
assert :ok = TestServer.add("/", to: fn conn ->
201+
assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1}
202+
assert conn.host == "custom-host"
203+
204+
Plug.Conn.send_resp(conn, 200, "OK")
205+
end)
206+
207+
assert {:ok, _} = request(TestServer.url("/", host: "custom-host"))
208+
end
209+
167210
test "with multiple instances" do
168211
{:ok, instance_1} = TestServer.start()
169212
{:ok, instance_2} = TestServer.start()
@@ -674,13 +717,14 @@ defmodule TestServerTest do
674717

675718
def request(url, opts \\ []) do
676719
url = String.to_charlist(url)
677-
httpc_opts = Keyword.get(opts, :httpc_opts, [])
720+
httpc_http_opts = Keyword.get(opts, :http_opts, [])
721+
httpc_opts = Keyword.get(opts, :opts, [])
678722

679723
opts
680724
|> Keyword.get(:method, :get)
681725
|> case do
682-
:post -> :httpc.request(:post, {url, [], 'plain/text', 'OK'}, httpc_opts, [])
683-
:get -> :httpc.request(:get, {url, []}, httpc_opts, [])
726+
:post -> :httpc.request(:post, {url, [], 'plain/text', 'OK'}, httpc_http_opts, httpc_opts)
727+
:get -> :httpc.request(:get, {url, []}, httpc_http_opts, httpc_opts)
684728
end
685729
|> case do
686730
{:ok, {{_, 200, _}, _headers, body}} -> {:ok, to_string(body)}

0 commit comments

Comments
 (0)