defmodule Liaison.IP do @moduledoc """ Allow the conversion of common IP formats to and from * string: "1.2.3.4" * component: {1, 2, 3, 4} * integer: 16909060 Input formats can be equal to output formats ## Example iex> Liaison.IP.as_string("1.2.3.4") "1.2.3.4" """ use Bitwise @type ip :: integer @type ip_comp :: {integer, integer, integer, integer} @type ip_addr :: ip | ip_comp | String.t() @type format :: :integer | :components | :string @doc """ Converts any valid address into a string `a.b.c.d` ## Example iex> Liaison.IP.as_string("1.2.3.4") "1.2.3.4" iex> Liaison.IP.as_string(16909060) "1.2.3.4" iex> Liaison.IP.as_string({1, 2, 3, 4}) "1.2.3.4" """ @spec as_string(ip_addr) :: String.t() def as_string(ip_addr) when is_binary(ip_addr), do: ip_addr def as_string(ip_addr) when is_integer(ip_addr) do ip_addr |> as_components() |> as_string() end def as_string(ip_addr) when is_tuple(ip_addr) do ip_addr |> Tuple.to_list() |> Enum.join(".") end @doc """ Converts any valid address into a tuple `{a, b, c, d}` ## Example iex> Liaison.IP.as_components("1.2.3.4") {1, 2, 3, 4} iex> Liaison.IP.as_components(16909060) {1, 2, 3, 4} iex> Liaison.IP.as_components({1, 2, 3, 4}) {1, 2, 3, 4} """ @spec as_components(ip_addr) :: ip_comp def as_components(ip_addr) when is_tuple(ip_addr), do: ip_addr def as_components(ip_addr) when is_integer(ip_addr) do a = (ip_addr &&& 0xFF000000) >>> 24 b = (ip_addr &&& 0x00FF0000) >>> 16 c = (ip_addr &&& 0x0000FF00) >>> 8 d = ip_addr &&& 0x000000FF {a, b, c, d} end def as_components(ip_addr) when is_binary(ip_addr) do ip_addr |> String.split(".") |> Enum.map(&String.to_integer/1) |> List.to_tuple() end @doc """ Converts any valid address into an integer ## Example iex> Liaison.IP.as_integer("1.2.3.4") 16909060 iex> Liaison.IP.as_integer(16909060) 16909060 iex> Liaison.IP.as_integer({1, 2, 3, 4}) 16909060 """ @spec as_integer(ip_addr) :: ip def as_integer(ip_addr) when is_integer(ip_addr), do: ip_addr def as_integer({a, b, c, d}), do: (a <<< 24) + (b <<< 16) + (c <<< 8) + d def as_integer(ip_addr) when is_binary(ip_addr) do as_integer(as_components(ip_addr)) end @doc """ Creates a range of integer ip addresses between the given addresses. Inclusivity: [from, to] ## Example iex> Liaison.IP.to_ip_range("1.2.3.4", "1.2.3.6", :string) ["1.2.3.4", "1.2.3.5", "1.2.3.6"] iex> Liaison.IP.to_ip_range("1.2.3.6", "1.2.3.4") {:error, "ip1 > ip2"} """ @spec to_ip_range(ip_addr, ip_addr, format) :: list(ip) | {:error, String.t()} def to_ip_range(from, to, format \\ :integer) do ip1_int = as_integer(from) ip2_int = as_integer(to) case ip1_int > ip2_int do true -> {:error, "ip1 > ip2"} _ -> range = to_range(ip1_int, ip2_int) case format do :integer -> Enum.map(range, &as_integer/1) :components -> Enum.map(range, &as_components/1) :string -> Enum.map(range, &as_string/1) end end end defp to_range(ip1, ip2) do case ip1 >= ip2 do true -> [ip2] false -> [ip1 | to_range(ip1 + 1, ip2)] end end @doc """ Returns the localhost ip address in component form. If there is more than one ip address for the machine, the nth can be returned. Else `nil` """ @spec get_ipv4_addr(integer) :: ip_comp | nil def get_ipv4_addr(pos \\ 0) do {:ok, host} = :inet.gethostname() {:ok, {:hostent, _name, _aliases, _type, _length, ips}} = :inet.gethostbyname(host) Enum.at(ips, pos) end @doc """ Returns `true` if the given address is valid. This checks if the format is correct, not if the address is functionally valid or accessible ## Example iex> Liaison.IP.is_valid?("1.2.3.4") true iex> Liaison.IP.is_valid?({1, 2, 3, 400}) false """ @spec is_valid?(any) :: boolean def is_valid?(ip_addr) do lowest_valid = as_integer("1.0.0.0") highest_valid = as_integer("255.255.255.255") try do ip_int = as_integer(ip_addr) {a, b, c, d} = as_components(ip_addr) ip_int < highest_valid && ip_int > lowest_valid && between?(a, 0, 255) && between?(b, 0, 255) && between?(c, 0, 255) && between?(d, 0, 255) rescue _ -> false end end defp between?(a, b, c) do a >= b && a <= c end end