8000 feat(ip_range): add support for iprange type · Pull Request #1 · mneudert/ecto_ip_range · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(ip_range): add support for iprange type #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions lib/ecto_ip_range/ip_range.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
defmodule EctoIPRange.IPRange do
@moduledoc """
Struct for PostgreSQL `:iprange`.

## Usage

When used during a changeset cast the following values are accepted:

- `:inet.ip4_address()`: an IP4 tuple, e.g. `{127, 0, 0, 1}` (single address only)
- `binary`
- `"127.0.0.1"`: single address
- `"127.0.0.0/24"`: CIDR notation for a range from `127.0.0.0` to `127.0.0.255`
- `"127.0.0.1-127.0.0.2"`: arbitrary range
- `"2001:0db8:85a3:0000:0000:8a2e:0370:7334"`: single address
- `"2001:0db8:85a3:0000:0000:8a2e:0370:0000/112"`: CIDR notation for a range from
`2001:0db8:85a3:0000:0000:8a2e:0370:0000` to `2001:0db8:85a3:0000:0000:8a2e:0370:ffff`
- `"2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335"`: arbitrary range
- `EctoIPRange.IPRange.t()`: a pre-casted struct

## Fields

* `range`
* `first_ip`
* `last_ip`
"""

use Ecto.Type

alias EctoIPRange.Util.{CIDR, Inet}

@type t :: %__MODULE__{
range: binary(),
first_ip: :inet.ip4_address() | :inet.ip6_address(),
last_ip: :inet.ip4_address() | :inet.ip6_address()
}

defstruct [:range, :first_ip, :last_ip]

@impl Ecto.Type
def type, do: :iprange

@impl Ecto.Type
def cast({_, _, _, _} = ip4_address) do
case Inet.ntoa(ip4_address) do
address when is_binary(address) ->
{:ok,
%__MODULE__{
range: address <> "/32",
first_ip: ip4_address,
last_ip: ip4_address
}}

_ ->
:error
end
end

def cast({_, _, _, _, _, _, _, _} = ip6_address) do
case Inet.ntoa(ip6_address) do
address when is_binary(address) ->
{:ok,
%__MODULE__{
range: String.upcase(address) <> "/128",
first_ip: ip6_address 10000 ,
last_ip: ip6_address
}}

_ ->
:error
end
end

def cast(address) when is_binary(address) do
cond do
String.contains?(address, "-") -> cast_range(address)
String.contains?(address, "/") -> cast_cidr(address)
true -> cast_binary(address)
end
end

def cast(%__MODULE__{} = address), do: {:ok, address}
def cast(_), do: :error

@impl Ecto.Type
def load(%__MODULE__{} = address), do: {:ok, address}
def load(_), do: :error

@impl Ecto.Type
def dump(%__MODULE__{} = address), do: {:ok, address}
def dump(_), do: :error

defp cast_binary(address) do
case Inet.parse_binary(address) do
{:ok, {_, _, _, _} = ip4_address} ->
{:ok,
%__MODULE__{
range: address <> "/32",
first_ip: ip4_address,
last_ip: ip4_address
}}

{:ok, {_, _, _, _, _, _, _, _} = ip6_address} ->
{:ok,
%__MODULE__{
range: String.upcase(address) <> "/128",
first_ip: ip6_address,
last_ip: ip6_address
}}

_ ->
:error
end
end

defp cast_cidr(cidr) do
with [address, maskstring] <- String.split(cidr, "/", parts: 2),
{first_ip_address, last_ip_address} <- cast_ip4_or_ip6_cidr(address, maskstring) do
{:ok,
%__MODULE__{
range: cidr,
first_ip: first_ip_address,
last_ip: last_ip_address
}}
end
end

defp cast_range(range) do
with [first_ip, last_ip] <- String.split(range, "-", parts: 2),
{:ok, first_ip_address} <- Inet.parse_binary(first_ip),
{:ok, last_ip_address} <- Inet.parse_binary(last_ip) do
{:ok,
%__MODULE__{
range: range,
first_ip: first_ip_address,
last_ip: last_ip_address
}}
else
_ -> :error
end
end

defp cast_ip4_or_ip6_cidr(address, maskstring) do
case Inet.parse_binary(address) do
{:ok, {_, _, _, _}} -> cast_ip4_cidr(address, maskstring)
{:ok, {_, _, _, _, _, _, _, _}} -> cast_ip6_cidr(address, maskstring)
_ -> :error
end
end

defp cast_ip4_cidr(address, maskstring) do
case Integer.parse(maskstring) do
{maskbits, ""} when maskbits in 0..32 -> CIDR.parse_ipv4(address, maskbits)
_ -> :error
end
end

defp cast_ip6_cidr(address, maskstring) do
case Integer.parse(maskstring) do
{maskbits, ""} when maskbits in 0..128 -> CIDR.parse_ipv6(address, maskbits)
_ -> :error
end
end
end
24 changes: 18 additions & 6 deletions lib/ecto_ip_range/postgrex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,28 @@ defmodule EctoIPRange.Postgrex do
Provides a simple list of available :postgrex extension modules.
"""

alias EctoIPRange.Postgrex.IP4Extension
alias EctoIPRange.Postgrex.IP4RExtension
alias EctoIPRange.Postgrex.IP6Extension
alias EctoIPRange.Postgrex.IP6RExtension
alias EctoIPRange.Postgrex.{
IP4Extension,
IP4RExtension,
IP6Extension,
IP6RExtension,
IPRangeExtension
}

@doc """
Returns all available :postgrex extension modules.
"""
@spec extensions() :: [IP4Extension | IP4RExtension | IP6Extension | IP6RExtension, ...]
@spec extensions() :: [
IP4Extension | IP4RExtension | IP6Extension | IP6RExtension | IPRangeExtension,
...
]
def extensions do
[IP4Extension, IP4RExtension, IP6Extension, IP6RExtension]
[
IP4Extension,
IP4RExtension,
IP6Extension,
IP6RExtension,
IPRangeExtension
]
end
end
140 changes: 140 additions & 0 deletions lib/ecto_ip_range/postgrex/ip_range_extension.ex
6D4E
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
defmodule EctoIPRange.Postgrex.IPRangeExtension do
@moduledoc """
Postgrex extension for "iprange" fields.
"""

use Postgrex.BinaryExtension, type: "iprange"

import Postgrex.BinaryUtils, warn: false

alias EctoIPRange.{
IPRange,
Util.CIDR,
Util.Inet
}

def encode(_) do
quote location: :keep do
%IPRange{
range: range,
first_ip: {first_a, first_b, first_c, first_d},
last_ip: {last_a, last_b, last_c, last_d}
} ->
case CIDR.parse_mask(range) do
{:ok, mask} ->
<<12::int32, 2, mask, 0, 8, first_a, first_b, first_c, first_d, last_a, last_b,
last_c, last_d>>

_ ->
<<12::int32, 2, 255, 0, 8, first_a, first_b, first_c, first_d, last_a, last_b, last_c,
last_d>>
end

%IPRange{
range: range,
first_ip: {first_a, first_b, first_c, first_d, first_e, first_f, first_g, first_h},
last_ip: {last_a, last_b, last_c, last_d, last_e, last_f, last_g, last_h}
} ->
<<36::int32, 3, 255, 0, 32, first_a::size(16)-integer-unsigned,
first_b::size(16)-integer-unsigned, first_c::size(16)-integer-unsigned,
first_d::size(16)-integer-unsigned, first_e::size(16)-integer-unsigned,
first_f::size(16)-integer-unsigned, first_g::size(16)-integer-unsigned,
first_h::size(16)-integer-unsigned, last_a::size(16)-integer-unsigned,
last_b::size(16)-integer-unsigned, last_c::size(16)-integer-unsigned,
last_d::size(16)-integer-unsigned, last_e::size(16)-integer-unsigned,
last_f::size(16)-integer-unsigned, last_g::size(16)-integer-unsigned,
last_h::size(16)-integer-unsigned>>
end
end

def decode(_) do
quote location: :keep do
<<len::int32, data::binary-size(len)>> ->
unquote(__MODULE__).decode_range(data)
end
end

def decode_range(<<2, bits, _flag, 4, first_a, first_b, first_c, first_d>>) do
first_ip4_address = {first_a, first_b, first_c, first_d}
first_address_string = Inet.ntoa(first_ip4_address)
{cidr_first, cidr_last} = CIDR.parse_ipv4(first_address_string, bits)

%IPRange{
range: first_address_string <> "/" <> Integer.to_string(bits),
first_ip: cidr_first,
last_ip: cidr_last
}
end

def decode_range(
<<2, _bits, _flag, 8, first_a, first_b, first_c, first_d, last_a, last_b, last_c, last_d>>
) do
first_ip4_address = {first_a, first_b, first_c, first_d}
last_ip4_address = {last_a, last_b, last_c, last_d}
first_address_string = Inet.ntoa(first_ip4_address)
last_address_string = Inet.ntoa(last_ip4_address)

%IPRange{
range: first_address_string <> "-" <> last_address_string,
first_ip: first_ip4_address,
last_ip: last_ip4_address
}
end

def decode_range(
<<3, bits, _flag, 8, first_a::size(16)-integer-unsigned,
first_b::size(16)-integer-unsigned, first_c::size(16)-integer-unsigned,
first_d::size(16)-integer-unsigned>>
) do
first_address = {first_a, first_b, first_c, first_d, 0, 0, 0, 0}
first_address_string = Inet.ntoa(first_address)
{cidr_first, cidr_last} = CIDR.parse_ipv6(first_address_string, bits)

%IPRange{
range: first_address_string <> "/" <> Integer.to_string(bits),
first_ip: cidr_first,
last_ip: cidr_last
}
end

def decode_range(
<<3, bits, _flag, 16, first_a::size(16)-integer-unsigned,
first_b::size(16)-integer-unsigned, first_c::size(16)-integer-unsigned,
first_d::size(16)-integer-unsigned, first_e::size(16)-integer-unsigned,
first_f::size(16)-integer-unsigned, first_g::size(16)-integer-unsigned,
first_h::size(16)-integer-unsigned>>
) do
first_address = {first_a, first_b, first_c, first_d, first_e, first_f, first_g, first_h}
first_address_string = Inet.ntoa(first_address)
{cidr_first, cidr_last} = CIDR.parse_ipv6(first_address_string, bits)

%IPRange{
range: String.upcase(first_address_string) <> "/" <> Integer.to_string(bits),
first_ip: cidr_first,
last_ip: cidr_last
}
end

def decode_range(
<<3, _bits, _flag, 32, first_a::size(16)-integer-unsigned,
first_b::size(16)-integer-unsigned, first_c::size(16)-integer-unsigned,
first_d::size(16)-integer-unsigned, first_e::size(16)-integer-unsigned,
first_f::size(16)-integer-unsigned, first_g::size(16)-integer-unsigned,
first_h::size(16)-integer-unsigned, last_a::size(16)-integer-unsigned,
last_b::size(16)-integer-unsigned, last_c::size(16)-integer-unsigned,
last_d::size(16)-integer-unsigned, last_e::size(16)-integer-unsigned,
last_f::size(16)-integer-unsigned, last_g::size(16)-integer-unsigned,
last_h::size(16)-integer-unsigned>>
) do
first_address = {first_a, first_b, first_c, first_d, first_e, first_f, first_g, first_h}
last_address = {last_a, last_b, last_c, last_d, last_e, last_f, last_g, last_h}
first_address_string = Inet.ntoa(first_address)
last_address_string = Inet.ntoa(last_address)

%IPRange{
range: String.upcase(first_address_string) <> "-" <> String.upcase(last_address_string),
first_ip: first_address,
last_ip: last_address
}
end
end
9 changes: 9 additions & 0 deletions lib/ecto_ip_range/util/cidr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ defmodule EctoIPRange.Util.CIDR do

def parse_ipv6(_, _), do: :error

def parse_mask(address) do
with [_address, maskstring] <- String.split(address, "/", parts: 2),
{maskbits, ""} when maskbits in 0..32 <- Integer.parse(maskstring) do
{:ok, maskbits}
else
_ -> :error
end
end

defp ipv4_start_address({start_a, start_b, start_c, start_d}, maskbits) do
{mask_a, mask_b, mask_c, mask_d} =
cond do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule EctoIPRange.TestRepo.Migrations.AddTest_SchemaIpRangeTable do
use Ecto.Migration

def change do
create table("test_schema_ip_range", primary_key: false) do
add :network, EctoIPRange.IPRange.type(), primary_key: true
end
end
end
Loading
0