// Matrix Construct // // Copyright (C) Matrix Construct Developers, Authors & Contributors // Copyright (C) 2016-2018 Jason Volk // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice is present in all copies. The // full license for this software is available in the LICENSE file. namespace ircd::m { extern conf::item alias_fetch_timeout; extern conf::item alias_cache_ttl; extern const room::id::buf alias_room_id; extern const room alias_room; static void auth_room_aliases(const event &, room::auth::hookdata &); extern hookfn auth_room_aliases_hookfn; static void changed_room_aliases(const event &, vm::eval &); extern hookfn changed_room_aliases_hookfn; extern hookfn create_alias_room_hookfn; } ircd::mapi::header IRCD_MODULE { "Matrix m.room.aliases" }; decltype(ircd::m::alias_room_id) ircd::m::alias_room_id { "alias", ircd::my_host() }; decltype(ircd::m::alias_room) ircd::m::alias_room { alias_room_id }; decltype(ircd::m::alias_cache_ttl) ircd::m::alias_cache_ttl { { "name", "ircd.m.room.aliases.cache.ttl" }, { "default", 604800L }, }; decltype(ircd::m::alias_fetch_timeout) ircd::m::alias_fetch_timeout { { "name", "ircd.m.room.aliases.fetch.timeout" }, { "default", 10L }, }; // // create the alias room as an effect of !ircd created on bootstrap // decltype(ircd::m::create_alias_room_hookfn) ircd::m::create_alias_room_hookfn { { { "_site", "vm.effect" }, { "room_id", "!ircd" }, { "type", "m.room.create" }, }, [](const m::event &, m::vm::eval &) { create(alias_room_id, m::me.user_id); } }; // // an effect of room aliases changed // decltype(ircd::m::changed_room_aliases_hookfn) ircd::m::changed_room_aliases_hookfn { changed_room_aliases, { { "_site", "vm.effect" }, { "type", "m.room.aliases" }, } }; void ircd::m::changed_room_aliases(const m::event &event, m::vm::eval &) { const m::room::id &room_id { at<"room_id"_>(event) }; const json::array &aliases { at<"content"_>(event).get("aliases") }; for(const json::string &alias : aliases) try { m::room::aliases::cache::set(alias, room_id); log::info { m::log, "Updated aliases of %s by %s in %s with %s", string_view{room_id}, json::get<"sender"_>(event), string_view{event.event_id}, string_view{alias}, }; } catch(const std::exception &e) { log::error { m::log, "Updating aliases of %s by %s in %s with %s :%s", string_view{room_id}, json::get<"sender"_>(event), string_view{event.event_id}, string_view{alias}, e.what(), }; } if(m::join_rule(room_id, "public")) rooms::summary::set(room_id); } // // auth handler // decltype(ircd::m::auth_room_aliases_hookfn) ircd::m::auth_room_aliases_hookfn { auth_room_aliases, { { "_site", "room.auth" }, { "type", "m.room.aliases" }, } }; void ircd::m::auth_room_aliases(const event &event, room::auth::hookdata &data) { using FAIL = m::room::auth::FAIL; using conforms = m::event::conforms; // 4. If type is m.room.aliases: assert(json::get<"type"_>(event) == "m.room.aliases"); // a. If event has no state_key, reject. if(empty(json::get<"state_key"_>(event))) throw FAIL { "m.room.aliases event is missing a state_key." }; // b. If sender's domain doesn't matches state_key, reject. if(json::get<"state_key"_>(event) != m::user::id(json::get<"sender"_>(event)).host()) throw FAIL { "m.room.aliases event state_key is not the sender's domain." }; // c. Otherwise, allow data.allow = true; } // // m::room::aliases // bool IRCD_MODULE_EXPORT ircd::m::room::aliases::for_each(const m::room &room, const string_view &server, const closure_bool &closure) { const room::state state { room }; assert(server); const event::idx &event_idx { state.get(std::nothrow, "m.room.aliases", server) }; if(!event_idx) return true; bool ret{true}; m::get(std::nothrow, event_idx, "content", [&closure, &ret] (const json::object &content) { const json::array &aliases { content["aliases"] }; for(auto it(begin(aliases)); it != end(aliases) && ret; ++it) { const json::string &alias(*it); if(!valid(m::id::ROOM_ALIAS, alias)) continue; if(!closure(alias)) ret = false; } }); return ret; } // // m::room::aliases::cache // bool IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::del(const alias &alias) { char buf[m::id::room_alias::buf::SIZE]; const string_view &key { make_key(buf, alias) }; const auto &event_idx { alias_room.get(std::nothrow, "ircd.room.alias", key) }; if(!event_idx) return false; const auto event_id { m::event_id(event_idx, std::nothrow) }; if(!event_id) return false; const auto ret { redact(alias_room, m::me.user_id, event_id, "deleted") }; return true; } bool IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::set(const alias &alias, const id &id) { char buf[m::id::room_alias::buf::SIZE]; const string_view &key { make_key(buf, alias) }; const auto ret { send(alias_room, m::me.user_id, "ircd.room.alias", key, { { "room_id", id } }) }; return true; } bool IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::get(std::nothrow_t, const alias &alias, const id::closure &closure) { m::event::idx event_idx { getidx(alias) }; if(!event_idx) { if(my_host(alias.host())) return false; if(!fetch(std::nothrow, alias, alias.host())) return false; event_idx = getidx(alias); } const bool expired { !my_host(alias.host()) && cache::expired(event_idx) }; if(expired) { log::dwarning { log, "Cached alias %s expired age:%ld ttl:%ld", string_view{alias}, cache::age(event_idx).count(), milliseconds(seconds(alias_cache_ttl)).count(), }; fetch(std::nothrow, alias, alias.host()); event_idx = getidx(alias); } if(!event_idx) return false; bool ret{false}; m::get(std::nothrow, event_idx, "content", [&closure, &ret] (const json::object &content) { const json::string &room_id { content.get("room_id") }; if(!empty(room_id)) { ret = true; closure(room_id); } }); return ret; } namespace ircd::m { thread_local char room_aliases_cache_fetch_hpbuf[384]; } void IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::fetch(const alias &alias, const net::hostport &hp) try { const unique_buffer buf { 16_KiB }; m::v1::query::opts opts; opts.remote = hp; opts.dynamic = true; m::v1::query::directory request { alias, buf, std::move(opts) }; request.wait(seconds(alias_fetch_timeout)); const http::code &code { request.get() }; const json::object response { request }; if(!response.has("room_id")) throw m::NOT_FOUND { "Server '%s' does not know room_id for %s", string(room_aliases_cache_fetch_hpbuf, hp), string_view{alias}, }; const m::room::id &room_id { unquote(response["room_id"]) }; set(alias, room_id); } catch(const ctx::timeout &e) { throw m::error { http::GATEWAY_TIMEOUT, "M_ROOM_ALIAS_TIMEOUT", "Server '%s' did not respond with a room_id for %s in time", string(room_aliases_cache_fetch_hpbuf, hp), string_view{alias}, }; } catch(const server::unavailable &e) { throw m::error { http::BAD_GATEWAY, "M_ROOM_ALIAS_UNAVAILABLE", "Server '%s' is not available to query a room_id for %s", string(room_aliases_cache_fetch_hpbuf, hp), string_view{alias}, }; } bool IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::for_each(const string_view &server, const closure_bool &closure) { const m::room::state state { alias_room }; bool ret{true}; const m::room::state::closure_bool reclosure{[&server, &closure, &ret] (const string_view &type, const string_view &state_key, const m::event::idx &event_idx) { thread_local char swapbuf[m::id::room_alias::buf::SIZE]; const alias &alias { m::id::unswap(state_key, swapbuf) }; if(server && alias.host() != server) return false; if(expired(event_idx)) return true; m::get(std::nothrow, event_idx, "content", [&closure, &ret, &alias] (const json::object &content) { const json::string &room_id { content.get("room_id") }; if(!empty(room_id)) ret = closure(alias, room_id); }); return ret; }}; state.for_each("ircd.room.alias", server, reclosure); return ret; } bool IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::has(const alias &alias) { const auto &event_idx { getidx(alias) }; if(!event_idx) return false; if(expired(event_idx)) return false; bool ret{false}; m::get(std::nothrow, event_idx, "content", [&ret] (const json::object &content) { const json::string &room_id { content.get("room_id") }; ret = !empty(room_id); }); return ret; } ircd::system_point IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::expires(const alias &alias) { const auto event_idx { getidx(alias) }; if(!event_idx) return system_point::min(); const milliseconds age { cache::age(event_idx) }; const seconds ttl { alias_cache_ttl }; return now() + (ttl - age); } bool IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::expired(const event::idx &event_idx) { const milliseconds age { cache::age(event_idx) }; const seconds ttl { alias_cache_ttl }; return age > ttl; } ircd::milliseconds IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::age(const event::idx &event_idx) { time_t ts; if(!m::get(event_idx, "origin_server_ts", ts)) return milliseconds::max(); const time_t now { ircd::time() }; return milliseconds { now - ts }; } ircd::m::event::idx IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::getidx(const alias &alias) { thread_local char swapbuf[m::id::room_alias::buf::SIZE]; const string_view &swapped { alias.swap(swapbuf) }; char buf[m::id::room_alias::buf::SIZE]; const string_view &key { tolower(buf, swapped) }; const auto &event_idx { alias_room.get(std::nothrow, "ircd.room.alias", key) }; return event_idx; } ircd::string_view IRCD_MODULE_EXPORT ircd::m::room::aliases::cache::make_key(const mutable_buffer &out, const alias &alias) { thread_local char swapbuf[m::id::room_alias::buf::SIZE] alignas(16); const string_view &swapped { alias.swap(swapbuf) }; const string_view &key { tolower(out, swapped) }; return key; }