Current section
Files
Jump to
Current section
Files
lib/ip.ex
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