mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-30 14:57:59 +01:00
Merge pull request #283 from philomena-dev/mask
Add netmask support to ip_profile tag and source changes
This commit is contained in:
commit
c19e873c9b
5 changed files with 134 additions and 7 deletions
109
lib/philomena_query/ip_mask.ex
Normal file
109
lib/philomena_query/ip_mask.ex
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
defmodule PhilomenaQuery.IpMask do
|
||||||
|
@moduledoc """
|
||||||
|
Postgres IP masks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Parse a netmask from a string parameter, producing an `m:Postgrex.INET` type suitable for use in
|
||||||
|
a containment (<<=, <<, >>, >>=) query. Ignores invalid strings and passes the IP through on
|
||||||
|
error. [Postgres documentation](https://www.postgresql.org/docs/current/functions-net.html)
|
||||||
|
has more information on `inet` operations.
|
||||||
|
|
||||||
|
> #### Info {: .info}
|
||||||
|
>
|
||||||
|
> Netmasks lower than /8 are clamped to a minimum of /8. Such low masks are unlikely to be
|
||||||
|
> useful and this avoids producing very expensive masks to evaluate.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "12"})
|
||||||
|
%Postgrex.INET{address: {192, 160, 0, 0}, netmask: 12}
|
||||||
|
|
||||||
|
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "4"})
|
||||||
|
%Postgrex.INET{address: {192, 0, 0, 0}, netmask: 8}
|
||||||
|
|
||||||
|
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "64"})
|
||||||
|
%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}
|
||||||
|
|
||||||
|
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "e"})
|
||||||
|
%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}
|
||||||
|
|
||||||
|
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{})
|
||||||
|
%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}
|
||||||
|
|
||||||
|
iex> parse_mask(%Postgrex.INET{
|
||||||
|
...> address: {0x2001, 0xab0, 0x33a8, 0xd6e2, 0x10e9, 0xac1b, 0x9b0f, 0x67bc},
|
||||||
|
...> netmask: 128
|
||||||
|
...> }, %{"mask" => "64"})
|
||||||
|
%Postgrex.INET{address: {8193, 2736, 13224, 55010, 0, 0, 0, 0}, netmask: 64}
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec parse_mask(Postgrex.INET.t(), map()) :: Postgrex.INET.t()
|
||||||
|
def parse_mask(ip, params)
|
||||||
|
|
||||||
|
def parse_mask(ip, %{"mask" => mask}) when is_binary(mask) do
|
||||||
|
case Integer.parse(mask) do
|
||||||
|
{mask, _rest} ->
|
||||||
|
mask = clamp_mask(ip.address, mask)
|
||||||
|
address = apply_mask(ip.address, mask)
|
||||||
|
|
||||||
|
%Postgrex.INET{address: address, netmask: mask}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
ip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_mask(ip, _params), do: ip
|
||||||
|
|
||||||
|
defp clamp(n, min, _max) when n < min, do: min
|
||||||
|
defp clamp(n, _min, max) when n > max, do: max
|
||||||
|
defp clamp(n, _min, _max), do: n
|
||||||
|
|
||||||
|
defp clamp_mask(ip, mask) do
|
||||||
|
# Clamp mask length:
|
||||||
|
# - low end 8 (too taxing to evaluate)
|
||||||
|
# - high end address_bits (limit of address)
|
||||||
|
case tuple_size(ip) do
|
||||||
|
4 ->
|
||||||
|
clamp(mask, 8, 32)
|
||||||
|
|
||||||
|
8 ->
|
||||||
|
clamp(mask, 8, 128)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp unit_length(ip) when tuple_size(ip) == 4, do: 8
|
||||||
|
defp unit_length(ip) when tuple_size(ip) == 8, do: 16
|
||||||
|
|
||||||
|
defp apply_mask(ip, mask) when is_tuple(ip) do
|
||||||
|
# Determine whether elements are octets or hexadectets
|
||||||
|
length = unit_length(ip)
|
||||||
|
|
||||||
|
# 1. Convert tuple to list of octets/hexadectets
|
||||||
|
# 2. Convert list to bitstring
|
||||||
|
# 3. Perform truncation operation on bitstring
|
||||||
|
# 4. Convert bitstring back to list of octets/hexadectets
|
||||||
|
# 5. Convert list to tuple
|
||||||
|
|
||||||
|
ip
|
||||||
|
|> Tuple.to_list()
|
||||||
|
|> list_to_bits(length)
|
||||||
|
|> apply_mask(mask)
|
||||||
|
|> bits_to_list(length)
|
||||||
|
|> List.to_tuple()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_mask(ip, mask) when is_binary(ip) do
|
||||||
|
# Truncate bit size of ip to mask length and zero-fill the remainder
|
||||||
|
<<ip::bits-size(mask), 0::integer-size(bit_size(ip)-mask)>>
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_to_bits(list, unit_length) do
|
||||||
|
for u <- list, into: <<>>, do: <<u::integer-size(unit_length)>>
|
||||||
|
end
|
||||||
|
|
||||||
|
defp bits_to_list(bits, unit_length) do
|
||||||
|
for <<u::integer-size(unit_length) <- bits>>, do: u
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,25 +1,27 @@
|
||||||
defmodule PhilomenaWeb.IpProfile.SourceChangeController do
|
defmodule PhilomenaWeb.IpProfile.SourceChangeController do
|
||||||
use PhilomenaWeb, :controller
|
use PhilomenaWeb, :controller
|
||||||
|
|
||||||
|
alias PhilomenaQuery.IpMask
|
||||||
alias Philomena.SourceChanges.SourceChange
|
alias Philomena.SourceChanges.SourceChange
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
plug :verify_authorized
|
plug :verify_authorized
|
||||||
|
|
||||||
def index(conn, %{"ip_profile_id" => ip}) do
|
def index(conn, %{"ip_profile_id" => ip} = params) do
|
||||||
{:ok, ip} = EctoNetwork.INET.cast(ip)
|
{:ok, ip} = EctoNetwork.INET.cast(ip)
|
||||||
|
range = IpMask.parse_mask(ip, params)
|
||||||
|
|
||||||
source_changes =
|
source_changes =
|
||||||
SourceChange
|
SourceChange
|
||||||
|> where(ip: ^ip)
|
|> where(fragment("? >>= ip", ^range))
|
||||||
|> order_by(desc: :id)
|
|> order_by(desc: :id)
|
||||||
|> preload([:user, image: [:user, :sources, tags: :aliases]])
|
|> preload([:user, image: [:user, :sources, tags: :aliases]])
|
||||||
|> Repo.paginate(conn.assigns.scrivener)
|
|> Repo.paginate(conn.assigns.scrivener)
|
||||||
|
|
||||||
render(conn, "index.html",
|
render(conn, "index.html",
|
||||||
title: "Source Changes for IP `#{ip}'",
|
title: "Source Changes for IP `#{ip}'",
|
||||||
ip: ip,
|
ip: range,
|
||||||
source_changes: source_changes
|
source_changes: source_changes
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule PhilomenaWeb.IpProfile.TagChangeController do
|
defmodule PhilomenaWeb.IpProfile.TagChangeController do
|
||||||
use PhilomenaWeb, :controller
|
use PhilomenaWeb, :controller
|
||||||
|
|
||||||
|
alias PhilomenaQuery.IpMask
|
||||||
alias Philomena.TagChanges.TagChange
|
alias Philomena.TagChanges.TagChange
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
@ -9,10 +10,11 @@ defmodule PhilomenaWeb.IpProfile.TagChangeController do
|
||||||
|
|
||||||
def index(conn, %{"ip_profile_id" => ip} = params) do
|
def index(conn, %{"ip_profile_id" => ip} = params) do
|
||||||
{:ok, ip} = EctoNetwork.INET.cast(ip)
|
{:ok, ip} = EctoNetwork.INET.cast(ip)
|
||||||
|
range = IpMask.parse_mask(ip, params)
|
||||||
|
|
||||||
tag_changes =
|
tag_changes =
|
||||||
TagChange
|
TagChange
|
||||||
|> where(ip: ^ip)
|
|> where(fragment("? >>= ip", ^range))
|
||||||
|> added_filter(params)
|
|> added_filter(params)
|
||||||
|> preload([:tag, :user, image: [:user, :sources, tags: :aliases]])
|
|> preload([:tag, :user, image: [:user, :sources, tags: :aliases]])
|
||||||
|> order_by(desc: :id)
|
|> order_by(desc: :id)
|
||||||
|
@ -20,7 +22,7 @@ defmodule PhilomenaWeb.IpProfile.TagChangeController do
|
||||||
|
|
||||||
render(conn, "index.html",
|
render(conn, "index.html",
|
||||||
title: "Tag Changes for IP `#{ip}'",
|
title: "Tag Changes for IP `#{ip}'",
|
||||||
ip: ip,
|
ip: range,
|
||||||
tag_changes: tag_changes
|
tag_changes: tag_changes
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,8 +11,17 @@ ul
|
||||||
|
|
||||||
h2 Administration Options
|
h2 Administration Options
|
||||||
ul
|
ul
|
||||||
li = link "View tag changes", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes"
|
li
|
||||||
li = link "View source URL history", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes"
|
=> link "View tag changes", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes"
|
||||||
|
= if ipv6?(@ip) do
|
||||||
|
' …
|
||||||
|
= link "(/64)", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes?mask=64"
|
||||||
|
li
|
||||||
|
=> link "View source URL history", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes"
|
||||||
|
= if ipv6?(@ip) do
|
||||||
|
' …
|
||||||
|
= link "(/64)", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes?mask=64"
|
||||||
|
|
||||||
li = link "View reports this IP has made", to: ~p"/admin/reports?#{[rq: "ip:#{@ip}"]}"
|
li = link "View reports this IP has made", to: ~p"/admin/reports?#{[rq: "ip:#{@ip}"]}"
|
||||||
li = link "View IP ban history", to: ~p"/admin/subnet_bans?#{[ip: to_string(@ip)]}"
|
li = link "View IP ban history", to: ~p"/admin/subnet_bans?#{[ip: to_string(@ip)]}"
|
||||||
li = link "Ban this sucker", to: ~p"/admin/subnet_bans/new?#{[specification: to_string(@ip)]}"
|
li = link "Ban this sucker", to: ~p"/admin/subnet_bans/new?#{[specification: to_string(@ip)]}"
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
defmodule PhilomenaWeb.IpProfileView do
|
defmodule PhilomenaWeb.IpProfileView do
|
||||||
use PhilomenaWeb, :view
|
use PhilomenaWeb, :view
|
||||||
|
|
||||||
|
@spec ipv6?(Postgrex.INET.t()) :: boolean()
|
||||||
|
def ipv6?(ip) do
|
||||||
|
tuple_size(ip.address) == 8
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue