defmodule OhMyExOhIrcd.IRC.OperTest do
@moduledoc """
IRC operator authentication (`OPER`) and the operator-only `KILL`
command. Opers are configured via `:opers` app env; we mutate that
for the duration of each test.
"""
use OhMyExOhIrcd.DataCase, async: false
import OhMyExOhIrcd.IRCTestHelper
@oper_password "operpass-#{System.unique_integer([:positive])}"
setup do
hash = Bcrypt.hash_pwd_salt(@oper_password)
prev = Application.get_env(:oh_my_ex_oh_ircd, :opers, %{})
Application.put_env(:oh_my_ex_oh_ircd, :opers, %{
"admin" => %{password_hash: hash, host_mask: "*@*"},
"tightbound" => %{password_hash: hash, host_mask: "noone@nowhere"}
})
on_exit(fn -> Application.put_env(:oh_my_ex_oh_ircd, :opers, prev) end)
:ok
end
defp drain_after_event(sock, pattern, timeout_ms \\ 600) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
Stream.repeatedly(fn ->
remaining = max(deadline - System.monotonic_time(:millisecond), 0)
:gen_tcp.recv(sock, 0, min(remaining, 200))
end)
|> Enum.find_value(fn
{:ok, line} ->
line = String.trim_trailing(line)
if line =~ pattern, do: line, else: nil
_ ->
nil
end)
end
test "OPER with correct credentials grants operator status" do
port = start_listener()
sock = register(port, unique_nick("oper"))
send_line(sock, "OPER admin #{@oper_password}")
line = drain_after_event(sock, ~r/ 381 /)
assert line, "expected RPL_YOUREOPER (381)"
end
test "OPER with bad password returns 464" do
port = start_listener()
sock = register(port, unique_nick("oper"))
send_line(sock, "OPER admin wrong-password")
line = drain_after_event(sock, ~r/ 464 /)
assert line
end
test "OPER with unknown name returns 464 (avoids leaking which is wrong)" do
port = start_listener()
sock = register(port, unique_nick("oper"))
send_line(sock, "OPER nobody #{@oper_password}")
line = drain_after_event(sock, ~r/ 464 /)
assert line
end
test "OPER with mismatched host mask returns 491" do
port = start_listener()
sock = register(port, unique_nick("oper"))
# `tightbound` mask is noone@nowhere — we connect from 127.0.0.1
send_line(sock, "OPER tightbound #{@oper_password}")
line = drain_after_event(sock, ~r/ 491 /)
assert line
end
test "KILL by non-oper returns 481" do
port = start_listener()
a = register(port, unique_nick("attacker"))
_b = register(port, unique_nick("victim"))
send_line(a, "KILL victim :no reason")
line = drain_after_event(a, ~r/ 481 /)
assert line
end
test "KILL by oper forces the target to disconnect with ERROR :Killed" do
port = start_listener()
a = register(port, unique_nick("admin1"))
victim_nick = unique_nick("victim")
b = register(port, victim_nick)
send_line(a, "OPER admin #{@oper_password}")
drain_after_event(a, ~r/ 381 /)
send_line(a, "KILL #{victim_nick} :you misbehaved")
err = drain_after_event(b, ~r/ERROR :Closing link: Killed by/)
assert err, "expected ERROR :Closing link: Killed by ..."
end
test "OPER with no args promotes a SASL-authed user with is_oper=true" do
import OhMyExOhIrcd.AccountsFixtures
alias OhMyExOhIrcd.Accounts
password = "longenoughpass#{System.unique_integer([:positive])}"
user = user_fixture()
{:ok, {user, _}} = Accounts.update_user_password(user, %{password: password})
{:ok, user} = Accounts.set_oper(user, true)
port = start_listener()
sock = register_sasl(port, unique_nick("authoper"), user.email, password)
send_line(sock, "OPER")
line = drain_after_event(sock, ~r/ 381 /)
assert line, "expected RPL_YOUREOPER (381) for is_oper user"
end
test "OPER with no args rejected when account isn't flagged" do
import OhMyExOhIrcd.AccountsFixtures
alias OhMyExOhIrcd.Accounts
password = "longenoughpass#{System.unique_integer([:positive])}"
user = user_fixture()
{:ok, {user, _}} = Accounts.update_user_password(user, %{password: password})
port = start_listener()
sock = register_sasl(port, unique_nick("plain"), user.email, password)
send_line(sock, "OPER")
assert drain_after_event(sock, ~r/ 464 /)
end
test "OPER name+password promotes a flagged SASL user (clients always send a password)" do
import OhMyExOhIrcd.AccountsFixtures
alias OhMyExOhIrcd.Accounts
password = "longenoughpass#{System.unique_integer([:positive])}"
user = user_fixture()
{:ok, {user, _}} = Accounts.update_user_password(user, %{password: password})
{:ok, user} = Accounts.set_oper(user, true)
port = start_listener()
sock = register_sasl(port, unique_nick("authoper"), user.email, password)
# irssi/weechat `/oper` always sends a name + (here irrelevant) password,
# never a bare OPER — the flagged account still gets promoted.
send_line(sock, "OPER whatever ignored-password")
line = drain_after_event(sock, ~r/ 381 /)
assert line, "expected RPL_YOUREOPER (381) via name+password for is_oper user"
end
test "an is_oper grant issued after authentication takes effect without reconnecting" do
import OhMyExOhIrcd.AccountsFixtures
alias OhMyExOhIrcd.Accounts
password = "longenoughpass#{System.unique_integer([:positive])}"
user = user_fixture()
{:ok, {user, _}} = Accounts.update_user_password(user, %{password: password})
port = start_listener()
sock = register_sasl(port, unique_nick("lateoper"), user.email, password)
# Not flagged yet — refused.
send_line(sock, "OPER")
assert drain_after_event(sock, ~r/ 464 /)
# Flag the account mid-session (mirrors `Release.grant_oper`).
{:ok, _} = Accounts.set_oper(user, true)
# Same connection, no reconnect: the flag is re-read from the DB, so
# the stale cached struct doesn't block promotion.
send_line(sock, "OPER")
assert drain_after_event(sock, ~r/ 381 /),
"expected RPL_YOUREOPER (381) after a mid-session grant"
end
test "WHOIS on an oper includes 313 RPL_WHOISOPERATOR" do
port = start_listener()
admin_nick = unique_nick("admin1")
a = register(port, admin_nick)
b = register(port, unique_nick("watcher"))
send_line(a, "OPER admin #{@oper_password}")
drain_after_event(a, ~r/ 381 /)
send_line(b, "WHOIS #{admin_nick}")
Process.sleep(200)
drained = drain(b, 400)
assert Enum.any?(drained, &String.contains?(&1, " 313 ")),
"expected 313 RPL_WHOISOPERATOR in WHOIS reply: #{inspect(drained)}"
end
end
neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
6.5 KiB · text
History
4b6fe3b