From 37917e9ef74d65ad179a4f5e8940b706955bab79 Mon Sep 17 00:00:00 2001 From: Jonathan Zarate Date: Thu, 13 Mar 2025 14:15:57 -0600 Subject: [PATCH] Add IP address validation whe is tuple --- lib/ecto_network/inet.ex | 19 +++++++++++++++++-- test/ecto_network_test.exs | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/lib/ecto_network/inet.ex b/lib/ecto_network/inet.ex index 5f87e2c..b1b8118 100644 --- a/lib/ecto_network/inet.ex +++ b/lib/ecto_network/inet.ex @@ -18,8 +18,13 @@ defmodule EctoNetwork.INET do @doc "Handle casting to Postgrex.INET." def cast(%Postgrex.INET{} = address), do: {:ok, address} - def cast(address) when is_tuple(address), - do: cast(%Postgrex.INET{address: address, netmask: address_netmask(address)}) + def cast(address) when is_tuple(address) do + if valid_ip_address?(address) do + cast(%Postgrex.INET{address: address, netmask: address_netmask(address)}) + else + :error + end + end def cast(address) when is_binary(address) do {address, netmask} = @@ -120,6 +125,16 @@ defmodule EctoNetwork.INET do end defp cast_netmask(_mask, _address), do: :error + + defp valid_ip_address?(ip_address) when is_tuple(ip_address) do + case tuple_size(ip_address) do + 4 -> Enum.all?(Tuple.to_list(ip_address), &(is_integer(&1) and &1 >= 0 and &1 < 256)) + 8 -> Enum.all?(Tuple.to_list(ip_address), &(is_integer(&1) and &1 >= 0 and &1 < 65536)) + _ -> false + end + end + + defp valid_ip_address?(_), do: false end defimpl String.Chars, for: Postgrex.INET do diff --git a/test/ecto_network_test.exs b/test/ecto_network_test.exs index e29ebd7..53f6642 100644 --- a/test/ecto_network_test.exs +++ b/test/ecto_network_test.exs @@ -99,6 +99,14 @@ defmodule EctoNetworkTest do assert "#{device.ip_address}" == "127.0.0.1" end + test "rejects an IPv4 tuple with out-of-range values" do + changeset = Device.changeset(%Device{}, %{ip_address: {300, 300, 300, 300}}) + {:error, changeset} = TestRepo.insert(changeset) + + assert changeset.errors[:ip_address] == + {"is invalid", [type: EctoNetwork.INET, validation: :cast]} + end + test "accepts ipv6 address as tuple and saves" do ip_address = {8193, 3512, 0, 0, 0, 65280, 66, 33577} short_ip_address = "2001:db8::ff00:42:8329" @@ -109,6 +117,23 @@ defmodule EctoNetworkTest do assert String.downcase("#{device.ip_address}") == String.downcase(short_ip_address) end + test "rejects an IPv6 tuple with out-of-range values" do + ip_address = {65536, 0, 0, 0, 0, 0, 0, 0} + changeset = Device.changeset(%Device{}, %{ip_address: ip_address}) + {:error, changeset} = TestRepo.insert(changeset) + + assert changeset.errors[:ip_address] == + {"is invalid", [type: EctoNetwork.INET, validation: :cast]} + end + + test "rejects tuples with incorrect size" do + changeset = Device.changeset(%Device{}, %{ip_address: {1, 2, 3}}) + {:error, changeset} = TestRepo.insert(changeset) + + assert changeset.errors[:ip_address] == + {"is invalid", [type: EctoNetwork.INET, validation: :cast]} + end + test "accepts cidr address as binary and saves" do changeset = Device.changeset(%Device{}, %{network: "127.0.0.0/24"}) device = TestRepo.insert!(changeset)