- MUC сохраняет согласованный журнал конфигурации, affiliations, nick,
ролей, членства и ограниченной истории без MAM. Изменение фиксируется до публикации ответа; при ошибке записи состояние комнаты откатывается. - Crash recovery использует последнее сохранённое поколение после SIGKILL, включая временные комнаты. Heartbeat обновляет живое членство; ожидающие записи сохраняют исходный deadline через повторные перезапуски. `muc_recovery_grace_period`: по умолчанию 300 секунд, пределы 1..604800. - Подтверждённые выходы, смена nick, ролей и политик комнаты не заменяются старой записью восстановления. Native config/state остаются экспортом для Prosody; согласованное восстановление Rust использует собственный sidecar. - Исправлены MUC probe, неизвестные типы presence, вход без MUC `<x/>`, ошибки доставки и статус 333. Обычные presence extensions сохраняются в живом обмене; поддельная MUC identity фильтруется. `subject` вместе с `body` либо `thread` не изменяет тему комнаты. - Подключены `muc_max_rooms`, `restrict_room_creation`, политики регистрации и зарегистрированного nick, `muc_tombstone_expiry`; добавлены встроенные административные ad-hoc команды уничтожения комнат и изменения affiliation. - Hub сохраняет resumable XEP-0198 сессии, счётчики и очередь. Resume после перезапуска доступен на новом TLS/SASL-транспорте через legacy SASL и SASL2. Действующие приватные SM-записи восстанавливают логические сессии и MUC до открытия listeners: offline-доставка попадает в очередь, синтетический join и история повторно не отправляются. SM resume не требует дополнительной available presence; без SM восстановление MUC требует новой аутентификации точного полного JID и available presence либо явного MUC-входа. Входящий handled-счётчик обновляется после обработки. Сокеты TCP/TLS не сохраняются; неопределённая сетевая отправка допускает повторную доставку. - Известные встроенные MUC-возможности Prosody отображаются на Rust-реализацию. Произвольные Lua/community-модули не исполняются; неизвестные явно включённые MUC-модули отклоняются при запуске. Общие и host `modules_disabled` учитываются для vCard и occupant-id. Порядок восстановления, ограничения хранения и отката на Prosody: [docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md). Проверка: 293 Rust tests, Clippy/fmt/release, полный сетевой прогон, 500 TLS MUC+SM ресурсов с SIGKILL/resume и native reference — успешно. Подробности: [docs/VALIDATION-0.8.0.md](docs/VALIDATION-0.8.0.md).
This commit is contained in:
1 parent
b30fc524d8
commit
7f60661eef
41 files changed
+9804
-452
No files matched your search
@@ -0,0 +1,201 @@
|
||||
use rust_xmpp::{muc::Muc, protocol::ModuleResult};
|
||||
use serde_json::json;
|
||||
const ROOM: &str = "room@rooms.example.org";
|
||||
const COMMANDS: &str = "http://jabber.org/protocol/commands";
|
||||
const AFF: &str = "http://prosody.im/protocol/muc#set-affiliation";
|
||||
const DESTROY: &str = "http://prosody.im/protocol/muc#destroy";
|
||||
fn send(m: &mut Muc, jid: &str, xml: &str) -> ModuleResult {
|
||||
let doc = roxmltree::Document::parse(xml).unwrap();
|
||||
m.handle(jid, doc.root_element()).unwrap().unwrap()
|
||||
}
|
||||
fn room(m: &mut Muc) {
|
||||
send(
|
||||
m,
|
||||
"owner@example.org/r",
|
||||
&format!(
|
||||
"<presence to='{ROOM}/Owner'><x xmlns='http://jabber.org/protocol/muc'/></presence>"
|
||||
),
|
||||
);
|
||||
send(
|
||||
m,
|
||||
"owner@example.org/r",
|
||||
&format!(
|
||||
"<iq to='{ROOM}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x xmlns='jabber:x:data' type='submit'><field var='muc#roomconfig_persistentroom'><value>1</value></field></x></query></iq>"
|
||||
),
|
||||
);
|
||||
}
|
||||
fn register(m: &mut Muc, jid: &str, nick: &str) -> ModuleResult {
|
||||
send(
|
||||
m,
|
||||
jid,
|
||||
&format!(
|
||||
"<iq to='{ROOM}' type='set'><query xmlns='jabber:iq:register'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE'><value>http://jabber.org/protocol/muc#register</value></field><field var='muc#register_roomnick'><value>{nick}</value></field></x></query></iq>"
|
||||
),
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn optional_registration_grants_membership_and_enforces_reserved_nickname() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut m = Muc::new(root.path().into(), "rooms.example.org".into());
|
||||
m.set_component_options(json!({"allow_unaffiliated_register":true,"enforce_registered_nickname":true,"muc_registration_include_form":true}),false);
|
||||
room(&mut m);
|
||||
assert!(register(&mut m, "bob@example.org/r", "Reserved").replies[0].contains("type='result'"));
|
||||
let result = send(
|
||||
&mut m,
|
||||
"bob@example.org/r",
|
||||
&format!("<iq to='{ROOM}' type='get'><query xmlns='jabber:iq:register'/></iq>"),
|
||||
);
|
||||
assert!(result.replies[0].contains("type='result'><field"));
|
||||
assert!(result.replies[0].contains("<value>Reserved</value>"));
|
||||
let joined = send(
|
||||
&mut m,
|
||||
"bob@example.org/r",
|
||||
&format!(
|
||||
"<presence to='{ROOM}/Other'><x xmlns='http://jabber.org/protocol/muc'/></presence>"
|
||||
),
|
||||
);
|
||||
assert!(
|
||||
joined
|
||||
.replies
|
||||
.iter()
|
||||
.any(|p| p.contains("/Reserved'") && p.contains("affiliation='member'"))
|
||||
);
|
||||
let rename = send(
|
||||
&mut m,
|
||||
"bob@example.org/r",
|
||||
&format!("<presence to='{ROOM}/Other'/>"),
|
||||
);
|
||||
assert!(rename.replies[0].contains("not-acceptable"));
|
||||
let reregister = register(&mut m, "bob@example.org/r", "New");
|
||||
assert!(
|
||||
reregister
|
||||
.deliveries
|
||||
.iter()
|
||||
.any(|(_, p)| p.contains("code='307'"))
|
||||
);
|
||||
let message = send(
|
||||
&mut m,
|
||||
"bob@example.org/r",
|
||||
&format!("<message to='{ROOM}' type='groupchat'><body>departed</body></message>"),
|
||||
);
|
||||
assert!(message.replies[0].contains("not-acceptable"));
|
||||
}
|
||||
#[test]
|
||||
fn default_registration_denies_unaffiliated_users() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut m = Muc::new(root.path().into(), "rooms.example.org".into());
|
||||
room(&mut m);
|
||||
assert!(
|
||||
register(&mut m, "bob@example.org/r", "Bob").replies[0].contains("registration-required")
|
||||
);
|
||||
}
|
||||
fn start(m: &mut Muc, jid: &str, node: &str) -> String {
|
||||
let result = send(
|
||||
m,
|
||||
jid,
|
||||
&format!(
|
||||
"<iq to='rooms.example.org' type='set'><command xmlns='{COMMANDS}' node='{node}'/></iq>"
|
||||
),
|
||||
);
|
||||
let doc = roxmltree::Document::parse(&result.replies[0]).unwrap();
|
||||
doc.descendants()
|
||||
.find(|n| n.has_tag_name((COMMANDS, "command")))
|
||||
.unwrap()
|
||||
.attribute("sessionid")
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
fn submit(m: &mut Muc, jid: &str, node: &str, id: &str, fields: &str) -> ModuleResult {
|
||||
send(
|
||||
m,
|
||||
jid,
|
||||
&format!(
|
||||
"<iq to='rooms.example.org' type='set'><command xmlns='{COMMANDS}' node='{node}' sessionid='{id}' action='complete'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE'><value>{node}</value></field>{fields}</x></command></iq>"
|
||||
),
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn administration_commands_recheck_authority_bind_sessions_and_update_live_rooms() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut m = Muc::new(root.path().into(), "rooms.example.org".into());
|
||||
m.set_component_options(json!({"admins":["admin@example.org","other@example.org"],"muc_tombstone_expiry":"0 seconds"}),false);
|
||||
room(&mut m);
|
||||
let outsider = send(
|
||||
&mut m,
|
||||
"stranger@example.org/r",
|
||||
&format!(
|
||||
"<iq to='rooms.example.org' type='get'><query xmlns='http://jabber.org/protocol/disco#items' node='{COMMANDS}'/></iq>"
|
||||
),
|
||||
);
|
||||
assert!(!outsider.replies[0].contains("node='http://prosody.im/protocol/muc#destroy'"));
|
||||
let denied = send(
|
||||
&mut m,
|
||||
"stranger@example.org/r",
|
||||
&format!(
|
||||
"<iq to='rooms.example.org' type='set'><command xmlns='{COMMANDS}' node='{AFF}'/></iq>"
|
||||
),
|
||||
);
|
||||
assert!(denied.replies[0].contains("forbidden"));
|
||||
let id = start(&mut m, "admin@example.org/r", AFF);
|
||||
let fields = format!(
|
||||
"<field var='room'><value>{ROOM}</value></field><field var='jid'><value>owner@example.org</value></field><field var='affiliation'><value>outcast</value></field>"
|
||||
);
|
||||
assert!(
|
||||
submit(&mut m, "other@example.org/r", AFF, &id, &fields).replies[0].contains("bad-request")
|
||||
);
|
||||
let updated = submit(&mut m, "admin@example.org/r", AFF, &id, &fields);
|
||||
assert!(updated.replies[0].contains("status='completed'"));
|
||||
assert!(
|
||||
updated
|
||||
.deliveries
|
||||
.iter()
|
||||
.any(|(_, p)| p.contains("code='301'"))
|
||||
);
|
||||
assert!(
|
||||
submit(&mut m, "admin@example.org/r", AFF, &id, &fields).replies[0].contains("bad-request")
|
||||
);
|
||||
// A privileged component command may remove the final owner, just like
|
||||
// native room:set_affiliation(true,...); destroying such a room still works.
|
||||
let id = start(&mut m, "admin@example.org/r", DESTROY);
|
||||
assert!(
|
||||
submit(
|
||||
&mut m,
|
||||
"admin@example.org/r",
|
||||
DESTROY,
|
||||
&id,
|
||||
&format!("<field var='rooms'><value>{ROOM}</value></field>")
|
||||
)
|
||||
.replies[0]
|
||||
.contains("status='completed'")
|
||||
);
|
||||
let rejoin = send(
|
||||
&mut m,
|
||||
"new@example.org/r",
|
||||
&format!(
|
||||
"<presence to='{ROOM}/New'><x xmlns='http://jabber.org/protocol/muc'/></presence>"
|
||||
),
|
||||
);
|
||||
assert!(rejoin.replies.iter().any(|p| p.contains("code='201'")));
|
||||
}
|
||||
#[test]
|
||||
fn configured_tombstone_expiry_allows_immediate_recreation() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut m = Muc::new(root.path().into(), "rooms.example.org".into());
|
||||
m.set_component_options(json!({"muc_tombstone_expiry":"0 seconds"}), false);
|
||||
room(&mut m);
|
||||
send(
|
||||
&mut m,
|
||||
"owner@example.org/r",
|
||||
&format!(
|
||||
"<iq to='{ROOM}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><destroy/></query></iq>"
|
||||
),
|
||||
);
|
||||
let joined = send(
|
||||
&mut m,
|
||||
"new@example.org/r",
|
||||
&format!(
|
||||
"<presence to='{ROOM}/New'><x xmlns='http://jabber.org/protocol/muc'/></presence>"
|
||||
),
|
||||
);
|
||||
assert!(joined.replies.iter().any(|p| p.contains("code='201'")));
|
||||
}
|
||||
Reference in new issue
Block a user