From 054703e13011af417fbade52adae2c5f8454333d Mon Sep 17 00:00:00 2001 From: Timmo Verlaan Date: Wed, 25 Oct 2023 13:07:43 +0200 Subject: [PATCH] Allow IP address to be given per instance This is an improvement of the current global listen_ip env config. This way one can use the full 127/8 localhost range for testing. Also see https://datatracker.ietf.org/doc/html/rfc6890#section-2.2.2 --- lib/bypass.ex | 6 ++++-- lib/bypass/instance.ex | 22 +++++++++++++++------- test/bypass_test.exs | 30 ++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/lib/bypass.ex b/lib/bypass.ex index 4d7ae77..8adcdc7 100644 --- a/lib/bypass.ex +++ b/lib/bypass.ex @@ -5,7 +5,7 @@ defmodule Bypass do |> String.split("") |> Enum.fetch!(1) - defstruct pid: nil, port: nil + defstruct pid: nil, port: nil, ip: nil @typedoc """ Represents a Bypass server process. @@ -25,6 +25,7 @@ defmodule Bypass do ## Options - `port` - Optional TCP port to listen to requests. + - `ip` - Optional TCP IP to listen to requests. Default is `{127, 0, 0, 1}` ## Examples @@ -43,8 +44,9 @@ defmodule Bypass do def open(opts \\ []) do pid = start_instance(opts) port = Bypass.Instance.call(pid, :port) + ip = Bypass.Instance.call(pid, :ip) debug_log("Did open connection #{inspect(pid)} on port #{inspect(port)}") - bypass = %Bypass{pid: pid, port: port} + bypass = %Bypass{pid: pid, port: port, ip: ip} setup_framework_integration(test_framework(), bypass) bypass end diff --git a/lib/bypass/instance.ex b/lib/bypass/instance.ex index 9426c00..12a41b3 100644 --- a/lib/bypass/instance.ex +++ b/lib/bypass/instance.ex @@ -25,16 +25,20 @@ defmodule Bypass.Instance do def init([opts]) do # Get a free port from the OS - case :ranch_tcp.listen(so_reuseport() ++ [ip: listen_ip(), port: Keyword.get(opts, :port, 0)]) do + case :ranch_tcp.listen( + so_reuseport() ++ + [ip: Keyword.get(opts, :ip, listen_ip()), port: Keyword.get(opts, :port, 0)] + ) do {:ok, socket} -> - {:ok, port} = :inet.port(socket) + {:ok, {ip, port}} = :inet.sockname(socket) :erlang.port_close(socket) ref = make_ref() - socket = do_up(port, ref) + socket = do_up(ip, port, ref) state = %{ expectations: %{}, + ip: ip, port: port, ref: ref, socket: socket, @@ -76,8 +80,12 @@ defmodule Bypass.Instance do {:reply, port, state} end - defp do_handle_call(:up, _from, %{port: port, ref: ref, socket: nil} = state) do - socket = do_up(port, ref) + defp do_handle_call(:ip, _, %{ip: ip} = state) do + {:reply, ip, state} + end + + defp do_handle_call(:up, _from, %{ip: ip, port: port, ref: ref, socket: nil} = state) do + socket = do_up(ip, port, ref) {:reply, :ok, %{state | socket: socket}} end @@ -317,9 +325,9 @@ defmodule Bypass.Instance do defp match_route(_, _), do: {false, nil} - defp do_up(port, ref) do + defp do_up(ip, port, ref) do plug_opts = [bypass_instance: self()] - {:ok, socket} = :ranch_tcp.listen(so_reuseport() ++ [ip: listen_ip(), port: port]) + {:ok, socket} = :ranch_tcp.listen(so_reuseport() ++ [ip: ip, port: port]) cowboy_opts = cowboy_opts(port, ref, socket) {:ok, _pid} = Plug.Cowboy.http(Bypass.Plug, plug_opts, cowboy_opts) socket diff --git a/test/bypass_test.exs b/test/bypass_test.exs index 6f41000..f2009a9 100644 --- a/test/bypass_test.exs +++ b/test/bypass_test.exs @@ -39,6 +39,32 @@ defmodule BypassTest do assert(is_map(bypass2) and bypass2.__struct__ == Bypass) end + test "Bypass.open can specify an ip to operate on with expect" do + specify_ip({127, 0, 0, 2}, :expect) + end + + test "Bypass.open can specify an ip to operate on with expect_once" do + specify_ip({127, 1, 2, 3}, :expect_once) + end + + defp specify_ip(ip, expect_fun) do + port = 9876 + bypass = Bypass.open(ip: ip, port: port) + address = ip |> :inet.ntoa() |> to_string() + + apply(Bypass, expect_fun, [ + bypass, + fn conn -> + assert address == conn.host + Plug.Conn.send_resp(conn, 200, "") + end + ]) + + assert {:ok, 200, ""} = request(port, "/", "GET", address) + bypass2 = Bypass.open(ip: ip, port: port) + assert(is_map(bypass2) and bypass2.__struct__ == Bypass) + end + test "Bypass.down takes down the socket with expect" do :expect |> down_socket end @@ -430,8 +456,8 @@ defmodule BypassTest do "high-level" HTTP client, since they do connection pooling and we will sometimes get a connection closed error and not a failed to connect error, when we test Bypass.down. """ - def request(port, path \\ "/example_path", method \\ "POST") do - with {:ok, conn} <- Mint.HTTP.connect(:http, "127.0.0.1", port, mode: :passive), + def request(port, path \\ "/example_path", method \\ "POST", ip \\ "127.0.0.1") do + with {:ok, conn} <- Mint.HTTP.connect(:http, ip, port, mode: :passive), {:ok, conn, ref} <- Mint.HTTP.request(conn, method, path, [], "") do receive_responses(conn, ref, 100, []) end