defmodule OhMyExOhIrcd.IRC.FloodTest do
@moduledoc """
Flood control: a leaky bucket measured in seconds of penalty. Each
parsed command adds a per-command penalty, the bucket drains at
1 sec/sec real time, and overflow drops the connection with
`ERROR :Excess Flood`.
"""
use ExUnit.Case, async: true
import OhMyExOhIrcd.IRCTestHelper
test "rapid burst exceeds the threshold and gets dropped with Excess Flood" do
# Threshold 8s leaves ~4s of headroom after the register+JOIN setup
# (NICK 1.5 + USER 1.0 + JOIN 1.5 ≈ 4s). A burst of 30 PRIVMSGs at
# penalty 1.0 each easily blows past.
port =
start_listener(%{
flood_threshold_seconds: 8,
flood_penalty_default: 1.0
})
chan = unique_channel("flood")
sock = register(port, unique_nick("flooder"))
send_line(sock, "JOIN #{chan}")
drain(sock, 200)
Enum.each(1..30, fn n ->
# `send_line` can hit {:error, :closed} once the server has dropped
# us — just keep going; the test only cares that the ERROR line lands.
try do
send_line(sock, "PRIVMSG #{chan} :spam #{n}")
rescue
_ -> :ok
end
end)
collected = drain(sock, 800)
assert Enum.any?(collected, &String.contains?(&1, "Excess Flood")),
"expected ERROR :Excess Flood, got: #{inspect(collected)}"
end
test "PRIVMSG to a +f channel bypasses flood control" do
port =
start_listener(%{
flood_threshold_seconds: 5,
flood_penalty_default: 1.0
})
chan = unique_channel("broker")
nick = unique_nick("publisher")
sock = register(port, nick)
send_line(sock, "JOIN #{chan}")
drain(sock, 200)
# Founder bypass via the test back door; then set +f on the channel.
alias OhMyExOhIrcd.IRC.Channel
:ok = Channel.__seed_access__(chan, nick, :founder)
drain(sock, 100)
send_line(sock, "MODE #{chan} +f")
drain(sock, 200)
# 50 PRIVMSGs that would otherwise hit the ~5s threshold many times over.
Enum.each(1..50, fn n -> send_line(sock, "PRIVMSG #{chan} :broadcast #{n}") end)
collected = drain(sock, 800)
refute Enum.any?(collected, &String.contains?(&1, "Excess Flood")),
"expected no Excess Flood on +f channel, but got: #{inspect(collected)}"
end
test "non-founder can't set +f" do
port = start_listener()
chan = unique_channel("nof")
a_nick = unique_nick("a")
b_nick = unique_nick("b")
a = register(port, a_nick)
b = register(port, b_nick)
send_line(a, "JOIN #{chan}")
send_line(b, "JOIN #{chan}")
drain(a, 200)
drain(b, 200)
# Make B a halfop — sufficient for normal channel modes but NOT for +f.
alias OhMyExOhIrcd.IRC.Channel
:ok = Channel.__seed_access__(chan, b_nick, :halfop)
drain(a, 100)
drain(b, 100)
send_line(b, "MODE #{chan} +f")
assert Enum.any?(drain(b, 300), &String.contains?(&1, " 482 "))
end
test "paced traffic does NOT trip flood" do
port =
start_listener(%{
flood_threshold_seconds: 5,
flood_penalty_default: 1.0
})
chan = unique_channel("paced")
sock = register(port, unique_nick("paced"))
send_line(sock, "JOIN #{chan}")
drain(sock, 200)
# 5 messages spaced > 1s apart — the bucket drains as fast as we fill.
Enum.each(1..5, fn n ->
send_line(sock, "PRIVMSG #{chan} :ping #{n}")
Process.sleep(1_100)
end)
collected = drain(sock, 300)
refute Enum.any?(collected, &String.contains?(&1, "Excess Flood"))
end
end
neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
3.5 KiB · text
History
4b6fe3b