lib/ip/address.ex

defmodule IP.Address do
  alias IP.Address
  alias IP.Address.{Helpers, InvalidAddress, ULA}
  alias IP.Prefix
  defstruct ~w(address version)a
  import Helpers
  use Bitwise

  @moduledoc """
  Simple representations of IP Addresses.
  """

  @typedoc "Valid IPv4 address - integer between zero and 32 ones."
  @type ipv4 :: 0..0xFFFFFFFF

  @typedoc "Valid IPv6 address - integer between zero and 128 ones."
  @type ipv6 :: 0..0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

  @typedoc "Valid IP address"
  @type ip :: ipv4 | ipv6

  @typedoc "Valid IP version (currently only 4 and 6 are deployed in the wild)."
  @type version :: 4 | 6

  @typedoc "IP address struct type, contains a valid address and version."
  @type t :: %Address{address: ip, version: version}

  @doc """
  Convert from (packed) binary representations (either 32 or 128 bits long) into an address.

  ## Examples

      iex> <<192, 0, 2, 1>>
      ...> |> IP.Address.from_binary()
      {:ok, %IP.Address{address: 3221225985, version: 4}}

      iex> <<32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
      ...> |> IP.Address.from_binary()
      {:ok, %IP.Address{address: 42540766411282592856903984951653826560, version: 6}}

      iex> "192.0.2.1"
      ...> |> IP.Address.from_binary()
      {:error, "Unable to convert binary to address"}
  """
  @spec from_binary(binary) :: {:ok, t} | {:error, term}
  def from_binary(<<address::unsigned-integer-size(32)>>),
    do: {:ok, %Address{address: address, version: 4}}

  def from_binary(<<address::unsigned-integer-size(128)>>),
    do: {:ok, %Address{address: address, version: 6}}

  def from_binary(_address), do: {:error, "Unable to convert binary to address"}

  @doc """
  Convert from a packed binary presentation to an address or raise an
  `IP.Address.InvalidAddress` exception.

  ## Examples

      iex> <<192, 0, 2, 1>>
      ...> |> IP.Address.from_binary!()
      %IP.Address{address: 3221225985, version: 4}

      iex> <<32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>
      ...> |> IP.Address.from_binary!()
      %IP.Address{address: 42540766411282592856903984951653826561, version: 6}
  """
  @spec from_binary!(binary) :: t
  def from_binary!(address) do
    case from_binary(address) do
      {:ok, address} -> address
      {:error, msg} -> raise(InvalidAddress, message: msg)
    end
  end

  @doc """
  Convert an integer into an IP address of specified version.

  ## Examples

      iex> 3221225985
      ...> |> IP.Address.from_integer(4)
      {:ok, %IP.Address{address: 3221225985, version: 4}}

      iex> 42540766411282592856903984951653826561
      ...> |> IP.Address.from_integer(6)
      {:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
  """
  @spec from_integer(ip, version) :: {:ok, t} | {:error, term}
  def from_integer(address, 4) when valid_ipv4_integer?(address) do
    {:ok, %Address{address: address, version: 4}}
  end

  def from_integer(address, 6) when valid_ipv6_integer?(address) do
    {:ok, %Address{address: address, version: 6}}
  end

  def from_integer(_address, 4), do: {:error, "Supplied address not within IPv4 address space"}
  def from_integer(_address, 6), do: {:error, "Supplied address not within IPv6 address space"}
  def from_integer(_address, version), do: {:error, "No such IP version #{inspect(version)}"}

  @doc """
  Convert an integer into an IP address of specified version or raise an
  `IP.Address.InvalidAddress` exception.

  ## Examples

      iex> 3221225985
      ...> |> IP.Address.from_integer!(4)
      %IP.Address{address: 3221225985, version: 4}

      iex> 42540766411282592856903984951653826561
      ...> |> IP.Address.from_integer!(6)
      %IP.Address{address: 42540766411282592856903984951653826561, version: 6}
  """
  @spec from_integer!(ip, version) :: t
  def from_integer!(address, version) do
    case from_integer(address, version) do
      {:ok, address} -> address
      {:error, msg} -> raise(InvalidAddress, message: msg)
    end
  end

  @doc """
  Convert a string representation into an IP address of unknown version.

  Tries to parse the string as IPv6, then IPv4 before failing.  Obviously if
  you know the version then using `from_string/2` is faster.

  ## Examples

      iex> "192.0.2.1"
      ...> |> IP.Address.from_string()
      {:ok, %IP.Address{address: 3221225985, version: 4}}

      iex> "2001:db8::1"
      ...> |> IP.Address.from_string()
      {:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
  """
  @spec from_string(binary) :: {:ok, t} | {:error, term}
  def from_string(address) when is_binary(address) do
    case from_string(address, 6) do
      {:ok, address} ->
        {:ok, address}

      {:error, _} ->
        case from_string(address, 4) do
          {:ok, address} -> {:ok, address}
          {:error, _} -> {:error, "Unable to parse IP address"}
        end
    end
  end

  @doc """
  Convert a string representation into an IP address or raise an
  `IP.Address.InvalidAddress` exception.

  ## Examples

      iex> "192.0.2.1"
      ...> |> IP.Address.from_string!()
      %IP.Address{address: 3221225985, version: 4}

      iex> "2001:db8::1"
      ...> |> IP.Address.from_string!()
      %IP.Address{address: 42540766411282592856903984951653826561, version: 6}
  """
  @spec from_string!(binary) :: t
  def from_string!(address) when is_binary(address) do
    case from_string(address) do
      {:ok, addr} -> addr
      {:error, msg} -> raise(InvalidAddress, message: msg)
    end
  end

  @doc """
  Convert a string representation into an IP address of specified version.

  ## Examples

      iex> "192.0.2.1"
      ...> |> IP.Address.from_string(4)
      {:ok, %IP.Address{address: 3221225985, version: 4}}

      iex> "2001:db8::1"
      ...> |> IP.Address.from_string(6)
      {:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
  """
  @spec from_string(binary, version) :: {:ok, t} | {:error, term}
  def from_string(address, 4) when is_binary(address) do
    case :inet.parse_ipv4strict_address(String.to_charlist(address)) do
      {:ok, addr} ->
        addr =
          addr
          |> Tuple.to_list()
          |> from_bytes()

        {:ok, %Address{version: 4, address: addr}}

      {:error, _} ->
        {:error, "Cannot parse IPv4 address"}
    end
  end

  def from_string(address, 6) when is_binary(address) do
    case :inet.parse_ipv6strict_address(String.to_charlist(address)) do
      {:ok, addr} ->
        addr =
          addr
          |> Tuple.to_list()
          |> from_bytes()

        {:ok, %Address{version: 6, address: addr}}

      {:error, _} ->
        {:error, "Cannot parse IPv6 address"}
    end
  end

  def from_string(_address, 4), do: {:error, "Cannot parse IPv4 address"}
  def from_string(_address, 6), do: {:error, "Cannot parse IPv6 address"}
  def from_string(_address, version), do: {:error, "No such IP version #{inspect(version)}"}

  @doc """
  Convert a string representation into an IP address of specified version or raise an
  `IP.Address.InvalidAddress` exception.

  ## Examples

      iex> "192.0.2.1"
      ...> |> IP.Address.from_string!(4)
      %IP.Address{address: 3221225985, version: 4}

      iex> "2001:db8::1"
      ...> |> IP.Address.from_string!(6)
      %IP.Address{address: 42540766411282592856903984951653826561, version: 6}
  """
  @spec from_string!(binary, version) :: t
  def from_string!(address, version) do
    case from_string(address, version) do
      {:ok, address} -> address
      {:error, msg} -> raise(InvalidAddress, msg)
    end
  end

  @doc """
  Convert an Erlang-style tuple of bytes to an address.

  ## Examples

      iex> {192, 0, 2, 1}
      ...> |> IP.Address.from_tuple()
      {:ok, %IP.Address{address: 3221225985, version: 4}}

      iex> {8193, 3512, 0, 0, 0, 0, 0, 1}
      ...> |> IP.Address.from_tuple()
      {:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
  """
  @spec from_tuple(:socket.in_addr() | :socket.in6_addr()) :: {:ok, t} | {:error, term}
  def from_tuple({a, b, c, d})
      when valid_byte?(a) and valid_byte?(b) and valid_byte?(c) and valid_byte?(d),
      do: {:ok, %Address{version: 4, address: from_bytes([a, b, c, d])}}

  def from_tuple({a, b, c, d, e, f, g, h})
      when valid_quartet?(a) and valid_quartet?(b) and valid_quartet?(c) and valid_quartet?(d) and
             valid_quartet?(e) and valid_quartet?(f) and valid_quartet?(g) and valid_quartet?(h),
      do: {:ok, %Address{version: 6, address: from_bytes([a, b, c, d, e, f, g, h])}}

  def from_tuple(_), do: {:error, "Invalid address"}

  @doc """
  Convert an Erlang-style tuple of bytes to an address.

  ## Examples

      iex> {192, 0, 2, 1}
      ...> |> IP.Address.from_tuple!()
      %IP.Address{address: 3221225985, version: 4}

      iex> {8193, 3512, 0, 0, 0, 0, 0, 1}
      ...> |> IP.Address.from_tuple!()
      %IP.Address{address: 42540766411282592856903984951653826561, version: 6}
  """
  @spec from_tuple!(:socket.in_addr() | :socket.in6_addr()) :: t | no_return
  def from_tuple!(tuple) do
    case from_tuple(tuple) do
      {:ok, address} -> address
      {:error, msg} -> raise(InvalidAddress, msg)
    end
  end

  @doc """
  Convert an `address` into a string.

  ## Examples

      iex> ~i(192.0.2.1)
      ...> |> IP.Address.to_string()
      "192.0.2.1"

      iex> ~i(2001:db8::1)
      ...> |> IP.Address.to_string()
      "2001:db8::1"
  """
  @spec to_string(t) :: binary
  def to_string(%Address{version: 4, address: addr}) do
    a = addr >>> 0x18 &&& 0xFF
    b = addr >>> 0x10 &&& 0xFF
    c = addr >>> 0x08 &&& 0xFF
    d = addr &&& 0xFF

    {a, b, c, d}
    |> :inet.ntoa()
    |> List.to_string()
  end

  def to_string(%Address{version: 6, address: addr}) do
    a = addr >>> 0x70 &&& 0xFFFF
    b = addr >>> 0x60 &&& 0xFFFF
    c = addr >>> 0x50 &&& 0xFFFF
    d = addr >>> 0x40 &&& 0xFFFF
    e = addr >>> 0x30 &&& 0xFFFF
    f = addr >>> 0x20 &&& 0xFFFF
    g = addr >>> 0x10 &&& 0xFFFF
    h = addr &&& 0xFFFF

    {a, b, c, d, e, f, g, h}
    |> :inet.ntoa()
    |> List.to_string()
  end

  @doc """
  Convert an `address` to an `IP.Prefix`.

  ## Examples

      iex> ~i(192.0.2.1)
      ...> |> IP.Address.to_prefix(32)
      #IP.Prefix<192.0.2.1/32 DOCUMENTATION>
  """
  @spec to_prefix(t, Prefix.prefix_length()) :: Prefix.t()
  def to_prefix(%Address{} = address, length), do: Prefix.new(address, length)

  @doc """
  Returns the IP version of the address.

  ## Examples

      iex> ~i(192.0.2.1)
      ...> |> IP.Address.version()
      4

      iex> ~i(2001:db8::1)
      ...> |> IP.Address.version()
      6
  """
  @spec version(t) :: version
  def version(%Address{version: version}), do: version

  @doc """
  Returns the IP Address as an integer

  ## Examples

      iex> ~i(192.0.2.1)
      ...> |> IP.Address.to_integer()
      3221225985

      iex> ~i(2001:db8::1)
      ...> |> IP.Address.to_integer()
      42540766411282592856903984951653826561
  """
  @spec to_integer(t) :: ip
  def to_integer(%Address{address: address}), do: address

  @doc """
  Returns true if `address` is version 6.

  ## Examples

      iex> ~i(192.0.2.1)
      ...> |> IP.Address.v6?
      false

      iex> ~i(2001:db8::)
      ...> |> IP.Address.v6?
      true
  """
  @spec v6?(t) :: boolean
  def v6?(%Address{version: 6} = _address), do: true
  def v6?(_address), do: false

  @doc """
  Returns true if `address` is version 4.

  ## Examples

      iex> ~i(192.0.2.1)
      ...> |> IP.Address.v4?
      true

      iex> ~i(2001:db8::)
      ...> |> IP.Address.v4?
      false
  """
  @spec v4?(t) :: boolean
  def v4?(%Address{version: 4} = _address), do: true
  def v4?(_address), do: false

  @doc """
  Returns true if the address is an EUI-64 address.

  ## Examples

      iex> ~i(2001:db8::62f8:1dff:fead:d890)
      ...> |> IP.Address.eui_64?()
      true
  """
  @spec eui_64?(t) :: boolean
  def eui_64?(%Address{address: address, version: 6})
      when (address &&& 0x20000FFFE000000) == 0x20000FFFE000000,
      do: true

  def eui_64?(_address), do: false

  @doc """
  Return a MAC address coded in an EUI-64 address.

  ## Examples

      iex> ~i(2001:db8::62f8:1dff:fead:d890)
      ...> |> IP.Address.eui_64_mac()
      {:ok, "60f8.1dad.d890"}
  """
  @spec eui_64_mac(t) :: {:ok, binary} | {:error, term}
  def eui_64_mac(%Address{address: address, version: 6})
      when (address &&& 0x20000FFFE000000) == 0x20000FFFE000000 do
    mac = address &&& 0xFFFFFFFFFFFFFFFF
    head = mac >>> 40
    tail = mac &&& 0xFFFFFF
    mac = Bitwise.bxor((head <<< 24) + tail, 0x20000000000)

    <<a::binary-size(4), b::binary-size(4), c::binary-size(4)>> =
      mac
      |> Integer.to_string(16)
      |> String.downcase()
      |> String.pad_leading(12, "0")

    {:ok, "#{a}.#{b}.#{c}"}
  end

  def eui_64_mac(_address), do: {:error, "Not an EUI-64 address"}

  @doc """
  Convert an IPv4 address into a 6to4 address.

  ## Examples

      iex> ~i(192.0.2.1)
      ...> |> IP.Address.to_6to4()
      #IP.Address<2002:c000:201:: GLOBAL UNICAST (6to4)>
  """
  @spec to_6to4(t) :: {:ok, t} | {:error, term}
  def to_6to4(%Address{address: address, version: 4}) do
    address = (0x2002 <<< 112) + (address <<< 80)
    %Address{address: address, version: 6}
  end

  def to_6to4(_address), do: {:error, "Not an IPv4 address"}

  @doc """
  Determine if the IP address is a 6to4 address.

  ## Examples

      iex> ~i(2002:c000:201::)
      ...> |> IP.Address.is_6to4?()
      true

      iex> ~i(2001:db8::)
      ...> |> IP.Address.is_6to4?()
      false
  """
  @spec is_6to4?(t) :: boolean
  def is_6to4?(%Address{address: address, version: 6})
      when address >>> 112 == 0x2002,
      do: true

  def is_6to4?(_address), do: false

  @doc """
  Convert a 6to4 IPv6 address to it's correlated IPv6 address.

  ## Examples

      iex> ~i(2002:c000:201::)
      ...> |> IP.Address.from_6to4()
      ...> |> inspect()
      "{:ok, #IP.Address<192.0.2.1 DOCUMENTATION>}"

      iex> ~i(2001:db8::)
      ...> |> IP.Address.from_6to4()
      {:error, "Not a 6to4 address"}
  """
  @spec from_6to4(t) :: {:ok, t} | {:error, term}
  def from_6to4(%Address{address: address, version: 6})
      when address >>> 112 == 0x2002 do
    address = address >>> 80 &&& 0xFFFFFFFF
    Address.from_integer(address, 4)
  end

  def from_6to4(_address), do: {:error, "Not a 6to4 address"}

  @doc """
  Determine if an IP address is a teredo connection.

  ## Examples

      iex> ~i(2001::)
      ...> |> IP.Address.is_teredo?()
      true
  """
  @spec is_teredo?(t) :: boolean
  def is_teredo?(%Address{address: address, version: 6})
      when address >>> 96 == 0x20010000,
      do: true

  def is_teredo?(_address), do: false

  @doc """
  Return information about a teredo connection.

  ## Examples

      iex> ~i(2001:0:4136:e378:8000:63bf:3fff:fdd2)
      ...> |> IP.Address.teredo()
      ...> |> Map.get(:server)
      #IP.Address<65.54.227.120 GLOBAL UNICAST>

      iex> ~i(2001:0:4136:e378:8000:63bf:3fff:fdd2)
      ...> |> IP.Address.teredo()
      ...> |> Map.get(:client)
      #IP.Address<63.255.253.210 GLOBAL UNICAST>

      iex> ~i(2001:0:4136:e378:8000:63bf:3fff:fdd2)
      ...> |> IP.Address.teredo()
      ...> |> Map.get(:port)
      25535
  """
  @spec teredo(t) :: {:ok, map} | {:error, term}
  def teredo(%Address{address: address, version: 6})
      when address >>> 96 == 0x20010000 do
    server = address >>> 64 &&& (1 <<< 32) - 1
    client = address &&& (1 <<< 32) - 1 &&& (1 <<< 32) - 1
    port = address >>> 32 &&& (1 <<< 16) - 1

    %{
      server: Address.from_integer!(server, 4),
      client: Address.from_integer!(client, 4),
      port: port
    }
  end

  def teredo(_address), do: {:error, "Not a teredo address"}

  @doc """
  Generate an IPv6 Unique Local Address

  Note that the MAC address is just used as a source of randomness, so where you
  get it from is not important and doesn't restrict this ULA to just that system.
  See RFC4193

  ## Examples

      iex> IP.Address.generate_ula("60:f8:1d:ad:d8:90")
      #IP.Address<fd29:f1ef:86a1::>
  """
  @spec generate_ula(binary, non_neg_integer, boolean) :: {:ok, t} | {:error, term}
  def generate_ula(mac, subnet_id \\ 0, locally_assigned \\ true) do
    case ULA.generate(mac, subnet_id, locally_assigned) do
      {:ok, address} -> from_integer(address, 6)
      {:error, reason} -> {:error, reason}
    end
  end

  defp from_bytes([a, b, c, d]) do
    (a <<< 24) + (b <<< 16) + (c <<< 8) + d
  end

  defp from_bytes([a, b, c, d, e, f, g, h]) do
    (a <<< 0x70) + (b <<< 0x60) + (c <<< 0x50) + (d <<< 0x40) + (e <<< 0x30) + (f <<< 0x20) +
      (g <<< 0x10) + h
  end
end