Public Access
IRC: join after NickServ login, keyed auto-join, SASL fallback
- With NickServ, auto-join waits for the logged-in reply (900) or 2 s
at most, so registered-only IRC channels let us in
- A failed SASL login falls back to NickServ (its own password, or the
SASL account and password)
- IDENTIFY names the account explicitly, and once logged in on a
fallback nick, REGAIN takes ours back from a stale session
- Auto-join entries take keys ("#private key, #public"); keys from
/join are reused when rejoining; keyed channels go first in JOIN
- Serial irc dump: whole Buffers, and which login is configured
(never the secrets)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -28,7 +28,7 @@ void test_save_and_reload() {
|
||||
c.nick = "clement";
|
||||
c.saslUser = "clement";
|
||||
c.saslPassword = "s3cret";
|
||||
c.autojoin = {"#roro", "#meshtastic"};
|
||||
c.autojoin = {{"#roro", ""}, {"#meshtastic", "k3y"}};
|
||||
TEST_ASSERT_TRUE(c.save().empty());
|
||||
}
|
||||
IrcConfig again(store);
|
||||
@@ -36,7 +36,8 @@ void test_save_and_reload() {
|
||||
TEST_ASSERT_EQUAL_STRING("clement", again.nick.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("s3cret", again.saslPassword.c_str());
|
||||
TEST_ASSERT_EQUAL(2, again.autojoin.size());
|
||||
TEST_ASSERT_EQUAL_STRING("#meshtastic", again.autojoin[1].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("#meshtastic", again.autojoin[1].channel.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("k3y", again.autojoin[1].key.c_str());
|
||||
}
|
||||
|
||||
void test_invalid_values_are_refused_with_a_reason() {
|
||||
@@ -54,14 +55,32 @@ void test_invalid_values_are_refused_with_a_reason() {
|
||||
c.host = "";
|
||||
TEST_ASSERT_FALSE(c.save().empty());
|
||||
c.host = "irc.libera.chat";
|
||||
c.autojoin = {"roro"}; // IRC channels start with # or &
|
||||
c.autojoin = {{"roro", ""}}; // IRC channels start with # or &
|
||||
TEST_ASSERT_FALSE(c.save().empty());
|
||||
}
|
||||
|
||||
void test_autojoin_text_round_trip() {
|
||||
TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a, #b").size());
|
||||
TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a #b").size());
|
||||
TEST_ASSERT_EQUAL_STRING("#a #b", IrcConfig::formatChannels({"#a", "#b"}).c_str());
|
||||
TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a #b").size()); // the older, space-separated form
|
||||
TEST_ASSERT_EQUAL_STRING("#a, #b", IrcConfig::formatChannels({{"#a", ""}, {"#b", ""}}).c_str());
|
||||
}
|
||||
|
||||
void test_autojoin_entries_can_carry_a_key() {
|
||||
auto list = IrcConfig::parseChannels("#private s3cret, #public &local");
|
||||
TEST_ASSERT_EQUAL(3, list.size());
|
||||
TEST_ASSERT_EQUAL_STRING("#private", list[0].channel.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("s3cret", list[0].key.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("", list[1].key.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("&local", list[2].channel.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("#private s3cret, #public, &local", IrcConfig::formatChannels(list).c_str());
|
||||
}
|
||||
|
||||
void test_a_key_without_a_channel_is_refused() {
|
||||
MemoryStore store;
|
||||
IrcConfig c(store);
|
||||
c.load("x");
|
||||
c.autojoin = IrcConfig::parseChannels("orphan #a");
|
||||
TEST_ASSERT_FALSE(c.validate().empty());
|
||||
}
|
||||
|
||||
void test_copy_settings_from_a_draft() {
|
||||
@@ -70,7 +89,7 @@ void test_copy_settings_from_a_draft() {
|
||||
live.load("x");
|
||||
IrcConfig draft = live;
|
||||
draft.host = "irc.example.org";
|
||||
draft.autojoin = {"#a"};
|
||||
draft.autojoin = {{"#a", ""}};
|
||||
TEST_ASSERT_EQUAL_STRING("irc.libera.chat", live.host.c_str()); // the draft is independent
|
||||
live.copySettingsFrom(draft);
|
||||
TEST_ASSERT_EQUAL_STRING("irc.example.org", live.host.c_str());
|
||||
@@ -84,5 +103,7 @@ int main() {
|
||||
RUN_TEST(test_invalid_values_are_refused_with_a_reason);
|
||||
RUN_TEST(test_autojoin_text_round_trip);
|
||||
RUN_TEST(test_copy_settings_from_a_draft);
|
||||
RUN_TEST(test_autojoin_entries_can_carry_a_key);
|
||||
RUN_TEST(test_a_key_without_a_channel_is_refused);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ struct Fixture {
|
||||
explicit Fixture(bool sasl = false) {
|
||||
config.load("roro");
|
||||
config.nick = "roro";
|
||||
config.autojoin = {"#roro"};
|
||||
config.autojoin = {{"#roro", ""}};
|
||||
if (sasl) {
|
||||
config.saslUser = "acct";
|
||||
config.saslPassword = "pw";
|
||||
@@ -28,6 +28,7 @@ struct Fixture {
|
||||
session.reset(new IrcSession(config));
|
||||
}
|
||||
void recv(const std::string& line) { session->receive(line, 1000); }
|
||||
void tick(uint32_t ms) { session->tick(ms); }
|
||||
IrcEffects& take() {
|
||||
fx = session->takeEffects();
|
||||
return fx;
|
||||
@@ -65,15 +66,127 @@ void test_welcome_registers_and_joins_autojoin_channels() {
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #roro"));
|
||||
}
|
||||
|
||||
void test_nickserv_identify_after_welcome() {
|
||||
void test_nickserv_identify_after_welcome_and_joins_wait_for_it() {
|
||||
Fixture f;
|
||||
f.config.nickservPassword = "pw";
|
||||
f.session.reset(new IrcSession(f.config));
|
||||
f.registerNow();
|
||||
f.session->connected(0);
|
||||
f.tick(0);
|
||||
f.recv(":srv 001 roro :Welcome");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY roro pw"));
|
||||
TEST_ASSERT_FALSE(f.sent("JOIN #roro")); // not before NickServ has had its say
|
||||
f.tick(1000);
|
||||
TEST_ASSERT_TRUE(f.take().send.empty());
|
||||
}
|
||||
|
||||
void test_joins_follow_the_logged_in_reply() {
|
||||
Fixture f;
|
||||
f.config.nickservPassword = "pw";
|
||||
f.session.reset(new IrcSession(f.config));
|
||||
f.session->connected(0);
|
||||
f.tick(0);
|
||||
f.recv(":srv 001 roro :Welcome");
|
||||
f.take();
|
||||
f.tick(300);
|
||||
f.recv(":srv 900 roro roro!u@h roro :You are now logged in as roro");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #roro"));
|
||||
f.tick(5000);
|
||||
TEST_ASSERT_TRUE(f.take().send.empty()); // and only once
|
||||
}
|
||||
|
||||
void test_joins_go_ahead_after_two_seconds_without_a_reply() {
|
||||
Fixture f;
|
||||
f.config.nickservPassword = "pw";
|
||||
f.session.reset(new IrcSession(f.config));
|
||||
f.session->connected(0);
|
||||
f.tick(10000);
|
||||
f.recv(":srv 001 roro :Welcome");
|
||||
f.take();
|
||||
f.tick(11999);
|
||||
TEST_ASSERT_TRUE(f.take().send.empty());
|
||||
f.tick(12000);
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #roro"));
|
||||
}
|
||||
|
||||
void test_failed_sasl_falls_back_to_nickserv_and_holds_joins() {
|
||||
Fixture f(true);
|
||||
f.config.nickservPassword = "nspw";
|
||||
f.session.reset(new IrcSession(f.config));
|
||||
f.session->connected(0);
|
||||
f.recv(":srv CAP * ACK :sasl");
|
||||
f.recv("AUTHENTICATE +");
|
||||
f.recv(":srv 904 roro :SASL authentication failed");
|
||||
f.recv(":srv 001 roro :Welcome");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY acct nspw"));
|
||||
TEST_ASSERT_FALSE(f.sent("JOIN #roro"));
|
||||
f.recv(":srv 900 roro roro!u@h roro :You are now logged in as roro");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #roro"));
|
||||
}
|
||||
|
||||
void test_failed_sasl_without_nickserv_password_identifies_with_the_sasl_account() {
|
||||
Fixture f(true); // SASL user "acct", password "pw"
|
||||
f.session->connected(0);
|
||||
f.recv(":srv 904 roro :SASL authentication failed");
|
||||
f.recv(":srv 001 roro :Welcome");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY acct pw"));
|
||||
}
|
||||
|
||||
void test_successful_sasl_joins_at_once() {
|
||||
Fixture f(true);
|
||||
f.config.nickservPassword = "nspw";
|
||||
f.session.reset(new IrcSession(f.config));
|
||||
f.session->connected(0);
|
||||
f.recv(":srv 903 roro :SASL authentication successful");
|
||||
f.recv(":srv 001 roro :Welcome");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #roro"));
|
||||
TEST_ASSERT_FALSE(f.sent("PRIVMSG NickServ :IDENTIFY acct nspw"));
|
||||
}
|
||||
|
||||
void test_identify_names_the_account_even_on_a_fallback_nick_then_regains_it() {
|
||||
Fixture f;
|
||||
f.config.nickservPassword = "pw";
|
||||
f.session.reset(new IrcSession(f.config));
|
||||
f.session->connected(0);
|
||||
f.recv(":srv 433 * roro :Nickname is already in use"); // a stale session still holds it
|
||||
f.recv(":srv 001 roro_ :Welcome");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY roro pw"));
|
||||
f.recv(":srv 900 roro_ roro_!u@h roro :You are now logged in as roro");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :REGAIN roro"));
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #roro"));
|
||||
}
|
||||
|
||||
void test_autojoin_sends_keyed_channels_first_with_their_keys() {
|
||||
Fixture f;
|
||||
f.config.autojoin = IrcConfig::parseChannels("#public, #private s3cret");
|
||||
f.session.reset(new IrcSession(f.config));
|
||||
f.session->connected(0);
|
||||
f.recv(":srv 001 roro :Welcome");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY pw"));
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #private,#public s3cret"));
|
||||
}
|
||||
|
||||
void test_keys_used_with_join_are_reused_on_rejoin() {
|
||||
Fixture f;
|
||||
f.registerNow();
|
||||
f.recv(":roro!u@h JOIN #roro");
|
||||
f.session->input(1, "/join #vault k3y", 0);
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #vault k3y"));
|
||||
f.recv(":roro!u@h JOIN #vault");
|
||||
f.session->disconnected(0, "Wi-Fi lost");
|
||||
f.session->connected(0);
|
||||
f.recv(":srv 001 roro :Welcome");
|
||||
f.take();
|
||||
TEST_ASSERT_TRUE(f.sent("JOIN #vault,#roro k3y"));
|
||||
}
|
||||
|
||||
void test_sasl_plain_flow() {
|
||||
@@ -347,7 +460,15 @@ int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_plain_registration_sends_nick_and_user);
|
||||
RUN_TEST(test_welcome_registers_and_joins_autojoin_channels);
|
||||
RUN_TEST(test_nickserv_identify_after_welcome);
|
||||
RUN_TEST(test_nickserv_identify_after_welcome_and_joins_wait_for_it);
|
||||
RUN_TEST(test_joins_follow_the_logged_in_reply);
|
||||
RUN_TEST(test_joins_go_ahead_after_two_seconds_without_a_reply);
|
||||
RUN_TEST(test_failed_sasl_falls_back_to_nickserv_and_holds_joins);
|
||||
RUN_TEST(test_failed_sasl_without_nickserv_password_identifies_with_the_sasl_account);
|
||||
RUN_TEST(test_successful_sasl_joins_at_once);
|
||||
RUN_TEST(test_identify_names_the_account_even_on_a_fallback_nick_then_regains_it);
|
||||
RUN_TEST(test_autojoin_sends_keyed_channels_first_with_their_keys);
|
||||
RUN_TEST(test_keys_used_with_join_are_reused_on_rejoin);
|
||||
RUN_TEST(test_sasl_plain_flow);
|
||||
RUN_TEST(test_sasl_failure_still_ends_negotiation_and_says_so);
|
||||
RUN_TEST(test_nick_in_use_during_registration_tries_another);
|
||||
|
||||
Reference in New Issue
Block a user