// 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 { std::unique_ptr _self; std::unique_ptr _dbs; std::unique_ptr _modules; static void on_load(); static void on_unload() noexcept; } ircd::mapi::header IRCD_MODULE { "Matrix Chat Protocol", ircd::m::on_load, ircd::m::on_unload, }; IRCD_MODULE_EXPORT_DATA decltype(ircd::m::log) ircd::m::log { "m", 'm' }; // // init // ircd::conf::item me_online_status_msg { { "name", "ircd.me.online.status_msg" }, { "default", "Wanna chat? IRCd at your service!" } }; ircd::conf::item me_offline_status_msg { { "name", "ircd.me.offline.status_msg" }, { "default", "Catch ya on the flip side..." } }; void ircd::m::on_load() try { _self = std::make_unique(); _dbs = std::make_unique(ircd::server_name, std::string{}); _modules = std::make_unique(); if(!ircd::write_avoid && vm::sequence::retired != 0) presence::set(me, "online", me_online_status_msg); } catch(const m::error &e) { log::error { log, "Failed to start matrix (%u) %s :%s :%s", uint(e.code), http::status(e.code), e.errcode(), e.errstr(), }; throw; } catch(const std::exception &e) { log::error { log, "Failed to start matrix :%s", e.what() }; throw; } void ircd::m::on_unload() noexcept try { mods::imports.erase("m_listen"s); if(m::sync::pool.size()) m::sync::pool.join(); if(!std::uncaught_exceptions() && !ircd::write_avoid) presence::set(me, "offline", me_offline_status_msg); _modules.reset(nullptr); _dbs.reset(nullptr); _self.reset(nullptr); //TODO: remove this for non-interfering shutdown server::interrupt_all(); client::terminate_all(); client::close_all(); server::close_all(); server::wait_all(); client::wait_all(); } catch(const m::error &e) { log::critical { log, "%s %s", e.what(), e.content }; ircd::terminate(); } // // init::modules // namespace ircd::m { extern const std::vector module_names; extern const std::vector module_names_optional; } ircd::m::init::modules::modules() { const unwind::exceptional unload{[this] { this->fini_imports(); }}; init_imports(); } ircd::m::init::modules::~modules() noexcept { fini_imports(); } void ircd::m::init::modules::init_imports() { if(!bool(ircd::mods::autoload)) { log::warning { "Not loading modules because noautomod flag is set. " "You may still load modules manually." }; return; } for(const auto &name : module_names) try { mods::imports.emplace(name, name); } catch(...) { const auto &optional(module_names_optional); if(std::count(begin(optional), end(optional), name)) continue; throw; } if(vm::sequence::retired == 0) { log::notice { log, "This appears to be your first time running IRCd because the events " "database is empty. I will be bootstrapping it with initial events now..." }; m::init::bootstrap{}; } } void ircd::m::init::modules::fini_imports() noexcept { for(auto it(module_names.rbegin()); it != module_names.rend(); ++it) mods::imports.erase(*it); } /// This is an ordered list for loading and unloading modules. This is not the /// solution I really want at all so consider it temporary. Modules are loaded /// in the order of the lines and unloaded in reverse order. decltype(ircd::m::module_names) ircd::m::module_names { "m_node", "m_keys", "m_event", "m_fetch", "m_feds", "m_room", "m_room_events", "m_room_auth", "m_room_head", "m_room_bootstrap", "m_room_join", "m_room_leave", "m_room_aliases", "m_room_canonical_alias", "m_room_create", "m_room_history_visibility", "m_room_join_rules", "m_room_member", "m_room_name", "m_room_third_party_invite", "m_room_message", "m_room_power_levels", "m_room_server_acl", "m_room_redaction", "m_user", "m_user_filter", "m_user_rooms", "m_user_mitsein", "m_user_servers", "m_user_events", "m_user_highlight", "m_user_profile", "m_user_account_data", "m_user_room_account_data", "m_user_room_tags", "m_user_register", "m_events", "m_rooms", "m_rooms_summary", "m_users", "m_presence", "m_receipt", "m_typing", "m_device_list_update", "m_device", "m_direct", "m_direct_to_device", "m_breadcrumb_rooms", "m_ignored_user_list", "m_command", "m_control", "m_create", "m_profile", "m_noop", "m_event_append", "m_event_horizon", "m_event_pretty", "conf", "net_dns", "key_query", "key_server", "identity_pubkey", "identity_v1", "media_media", "federation_backfill_ids", "federation_backfill", "federation_event_auth", "federation_event", "federation_get_groups_publicised", "federation_get_missing_events", "federation_invite", "federation_invite2", "federation_make_join", "federation_make_leave", "federation_publicrooms", "federation_query_auth", "federation_query", "federation_sender", "federation_send_join", "federation_send_leave", "federation_send", "federation_state_ids", "federation_state", "federation_user_devices", "federation_user_keys_claim", "federation_user_keys_query", "federation_version", "client_user", "client_rooms", "client_createroom", "client_join", "client_account", "client_profile", "client_notifications", "client_devices", "client_delete_devices", "client_send_to_device", "client_keys_changes", "client_keys_upload", "client_keys_claim", "client_keys_query", "client_presence", "client_joined_groups", "client_publicised_groups", "client_login", "client_logout", "client_register_available", "client_register_email", "client_register", "client_directory_list_appservice", "client_directory_list_room", "client_directory_room", "client_directory_user", "client_publicrooms", "client_search", "client_pushers", "client_pushrules", "client_events", "client_initialsync", "client_sync", "client_sync_account_data", "client_sync_device_lists", "client_sync_device_one_time_keys_count", "client_sync_presence", "client_sync_to_device", "client_sync_rooms_account_data", "client_sync_rooms_ephemeral_receipt", "client_sync_rooms_ephemeral", "client_sync_rooms_ephemeral_typing", "client_sync_rooms", "client_sync_rooms_state", "client_sync_rooms_timeline", "client_sync_rooms_unread_notifications", "client_sync_rooms_summary", "client_voip_turnserver", "client_thirdparty_protocols", "client_versions", "client_capabilities", "well_known", "web_root", "web_hook", "stats", "m_vm_fetch", "m_vm", "m_init_backfill", "m_listen", }; /// This is a list of modules that are considered "optional" and any loading /// error for them will not propagate and interrupt m::init. decltype(ircd::m::module_names_optional) ircd::m::module_names_optional { "web_hook", }; /////////////////////////////////////////////////////////////////////////////// // // m/self.h // std::string ircd::m::self::origin {}; std::string ircd::m::self::servername {}; ircd::ed25519::sk ircd::m::self::secret_key {}; ircd::ed25519::pk ircd::m::self::public_key {}; std::string ircd::m::self::public_key_b64 {}; std::string ircd::m::self::public_key_id {}; std::string ircd::m::self::tls_cert_der {}; std::string ircd::m::self::tls_cert_der_sha256_b64 {}; // // my user // ircd::m::user::id::buf ircd_user_id { "ircd", "localhost" // gets replaced after conf init }; ircd::m::user ircd::m::me { ircd_user_id }; // // my room // ircd::m::room::id::buf ircd_room_id { "ircd", "localhost" // replaced after conf init }; ircd::m::room ircd::m::my_room { ircd_room_id }; // // my node // std::array ircd_node_id { "localhost" // replaced after conf init }; ircd::m::node ircd::m::my_node { ircd_node_id }; bool ircd::m::self::host(const string_view &other) { // port() is 0 when the origin has no port (and implies 8448) const auto port { me.user_id.port() }; // If my_host has a port number, then the argument must also have the // same port number. if(port) return host() == other; /// If my_host has no port number, then the argument can have port /// 8448 or no port number, which will initialize net::hostport.port to /// the "canon_port" of 8448. assert(net::canon_port == 8448); const net::hostport _other{other}; if(net::port(_other) != net::canon_port) return false; if(host() != host(_other)) return false; return true; } ircd::string_view ircd::m::self::host() { return me.user_id.host(); } // // init // //TODO: XXX extern ircd::m::room::id::buf users_room_id; extern ircd::m::room::id::buf tokens_room_id; ircd::m::self::init::init() try { self::origin = string_view{ircd::network_name}; self::servername = string_view{ircd::server_name}; // Sanity check that these are valid hostname strings. This was likely // already checked, so these validators will simply throw without very // useful error messages if invalid strings ever make it this far. rfc3986::valid_host(origin); rfc3986::valid_host(servername); m::my_node = string_view{strlcpy { ircd_node_id, origin }}; ircd_user_id = {"ircd", origin}; m::me = {ircd_user_id}; ircd_room_id = {"ircd", origin}; m::my_room = {ircd_room_id}; tokens_room_id = {"tokens", origin}; m::user::tokens = {tokens_room_id}; if(origin == "localhost") log::warning { "The origin is configured or has defaulted to 'localhost'" }; // Loading the keys module in runlevel::START will do further // inits of m::self::globals. Calling the inits directly from // here makes the module dependent on libircd and unloadable. assert(ircd::run::level == run::level::START); mods::imports.emplace("m_keys"s, "m_keys"s); } catch(const std::exception &e) { log::critical { m::log, "Failed to init self origin[%s] servername[%s]", origin, servername, }; throw; } /////////////////////////////////////////////////////////////////////////////// // // m/sync.h // decltype(ircd::m::sync::log) ircd::m::sync::log { "m.sync", 's' }; namespace ircd::m::sync { const ctx::pool::opts pool_opts { ctx::DEFAULT_STACK_SIZE, 0, -1, 0 }; } decltype(ircd::m::sync::pool) ircd::m::sync::pool { "m.sync", pool_opts }; decltype(ircd::m::sync::stats_info) ircd::m::sync::stats_info { { "name", "ircd.m.sync.stats.info" }, { "default", false }, }; template<> decltype(ircd::util::instance_multimap>::map) ircd::util::instance_multimap>::map {}; template<> decltype(ircd::util::instance_list::allocator) ircd::util::instance_list::allocator {}; template<> decltype(ircd::util::instance_list::list) ircd::util::instance_list::list { allocator }; bool ircd::m::sync::for_each(const item_closure_bool &closure) { auto it(begin(item::map)); for(; it != end(item::map); ++it) if(!closure(*it->second)) return false; return true; } bool ircd::m::sync::for_each(const string_view &prefix, const item_closure_bool &closure) { const auto depth { token_count(prefix, '.') }; auto it { item::map.lower_bound(prefix) }; for(; it != end(item::map); ++it) { const auto item_depth { token_count(it->first, '.') }; if(item_depth > depth + 1) continue; if(it->first == prefix) continue; if(item_depth < depth + 1) break; if(!closure(*it->second)) return false; } return true; } bool ircd::m::sync::apropos(const data &d, const event &event) { return apropos(d, index(event, std::nothrow)); } bool ircd::m::sync::apropos(const data &d, const event::id &event_id) { return apropos(d, index(event_id, std::nothrow)); } bool ircd::m::sync::apropos(const data &d, const event::idx &event_idx) { return d.phased || (event_idx >= d.range.first && event_idx < d.range.second); } ircd::string_view ircd::m::sync::loghead(const data &data) { thread_local char headbuf[256], rembuf[128], iecbuf[2][64], tmbuf[32]; const auto remstr { data.client? string(rembuf, ircd::remote(*data.client)): string_view{} }; const auto flush_bytes { data.stats? data.stats->flush_bytes: 0U }; const auto flush_count { data.stats? data.stats->flush_count: 0U }; const auto tmstr { data.stats? ircd::pretty(tmbuf, data.stats->timer.at(), true): string_view{} }; return fmt::sprintf { headbuf, "%s %s %ld:%lu|%lu%s chunk:%zu sent:%s of %s in %s", remstr, string_view{data.user.user_id}, data.range.first, data.range.second, vm::sequence::retired, data.phased? "|P"_sv : ""_sv, flush_count, ircd::pretty(iecbuf[1], iec(flush_bytes)), data.out? ircd::pretty(iecbuf[0], iec(flush_bytes + size(data.out->completed()))): string_view{}, tmstr }; } // // item // // // item::item // ircd::m::sync::item::item(std::string name, handle polylog, handle linear, const json::members &feature) :instance_multimap { std::move(name) } ,conf_name { fmt::snstringf{128, "ircd.m.sync.%s.enable", this->name()}, fmt::snstringf{128, "ircd.m.sync.%s.stats.debug", this->name()}, } ,enable { { "name", conf_name[0] }, { "default", true }, } ,stats_debug { { "name", conf_name[1] }, { "default", false }, } ,_polylog { std::move(polylog) } ,_linear { std::move(linear) } ,feature { feature } ,opts { this->feature } ,phased { opts.get("phased", false) } { log::debug { log, "Registered sync item(%p) '%s' (%zu features)", this, this->name(), opts.size(), }; } ircd::m::sync::item::~item() noexcept { log::debug { log, "Unregistered sync item(%p) '%s'", this, this->name() }; } bool ircd::m::sync::item::polylog(data &data) try { // Skip the item if disabled by configuration if(!enable) return false; if(data.phased && !phased && int64_t(data.range.first) < 0) return false; #ifdef RB_DEBUG sync::stats stats { data.stats && (stats_info || stats_debug)? *data.stats: sync::stats{} }; if(data.stats && (stats_info || stats_debug)) stats.timer = {}; #endif const bool ret { _polylog(data) }; #ifdef RB_DEBUG if(data.stats && (stats_info || stats_debug)) { //data.out.flush(); thread_local char tmbuf[32]; log::debug { log, "polylog %s commit:%b '%s' %s", loghead(data), ret, name(), ircd::pretty(tmbuf, stats.timer.at(), true) }; } #endif this_ctx::interruption_point(); return ret; } catch(const std::bad_function_call &e) { log::dwarning { log, "polylog %s '%s' missing handler :%s", loghead(data), name(), e.what() }; return false; } catch(const m::error &e) { log::derror { log, "polylog %s '%s' :%s %s", loghead(data), name(), e.what(), e.content }; return false; } catch(const std::exception &e) { log::derror { log, "polylog %s '%s' :%s", loghead(data), name(), e.what() }; throw; } bool ircd::m::sync::item::linear(data &data) try { if(!enable) return false; return _linear(data); } catch(const std::bad_function_call &e) { thread_local char rembuf[128]; log::dwarning { log, "linear %s '%s' missing handler :%s", loghead(data), name(), e.what() }; return false; } catch(const m::error &e) { log::derror { log, "linear %s '%s' :%s %s", loghead(data), name(), e.what(), e.content }; return false; } catch(const std::exception &e) { log::derror { log, "linear %s '%s' :%s", loghead(data), name(), e.what() }; throw; } size_t ircd::m::sync::item::children() const { size_t ret(0); sync::for_each(this->name(), [&ret] (auto &item) { ++ret; return true; }); return ret; } ircd::string_view ircd::m::sync::item::member_name() const { return token_last(name(), '.'); } ircd::string_view ircd::m::sync::item::name() const { return this->instance_multimap::it->first; } /////////////////////////////////////////////////////////////////////////////// // // m/app.h // decltype(ircd::m::app::log) ircd::m::app::log { "m.app" }; std::string ircd::m::app::config::get(const string_view &id) { using prototype = std::string (const string_view &); static mods::import call { "app_app", "ircd::m::app::config::get" }; return call(id); } std::string ircd::m::app::config::get(std::nothrow_t, const string_view &id) { using prototype = std::string (std::nothrow_t, const string_view &); static mods::import call { "app_app", "ircd::m::app::config::get" }; return call(std::nothrow, id); } void ircd::m::app::config::get(const string_view &id, const event::fetch::view_closure &closure) { using prototype = void (const string_view &, const event::fetch::view_closure &); static mods::import call { "app_app", "ircd::m::app::config::get" }; return call(id, closure); } bool ircd::m::app::config::get(std::nothrow_t, const string_view &id, const event::fetch::view_closure &closure) { using prototype = bool (std::nothrow_t, const string_view &, const event::fetch::view_closure &); static mods::import call { "app_app", "ircd::m::app::config::get" }; return call(std::nothrow, id, closure); } ircd::m::event::idx ircd::m::app::config::idx(const string_view &id) { using prototype = event::idx (const string_view &); static mods::import call { "app_app", "ircd::m::app::config::idx" }; return call(id); } ircd::m::event::idx ircd::m::app::config::idx(std::nothrow_t, const string_view &id) { using prototype = event::idx (std::nothrow_t, const string_view &); static mods::import call { "app_app", "ircd::m::app::config::idx" }; return call(std::nothrow, id); } bool ircd::m::app::exists(const string_view &id) { using prototype = bool (const string_view &); static mods::import call { "app_app", "exists" }; return call(id); } /////////////////////////////////////////////////////////////////////////////// // // m/feds.h // ircd::m::feds::execute::execute(const opts &o, const closure &c) :execute { vector_view{&o, 1}, c } { } ircd::m::feds::execute::execute(const vector_view &o, const closure &c) { using prototype = bool (const vector_view &, const closure &); static mods::import call { "m_feds", "ircd::m::feds::execute" }; call(o, c); } /////////////////////////////////////////////////////////////////////////////// // // m/vm.h // decltype(ircd::m::vm::default_opts) ircd::m::vm::default_opts; decltype(ircd::m::vm::default_copts) ircd::m::vm::default_copts; decltype(ircd::m::vm::log) ircd::m::vm::log { "m.vm", 'v' }; decltype(ircd::m::vm::dock) ircd::m::vm::dock; decltype(ircd::m::vm::ready) ircd::m::vm::ready; ircd::string_view ircd::m::vm::loghead(const eval &eval) { thread_local char buf[128]; return loghead(buf, eval); } ircd::string_view ircd::m::vm::loghead(const mutable_buffer &buf, const eval &eval) { return fmt::sprintf { buf, "vm:%lu:%lu:%lu eval:%lu seq:%lu share:%lu:%lu %s", sequence::uncommitted, sequence::committed, sequence::retired, eval.id, sequence::get(eval), eval.sequence_shared[0], eval.sequence_shared[1], eval.event_? string_view{eval.event_->event_id}: ""_sv, }; } ircd::http::code ircd::m::vm::http_code(const fault &code) { switch(code) { case fault::ACCEPT: return http::OK; case fault::EXISTS: return http::CONFLICT; case fault::INVALID: return http::BAD_REQUEST; case fault::GENERAL: return http::UNAUTHORIZED; case fault::AUTH: return http::FORBIDDEN; case fault::STATE: return http::NOT_FOUND; case fault::EVENT: return http::NOT_FOUND; default: return http::INTERNAL_SERVER_ERROR; } } ircd::string_view ircd::m::vm::reflect(const enum fault &code) { switch(code) { case fault::ACCEPT: return "#ACCEPT"; case fault::EXISTS: return "#EXISTS"; case fault::GENERAL: return "#GENERAL"; case fault::INVALID: return "#INVALID"; case fault::AUTH: return "#AUTH"; case fault::EVENT: return "#EVENT"; case fault::STATE: return "#STATE"; case fault::INTERRUPT: return "#INTERRUPT"; } return "??????"; } // // Eval // // Processes any event from any place from any time and does whatever is // necessary to validate, reject, learn from new information, ignore old // information and advance the state of IRCd as best as possible. /// Instance list linkage for all of the evaluations. template<> decltype(ircd::util::instance_list::allocator) ircd::util::instance_list::allocator {}; template<> decltype(ircd::util::instance_list::list) ircd::util::instance_list::list { allocator }; decltype(ircd::m::vm::eval::id_ctr) ircd::m::vm::eval::id_ctr; decltype(ircd::m::vm::eval::executing) ircd::m::vm::eval::executing; decltype(ircd::m::vm::eval::injecting) ircd::m::vm::eval::injecting; void ircd::m::vm::eval::seqsort() { eval::list.sort([] (const auto *const &a, const auto *const &b) { if(sequence::get(*a) == 0) return false; if(sequence::get(*b) == 0) return true; return sequence::get(*a) < sequence::get(*b); }); } ircd::m::vm::eval * ircd::m::vm::eval::seqmin() { const auto it { std::min_element(begin(eval::list), end(eval::list), [] (const auto *const &a, const auto *const &b) { if(sequence::get(*a) == 0) return false; if(sequence::get(*b) == 0) return true; return sequence::get(*a) < sequence::get(*b); }) }; if(it == end(eval::list)) return nullptr; if(sequence::get(**it) == 0) return nullptr; return *it; } ircd::m::vm::eval * ircd::m::vm::eval::seqmax() { const auto it { std::max_element(begin(eval::list), end(eval::list), [] (const auto *const &a, const auto *const &b) { return sequence::get(*a) < sequence::get(*b); }) }; if(it == end(eval::list)) return nullptr; if(sequence::get(**it) == 0) return nullptr; return *it; } ircd::m::vm::eval * ircd::m::vm::eval::seqnext(const uint64_t &seq) { eval *ret{nullptr}; for(auto *const &eval : eval::list) { if(sequence::get(*eval) <= seq) continue; if(!ret || sequence::get(*eval) < sequence::get(*ret)) ret = eval; } assert(!ret || sequence::get(*ret) > seq); return ret; } bool ircd::m::vm::eval::sequnique(const uint64_t &seq) { return 1 == std::count_if(begin(eval::list), end(eval::list), [&seq] (const auto *const &eval) { return sequence::get(*eval) == seq; }); } ircd::m::vm::eval & ircd::m::vm::eval::get(const event::id &event_id) { auto *const ret { find(event_id) }; if(unlikely(!ret)) throw std::out_of_range { "eval::get(): event_id not being evaluated." }; return *ret; } ircd::m::vm::eval * ircd::m::vm::eval::find(const event::id &event_id) { eval *ret{nullptr}; for_each([&event_id, &ret](eval &e) { if(e.event_) { if(e.event_->event_id == event_id) ret = &e; } else if(e.issue) { if(e.issue->has("event_id")) if(string_view{e.issue->at("event_id")} == event_id) ret = &e; } else if(e.event_id == event_id) ret = &e; return ret == nullptr; }); return ret; } size_t ircd::m::vm::eval::count(const event::id &event_id) { size_t ret(0); for_each([&event_id, &ret](eval &e) { if(e.event_) { if(e.event_->event_id == event_id) ++ret; } else if(e.issue) { if(e.issue->has("event_id")) if(string_view{e.issue->at("event_id")} == event_id) ++ret; } else if(e.event_id == event_id) ++ret; return true; }); return ret; } size_t ircd::m::vm::eval::count(const ctx::ctx *const &c) { return std::count_if(begin(eval::list), end(eval::list), [&c] (const eval *const &eval) { return eval->ctx == c; }); } bool ircd::m::vm::eval::for_each(const std::function &closure) { for(eval *const &eval : eval::list) if(!closure(*eval)) return false; return true; } bool ircd::m::vm::eval::for_each(const ctx::ctx *const &c, const std::function &closure) { for(eval *const &eval : eval::list) if(eval->ctx == c) if(!closure(*eval)) return false; return true; } // // eval::eval // ircd::m::vm::eval::eval(json::iov &event, const json::iov &content, const vm::copts &opts) :eval{opts} { operator()(event, content); } ircd::m::vm::eval::eval(const event &event, const vm::opts &opts) :eval{opts} { operator()(event); } ircd::m::vm::eval::eval(const json::array &pdus, const vm::opts &opts) :opts{&opts} { // Sort the events first to avoid complicating the evals; the events might // be from different rooms but it doesn't matter. std::vector events(begin(pdus), end(pdus)); std::sort(begin(events), end(events)); this->pdus = events; // Conduct each eval without letting any one exception ruin things for the // others, including an interrupt. The only exception is a termination. for(auto it(begin(events)); it != end(events); ++it) try { auto &event{*it}; // If we set the event_id in the event instance we have to unset // it so other contexts don't see an invalid reference. const unwind event_id{[&event] { event.event_id = json::get<"event_id"_>(event)? event.event_id: m::event::id{}; }}; // We have to set the event_id in the event instance if it didn't come // with the event JSON. if(!opts.edu && !event.event_id) event.event_id = opts.room_version == "3"? event::id{event::id::v3{this->event_id, event}}: event::id{event::id::v4{this->event_id, event}}; // When a fault::EXISTS would not actually be revealed to the user in // any way we can elide a lot of grief by checking this here first and // skipping the event. The query path will be adequately cached anyway. if(~(opts.warnlog | opts.errorlog) & fault::EXISTS) if(event.event_id && m::exists(event.event_id)) continue; operator()(event); } catch(const ctx::interrupted &e) { if(opts.nothrows & fault::INTERRUPT) continue; else throw; } catch(const std::exception &e) { continue; } catch(const ctx::terminated &) { throw; } } ircd::m::vm::eval::eval(const vm::copts &opts) :opts{&opts} ,copts{&opts} { } ircd::m::vm::eval::eval(const vm::opts &opts) :opts{&opts} { } ircd::m::vm::eval::~eval() noexcept { } ircd::m::vm::eval::operator const event::id::buf &() const { return event_id; } const ircd::m::event * ircd::m::vm::eval::find_pdu(const event::id &event_id) { const m::event *ret{nullptr}; for_each_pdu([&ret, &event_id] (const m::event &event) { if(event.event_id != event_id) return true; ret = std::addressof(event); return false; }); return ret; } const ircd::m::event * ircd::m::vm::eval::find_pdu(const eval &eval, const event::id &event_id) { const m::event *ret{nullptr}; for(const auto &event : eval.pdus) { if(event.event_id != event_id) continue; ret = std::addressof(event); break; } return ret; } bool ircd::m::vm::eval::for_each_pdu(const std::function &closure) { return for_each([&closure](eval &e) { if(!empty(e.pdus)) { for(const auto &pdu : e.pdus) if(!closure(pdu)) return false; } else if(e.event_) { if(!closure(*e.event_)) return false; } return true; }); } /// /// Figure 1: /// in . <-- injection /// ===:::::::==// /// | ||||||| // <-- these functions /// | \\|// //| /// | ||| // | | acceleration /// | |||// | | /// | |||/ | | /// | ||| | V /// | !!! | /// | * | <----- nozzle /// | ///|||\\\ | /// |/|/|/|\|\|\| <---- propagation cone /// _/|/|/|/|\|\|\|\_ /// out /// /// Inject a new event originating from this server. /// enum ircd::m::vm::fault ircd::m::vm::eval::operator()(json::iov &event, const json::iov &contents) { using prototype = fault (eval &, json::iov &, const json::iov &); static mods::import call { "m_vm", "ircd::m::vm::inject" }; vm::dock.wait([] { return vm::ready; }); return call(*this, event, contents); } enum ircd::m::vm::fault ircd::m::vm::eval::operator()(const event &event) { using prototype = fault (eval &, const m::event &); static mods::import call { "m_vm", "ircd::m::vm::execute" }; vm::dock.wait([] { return vm::ready; }); return call(*this, event); } // // sequence // decltype(ircd::m::vm::sequence::dock) ircd::m::vm::sequence::dock; decltype(ircd::m::vm::sequence::retired) ircd::m::vm::sequence::retired; decltype(ircd::m::vm::sequence::committed) ircd::m::vm::sequence::committed; decltype(ircd::m::vm::sequence::uncommitted) ircd::m::vm::sequence::uncommitted; uint64_t ircd::m::vm::sequence::min() { const auto *const e { eval::seqmin() }; return e? get(*e) : 0; } uint64_t ircd::m::vm::sequence::max() { const auto *const e { eval::seqmax() }; return e? get(*e) : 0; } uint64_t ircd::m::vm::sequence::get(id::event::buf &event_id) { static constexpr auto column_idx { json::indexof() }; auto &column { dbs::event_column.at(column_idx) }; const auto it { column.rbegin() }; if(!it) { // If this iterator is invalid the events db should // be completely fresh. assert(db::sequence(*dbs::events) == 0); return 0; } const auto &ret { byte_view(it->first) }; event_id = it->second; return ret; } const uint64_t & ircd::m::vm::sequence::get(const eval &eval) { return eval.sequence; } /////////////////////////////////////////////////////////////////////////////// // // m/redacted.h // ircd::m::redacted::redacted(const event::idx &event_idx) :boolean { event_idx? event::refs(event_idx).has(dbs::ref::M_ROOM_REDACTION): false } { } bool ircd::m::redacted::prefetch(const event::idx &event_idx) { const event::refs refs { event_idx }; return refs.prefetch(dbs::ref::M_ROOM_REDACTION); } /////////////////////////////////////////////////////////////////////////////// // // m/presence.h // ircd::m::presence::presence(const user &user, const mutable_buffer &buf) :edu::m_presence{[&user, &buf] { json::object ret; get(user, [&ret, &buf] (const json::object &content) { ret = { data(buf), copy(buf, string_view{content}) }; }); return ret; }()} { } ircd::m::event::id::buf ircd::m::presence::set(const user &user, const string_view &presence, const string_view &status_msg) { return set(m::presence { { "user_id", user.user_id }, { "presence", presence }, { "status_msg", status_msg }, { "currently_active", presence == "online" }, }); } ircd::m::event::id::buf ircd::m::presence::set(const presence &object) { using prototype = event::id::buf (const presence &); static mods::import function { "m_presence", "ircd::m::presence::set" }; return function(object); } void ircd::m::presence::get(const user &user, const closure &closure) { if(!get(std::nothrow, user, closure)) throw m::NOT_FOUND { "No presence found for %s", string_view{user.user_id} }; } bool ircd::m::presence::get(std::nothrow_t, const user &user, const closure &closure) { static const m::event::fetch::opts fopts { m::event::keys::include {"content"} }; const auto reclosure{[&closure] (const m::event &event) { closure(json::get<"content"_>(event)); }}; return get(std::nothrow, user, reclosure, &fopts); } bool ircd::m::presence::get(std::nothrow_t, const user &user, const closure_event &closure, const event::fetch::opts *const &opts) { using prototype = bool (std::nothrow_t, const m::user &, const closure_event &, const event::fetch::opts *const &); static mods::import function { "m_presence", "ircd::m::presence::get" }; return function(std::nothrow, user, closure, opts); } ircd::m::event::idx ircd::m::presence::get(const user &user) { const event::idx ret { get(std::nothrow, user) }; if(!ret) throw m::NOT_FOUND { "No presence found for %s", string_view{user.user_id} }; return ret; } ircd::m::event::idx ircd::m::presence::get(std::nothrow_t, const user &user) { using prototype = event::idx (std::nothrow_t, const m::user &); static mods::import function { "m_presence", "ircd::m::presence::get" }; return function(std::nothrow, user); } bool ircd::m::presence::valid_state(const string_view &state) { using prototype = bool (const string_view &); static mods::import function { "m_presence", "ircd::m::presence::valid_state" }; return function(state); } /////////////////////////////////////////////////////////////////////////////// // // m/device.h // bool ircd::m::device::set(const m::user &user, const device &device_) { using prototype = bool (const m::user &, const device &); static mods::import function { "m_device", "ircd::m::device::set" }; return function(user, device_); } bool ircd::m::device::set(const m::user &user, const string_view &id, const string_view &prop, const string_view &val) { using prototype = bool (const m::user &, const string_view &, const string_view &, const string_view &); static mods::import function { "m_device", "ircd::m::device::set" }; return function(user, id, prop, val); } bool ircd::m::device::del(const m::user &user, const string_view &id) { using prototype = bool (const m::user &, const string_view &); static mods::import function { "m_device", "ircd::m::device::del" }; return function(user, id); } bool ircd::m::device::has(const m::user &user, const string_view &id) { using prototype = bool (const m::user &, const string_view &id); static mods::import function { "m_device", "ircd::m::device::has" }; return function(user, id); } bool ircd::m::device::has(const m::user &user, const string_view &id, const string_view &prop) { using prototype = bool (const m::user &, const string_view &id, const string_view &prop); static mods::import function { "m_device", "ircd::m::device::has" }; return function(user, id, prop); } bool ircd::m::device::get(const m::user &user, const string_view &id, const string_view &prop, const closure &c) { const bool ret { get(std::nothrow, user, id, prop, c) }; if(!ret) throw m::NOT_FOUND { "Property '%s' for device '%s' for user %s not found", id, prop, string_view{user.user_id} }; return ret; } bool ircd::m::device::get(std::nothrow_t, const m::user &user, const string_view &id, const string_view &prop, const closure &c) { using prototype = bool (std::nothrow_t, const m::user &, const string_view &, const string_view &, const closure &); static mods::import function { "m_device", "ircd::m::device::get" }; return function(std::nothrow, user, id, prop, c); } bool ircd::m::device::for_each(const m::user &user, const string_view &id, const closure_bool &c) { using prototype = bool (const m::user &, const string_view &id, const closure_bool &); static mods::import function { "m_device", "ircd::m::device::for_each" }; return function(user, id, c); } bool ircd::m::device::for_each(const m::user &user, const closure_bool &c) { using prototype = bool (const m::user &, const closure_bool &); static mods::import function { "m_device", "ircd::m::device::for_each" }; return function(user, c); } /////////////////////////////////////////////////////////////////////////////// // // m/node.h // // // node // ircd::m::node::node(const string_view &node_id) :node_id{node_id} { rfc3986::valid_remote(node_id); } void ircd::m::node::key(const string_view &key_id, const ed25519_closure &closure) const { key(key_id, key_closure{[&closure] (const string_view &keyb64) { const ed25519::pk pk { [&keyb64](auto &buf) { b64decode(buf, unquote(keyb64)); } }; closure(pk); }}); } void ircd::m::node::key(const string_view &key_id, const key_closure &closure) const { using prototype = void (const string_view &, const string_view &, const keys::closure &); //TODO: Remove this import once this callsite is outside of libircd. static mods::import call { "m_keys", "ircd::m::keys::get" }; call(node_id, key_id, [&closure, &key_id] (const json::object &keys) { const json::object &vks { keys.at("verify_keys") }; const json::object &vkk { vks.at(key_id) }; const string_view &key { vkk.at("key") }; closure(key); }); } /// Generates a node-room ID into buffer; see room_id() overload. ircd::m::id::room::buf ircd::m::node::room_id() const { ircd::m::id::room::buf buf; return buf.assigned(room_id(buf)); } /// This generates a room mxid for the "node's room" essentially serving as /// a database mechanism for this specific node. This room_id is a hash of /// the node's full mxid. /// ircd::m::id::room ircd::m::node::room_id(const mutable_buffer &buf) const { assert(!empty(this->node_id)); // for compatibility with hashing legacy node_id's thread_local char node_id_buf[m::id::MAX_SIZE]; mutable_buffer mb{node_id_buf}; consume(mb, copy(mb, "::"_sv)); consume(mb, copy(mb, this->node_id)); const string_view &node_id { node_id_buf, data(mb) }; const sha256::buf hash { sha256{node_id} }; thread_local char b58_buf[b58encode_size(size(hash))]; const string_view b58 { b58encode(b58_buf, hash) }; return id::room { buf, b58, my_host() }; } // // node::room // ircd::m::node::room::room(const string_view &node_id) :room{m::node{node_id}} {} ircd::m::node::room::room(const m::node &node) :node{node} ,room_id{node.room_id()} { static_cast(*this) = room_id; } /////////////////////////////////////////////////////////////////////////////// // // m/filter.h // //TODO: globular expression //TODO: tribool for contains_url; we currently ignore the false value. bool ircd::m::match(const room_event_filter &filter, const event &event) { if(json::get<"contains_url"_>(filter) == true) if(!at<"content"_>(event).has("url")) return false; for(const auto &room_id : json::get<"not_rooms"_>(filter)) if(at<"room_id"_>(event) == unquote(room_id)) return false; if(empty(json::get<"rooms"_>(filter))) return match(event_filter{filter}, event); for(const auto &room_id : json::get<"rooms"_>(filter)) if(at<"room_id"_>(event) == unquote(room_id)) return match(event_filter{filter}, event); return false; } //TODO: globular expression bool ircd::m::match(const event_filter &filter, const event &event) { for(const auto &type : json::get<"not_types"_>(filter)) if(at<"type"_>(event) == unquote(type)) return false; for(const auto &sender : json::get<"not_senders"_>(filter)) if(at<"sender"_>(event) == unquote(sender)) return false; if(empty(json::get<"senders"_>(filter)) && empty(json::get<"types"_>(filter))) return true; if(empty(json::get<"senders"_>(filter))) { for(const auto &type : json::get<"types"_>(filter)) if(at<"type"_>(event) == unquote(type)) return true; return false; } if(empty(json::get<"types"_>(filter))) { for(const auto &sender : json::get<"senders"_>(filter)) if(at<"sender"_>(event) == unquote(sender)) return true; return false; } return true; } // // filter // /// Convenience interface for filters out of common `?filter=` query string /// arguments. This function expects a raw urlencoded value of the filter /// query parameter. It detects if the value is an "inline" filter by being /// a valid JSON object; otherwise it considers the value an ID and fetches /// the filter stored previously by the user. std::string ircd::m::filter::get(const string_view &val, const m::user &user) { if(!val) return {}; const bool is_inline { startswith(val, "{") || startswith(val, "%7B") }; if(is_inline) return util::string(val.size(), [&val] (const mutable_buffer &buf) { return url::decode(buf, val); }); if(!user.user_id) return {}; char idbuf[m::event::STATE_KEY_MAX_SIZE]; const string_view &id { url::decode(idbuf, val) }; const m::user::filter filter { user }; return filter.get(id); } // // filter::filter // ircd::m::filter::filter(const user &user, const string_view &filter_id, const mutable_buffer &buf) { const json::object &obj { user::filter(user).get(buf, filter_id) }; new (this) m::filter { obj }; } // // room_filter // ircd::m::room_filter::room_filter(const mutable_buffer &buf, const json::members &members) :super_type::tuple { json::stringify(mutable_buffer{buf}, members) } { } // // state_filter // ircd::m::state_filter::state_filter(const mutable_buffer &buf, const json::members &members) :super_type::tuple { json::stringify(mutable_buffer{buf}, members) } { } // // room_event_filter // ircd::m::room_event_filter::room_event_filter(const mutable_buffer &buf, const json::members &members) :super_type::tuple { json::stringify(mutable_buffer{buf}, members) } { } // // event_filter // ircd::m::event_filter::event_filter(const mutable_buffer &buf, const json::members &members) :super_type::tuple { json::stringify(mutable_buffer{buf}, members) } { } /////////////////////////////////////////////////////////////////////////////// // // m/membership.h // decltype(ircd::m::membership_positive) ircd::m::membership_positive { "join"_sv, "invite"_sv, }; decltype(ircd::m::membership_negative) ircd::m::membership_negative { "leave"_sv, "ban"_sv, ""_sv, }; bool ircd::m::membership(const room &room, const user::id &user_id, const vector_view &membership) { const room::state state { room }; const auto event_idx { state.get(std::nothrow, "m.room.member", user_id) }; return m::membership(event_idx, membership); } bool ircd::m::membership(const event::idx &event_idx, const vector_view &membership) { bool ret; const auto closure { [&membership, &ret](const json::object &content) { const json::string &content_membership { content["membership"] }; const auto it { std::find(begin(membership), end(membership), content_membership) }; ret = content_membership && it != end(membership); } }; // If the query was successful a membership state exists (even if the // string found was illegally empty) thus we must return the value of ret. if(m::get(std::nothrow, event_idx, "content", closure)) return ret; // If the user included an empty string in the vector, they want us to // return true for non-membership (i.e a failed query). static const auto &empty{[](auto&& s) { return !s; }}; if(std::any_of(begin(membership), end(membership), empty)) return true; // If the membership vector itself was empty that's also a non-membership. if(membership.empty()) return true; return false; } ircd::string_view ircd::m::membership(const mutable_buffer &out, const room &room, const user::id &user_id) { const room::state state { room }; const auto event_idx { state.get(std::nothrow, "m.room.member", user_id) }; return m::membership(out, event_idx); } ircd::string_view ircd::m::membership(const mutable_buffer &out, const event::idx &event_idx) { return m::query(std::nothrow, event_idx, "content", [&out] (const json::object &content) -> string_view { const json::string &content_membership { content["membership"] }; return strlcpy { out, content_membership }; }); } ircd::string_view ircd::m::membership(const event &event) { const json::object &content { json::get<"content"_>(event) }; const string_view &membership { json::get<"membership"_>(event) }; if(membership) return membership; const json::string &content_membership { content.get("membership") }; return content_membership; } /////////////////////////////////////////////////////////////////////////////// // // m/user.h // /// ID of the room which stores ephemeral tokens (an instance of the room is /// provided below). ircd::m::room::id::buf tokens_room_id { "tokens", ircd::my_host() }; /// The tokens room serves as a key-value lookup for various tokens to /// users, etc. It primarily serves to store access tokens for users. This /// is a separate room from the users room because in the future it may /// have an optimized configuration as well as being more easily cleared. /// ircd::m::room ircd::m::user::tokens { tokens_room_id }; bool ircd::m::exists(const user::id &user_id) { // The way we know a user exists is testing if their room exists. const m::user::room user_room { user_id }; return m::exists(user_room); } bool ircd::m::exists(const user &user) { return exists(user.user_id); } bool ircd::m::my(const user &user) { return my(user.user_id); } // // user // /// Generates a user-room ID into buffer; see room_id() overload. ircd::m::id::room::buf ircd::m::user::room_id() const { ircd::m::id::room::buf buf; return buf.assigned(room_id(buf)); } /// This generates a room mxid for the "user's room" essentially serving as /// a database mechanism for this specific user. This room_id is a hash of /// the user's full mxid. /// ircd::m::id::room ircd::m::user::room_id(const mutable_buffer &buf) const { assert(!empty(user_id)); const ripemd160::buf hash { ripemd160{user_id} }; char b58[size(hash) * 2]; return { buf, b58encode(b58, hash), my_host() }; } ircd::m::device::id::buf ircd::m::user::get_device_from_access_token(const string_view &token) { const event::idx event_idx { user::tokens.get("ircd.access_token", token) }; device::id::buf ret; m::get(event_idx, "content", [&ret] (const json::object &content) { ret = unquote(content.at("device_id")); }); return ret; } ircd::string_view ircd::m::user::gen_access_token(const mutable_buffer &buf) { static const size_t token_max{32}; static const auto &token_dict{rand::dict::alpha}; const mutable_buffer out { data(buf), std::min(token_max, size(buf)) }; return rand::string(token_dict, out); } ircd::m::event::id::buf ircd::m::user::activate() { using prototype = event::id::buf (const m::user &); static mods::import function { "client_account", "activate__user" }; return function(*this); } ircd::m::event::id::buf ircd::m::user::deactivate() { using prototype = event::id::buf (const m::user &); static mods::import function { "client_account", "deactivate__user" }; return function(*this); } bool ircd::m::user::is_active() const { using prototype = bool (const m::user &); static mods::import function { "client_account", "is_active__user" }; return function(*this); } // // user::room // ircd::m::user::room::room(const m::user::id &user_id, const vm::copts *const &copts, const event::fetch::opts *const &fopts) :room { m::user{user_id}, copts, fopts } { } ircd::m::user::room::room(const m::user &user, const vm::copts *const &copts, const event::fetch::opts *const &fopts) :user{user} ,room_id{user.room_id()} { static_cast(*this) = { room_id, copts, fopts }; } bool ircd::m::user::room::is(const room::id &room_id, const user::id &user_id) { const user::room user_room{user_id}; return user_room.room_id == room_id; } /////////////////////////////////////////////////////////////////////////////// // // m/txn.h // /// Returns the serial size of the JSON this txn would consume. Note: this /// creates a json::iov involving a timestamp to figure out the total size /// of the txn. When the user creates the actual txn a different timestamp /// is created which may be a different size. Consider using the lower-level /// create(closure) or add some pad to be sure. /// size_t ircd::m::txn::serialized(const array &pdu, const array &edu, const array &pdu_failure) { size_t ret; const auto closure{[&ret] (const json::iov &iov) { ret = json::serialized(iov); }}; create(closure, pdu, edu, pdu_failure); return ret; } /// Stringifies a txn from the inputs into the returned std::string /// std::string ircd::m::txn::create(const array &pdu, const array &edu, const array &pdu_failure) { std::string ret; const auto closure{[&ret] (const json::iov &iov) { ret = json::strung(iov); }}; create(closure, pdu, edu, pdu_failure); return ret; } /// Stringifies a txn from the inputs into the buffer /// ircd::string_view ircd::m::txn::create(const mutable_buffer &buf, const array &pdu, const array &edu, const array &pdu_failure) { string_view ret; const auto closure{[&buf, &ret] (const json::iov &iov) { ret = json::stringify(mutable_buffer{buf}, iov); }}; create(closure, pdu, edu, pdu_failure); return ret; } /// Forms a txn from the inputs into a json::iov and presents that iov /// to the user's closure. /// void ircd::m::txn::create(const closure &closure, const array &pdu, const array &edu, const array &pdu_failure) { using ircd::size; json::iov iov; const json::iov::push push[] { { iov, { "origin", my_host() }}, { iov, { "origin_server_ts", ircd::time() }}, }; const json::iov::add _pdus { iov, !empty(pdu), { "pdus", [&pdu]() -> json::value { return { data(pdu), size(pdu) }; } } }; const json::iov::add _edus { iov, !empty(edu), { "edus", [&edu]() -> json::value { return { data(edu), size(edu) }; } } }; const json::iov::add _pdu_failures { iov, !empty(pdu_failure), { "pdu_failures", [&pdu_failure]() -> json::value { return { data(pdu_failure), size(pdu_failure) }; } } }; closure(iov); } ircd::string_view ircd::m::txn::create_id(const mutable_buffer &out, const string_view &txn) { const sha256::buf hash { sha256{txn} }; const string_view txnid { b58encode(out, hash) }; return txnid; } /////////////////////////////////////////////////////////////////////////////// // // m/request.h // // // request // ircd::m::request::request(const string_view &method, const string_view &uri, const mutable_buffer &body_buf, const json::members &body) :request { my_host(), string_view{}, method, uri, json::stringify(mutable_buffer{body_buf}, body) } {} ircd::m::request::request(const string_view &method, const string_view &uri) :request { my_host(), string_view{}, method, uri, json::object{} } {} ircd::m::request::request(const string_view &method, const string_view &uri, const json::object &content) :request { my_host(), string_view{}, method, uri, content } {} ircd::m::request::request(const string_view &origin, const string_view &destination, const string_view &method, const string_view &uri, const json::object &content) { json::get<"origin"_>(*this) = origin; json::get<"destination"_>(*this) = destination; json::get<"method"_>(*this) = method; json::get<"uri"_>(*this) = uri; json::get<"content"_>(*this) = content; if(unlikely(origin && !rfc3986::valid_remote(std::nothrow, origin))) throw m::error { http::BAD_REQUEST, "M_REQUEST_INVALID_ORIGIN", "This origin string '%s' is not a valid remote.", origin }; if(unlikely(destination && !rfc3986::valid_remote(std::nothrow, destination))) throw m::error { http::BAD_REQUEST, "M_REQUEST_INVALID_DESTINATION", "This destination string '%s' is not a valid remote.", destination }; } decltype(ircd::m::request::headers_max) ircd::m::request::headers_max { 32UL }; ircd::string_view ircd::m::request::operator()(const mutable_buffer &out, const vector_view &addl_headers) const { thread_local http::header header[headers_max]; const ctx::critical_assertion ca; size_t headers{0}; header[headers++] = { "User-Agent", info::user_agent }; thread_local char x_matrix[2_KiB]; if(startswith(json::at<"uri"_>(*this), "/_matrix/federation")) { const auto &sk{self::secret_key}; const auto &pkid{self::public_key_id}; header[headers++] = { "Authorization", generate(x_matrix, sk, pkid) }; } assert(headers <= headers_max); assert(headers + addl_headers.size() <= headers_max); for(size_t i(0); i < addl_headers.size() && headers < headers_max; ++i) header[headers++] = addl_headers.at(i); static const string_view content_type { "application/json; charset=utf-8"_sv }; const auto content_length { string_view(json::get<"content"_>(*this)).size() }; window_buffer sb{out}; http::request { sb, json::at<"destination"_>(*this), json::at<"method"_>(*this), json::at<"uri"_>(*this), content_length, content_type, { header, headers } }; return sb.completed(); } decltype(ircd::m::request::generate_content_max) ircd::m::request::generate_content_max { { "name", "ircd.m.request.generate.content_max" }, { "default", long(1_MiB) }, }; ircd::string_view ircd::m::request::generate(const mutable_buffer &out, const ed25519::sk &sk, const string_view &pkid) const { const ctx::critical_assertion ca; thread_local unique_buffer buf { size_t(generate_content_max) }; if(unlikely(json::serialized(*this) > buffer::size(buf))) throw m::error { "M_REQUEST_TOO_LARGE", "This server generated a request of %zu bytes; limit is %zu", json::serialized(*this), buffer::size(buf) }; const json::object object { stringify(mutable_buffer{buf}, *this) }; const ed25519::sig sig { self::secret_key.sign(object) }; const auto &origin { unquote(string_view{json::at<"origin"_>(*this)}) }; thread_local char sigb64[1_KiB]; return fmt::sprintf { out, "X-Matrix origin=%s,key=\"%s\",sig=\"%s\"", origin, pkid, b64encode_unpadded(sigb64, sig) }; } bool ircd::m::request::verify(const string_view &key, const string_view &sig_) const { const ed25519::sig sig { [&sig_](auto &buf) { b64decode(buf, sig_); } }; const json::string &origin { json::at<"origin"_>(*this) }; const m::node node { origin }; bool verified{false}; node.key(key, [this, &verified, &sig] (const ed25519::pk &pk) { verified = verify(pk, sig); }); return verified; } decltype(ircd::m::request::verify_content_max) ircd::m::request::verify_content_max { { "name", "ircd.m.request.verify.content_max" }, { "default", long(1_MiB) }, }; bool ircd::m::request::verify(const ed25519::pk &pk, const ed25519::sig &sig) const { // Matrix spec sez that an empty content object {} is excluded entirely // from the verification. Our JSON only excludes members if they evaluate // to undefined i.e json::object{}/string_view{} but not json::object{"{}"} // or even json::object{""}; rather than burdening the caller with ensuring // their assignment conforms perfectly, we ensure correctness manually. auto _this(*this); if(empty(json::get<"content"_>(*this))) json::get<"content"_>(_this) = json::object{}; const ctx::critical_assertion ca; thread_local unique_buffer buf { size_t(verify_content_max) }; const size_t request_size { json::serialized(_this) }; if(unlikely(request_size > buffer::size(buf))) throw m::error { http::PAYLOAD_TOO_LARGE, "M_REQUEST_TOO_LARGE", "The request size %zu bytes exceeds maximum of %zu bytes", request_size, buffer::size(buf) }; const json::object object { stringify(mutable_buffer{buf}, _this) }; return verify(pk, sig, object); } bool ircd::m::request::verify(const ed25519::pk &pk, const ed25519::sig &sig, const json::object &object) { return pk.verify(object, sig); } // // x_matrix // ircd::m::request::x_matrix::x_matrix(const string_view &input) { string_view tokens[3]; if(ircd::tokens(split(input, ' ').second, ',', tokens) != 3) throw std::out_of_range{"The x_matrix header is malformed"}; for(const auto &token : tokens) { const auto &kv{split(token, '=')}; const auto &val{unquote(kv.second)}; switch(hash(kv.first)) { case hash("origin"): origin = val; break; case hash("key"): key = val; break; case hash("sig"): sig = val; break; } } if(empty(origin)) throw std::out_of_range{"The x_matrix header is missing 'origin='"}; if(empty(key)) throw std::out_of_range{"The x_matrix header is missing 'key='"}; if(empty(sig)) throw std::out_of_range{"The x_matrix header is missing 'sig='"}; } /////////////////////////////////////////////////////////////////////////////// // // m/hook.h // // Internal utils namespace ircd::m { static bool _hook_match(const m::event &matching, const m::event &); static void _hook_fix_state_key(const json::members &, json::member &); static void _hook_fix_room_id(const json::members &, json::member &); static void _hook_fix_sender(const json::members &, json::member &); static json::strung _hook_make_feature(const json::members &); } /// Instance list linkage for all hook sites template<> decltype(ircd::util::instance_list::allocator) ircd::util::instance_list::allocator {}; template<> decltype(ircd::util::instance_list::list) ircd::util::instance_list::list { allocator }; /// Instance list linkage for all hooks template<> decltype(ircd::util::instance_list::allocator) ircd::util::instance_list::allocator {}; template<> decltype(ircd::util::instance_list::list) ircd::util::instance_list::list { allocator }; // // hook::maps // struct ircd::m::hook::maps { std::multimap origin; std::multimap room_id; std::multimap sender; std::multimap state_key; std::multimap type; std::vector always; size_t match(const event &match, const std::function &) const; size_t add(base &hook, const event &matching); size_t del(base &hook, const event &matching); maps(); ~maps() noexcept; }; ircd::m::hook::maps::maps() { } ircd::m::hook::maps::~maps() noexcept { } size_t ircd::m::hook::maps::add(base &hook, const event &matching) { size_t ret{0}; const auto map{[&hook, &ret] (auto &map, const string_view &value) { map.emplace(value, &hook); ++ret; }}; if(json::get<"origin"_>(matching)) map(origin, at<"origin"_>(matching)); if(json::get<"room_id"_>(matching)) map(room_id, at<"room_id"_>(matching)); if(json::get<"sender"_>(matching)) map(sender, at<"sender"_>(matching)); if(json::get<"state_key"_>(matching)) map(state_key, at<"state_key"_>(matching)); if(json::get<"type"_>(matching)) map(type, at<"type"_>(matching)); // Hook had no mappings which means it will match everything. // We don't increment the matcher count for this case. if(!ret) always.emplace_back(&hook); return ret; } size_t ircd::m::hook::maps::del(base &hook, const event &matching) { size_t ret{0}; const auto unmap{[&hook, &ret] (auto &map, const string_view &value) { auto pit{map.equal_range(value)}; while(pit.first != pit.second) if(pit.first->second == &hook) { pit.first = map.erase(pit.first); ++ret; } else ++pit.first; }}; // Unconditional attempt to remove from always. std::remove(begin(always), end(always), &hook); if(json::get<"origin"_>(matching)) unmap(origin, at<"origin"_>(matching)); if(json::get<"room_id"_>(matching)) unmap(room_id, at<"room_id"_>(matching)); if(json::get<"sender"_>(matching)) unmap(sender, at<"sender"_>(matching)); if(json::get<"state_key"_>(matching)) unmap(state_key, at<"state_key"_>(matching)); if(json::get<"type"_>(matching)) unmap(type, at<"type"_>(matching)); return ret; } size_t ircd::m::hook::maps::match(const event &event, const std::function &callback) const { std::set matching { begin(always), end(always) }; const auto site_match{[&matching] (auto &map, const string_view &key) { auto pit{map.equal_range(key)}; for(; pit.first != pit.second; ++pit.first) matching.emplace(pit.first->second); }}; if(json::get<"origin"_>(event)) site_match(origin, at<"origin"_>(event)); if(json::get<"room_id"_>(event)) site_match(room_id, at<"room_id"_>(event)); if(json::get<"sender"_>(event)) site_match(sender, at<"sender"_>(event)); if(json::get<"type"_>(event)) site_match(type, at<"type"_>(event)); if(json::get<"state_key"_>(event)) site_match(state_key, at<"state_key"_>(event)); auto it(begin(matching)); while(it != end(matching)) { const base &hook(**it); if(!_hook_match(hook.matching, event)) it = matching.erase(it); else ++it; } size_t ret{0}; for(auto it(begin(matching)); it != end(matching); ++it, ++ret) if(!callback(**it)) return ret; return ret; } // // hook::base // /// Primary hook ctor ircd::m::hook::base::base(const json::members &members) :_feature { _hook_make_feature(members) } ,feature { _feature } ,matching { feature } { site *site; try { if((site = find_site())) site->add(*this); } catch(...) { if(registered) { auto *const site(find_site()); assert(site != nullptr); site->del(*this); } } } ircd::m::hook::base::~base() noexcept { if(!registered) return; auto *const site { find_site() }; // should be non-null if !registered assert(site != nullptr); // if someone is calling and inside this hook we shouldn't be destructing assert(calling == 0); // if someone is calling the hook::site but inside some other hook, we can // still remove this hook from the site. //assert(site->calling == 0); site->del(*this); } ircd::m::hook::base::site * ircd::m::hook::base::find_site() const { const auto &site_name { this->site_name() }; if(!site_name) return nullptr; for(auto *const &site : m::hook::base::site::list) if(site->name() == site_name) return site; return nullptr; } ircd::string_view ircd::m::hook::base::site_name() const try { return unquote(feature.at("_site")); } catch(const std::out_of_range &e) { throw panic { "Hook %p must name a '_site' to register with.", this }; } // // hook::site // // // hook::site::site // ircd::m::hook::base::site::site(const json::members &members) :_feature { members } ,feature { _feature } ,maps { std::make_unique() } ,exceptions { feature.get("exceptions", true) } { for(const auto &site : list) if(site->name() == name() && site != this) throw error { "Hook site '%s' already registered at %p", name(), site }; // Find and register all of the orphan hooks which were constructed before // this site was constructed. for(auto *const &hook : m::hook::base::list) if(hook->site_name() == name()) add(*hook); } ircd::m::hook::base::site::~site() noexcept { assert(!calling); const std::vector hooks { begin(this->hooks), end(this->hooks) }; for(auto *const hook : hooks) del(*hook); } void ircd::m::hook::base::site::match(const event &event, const std::function &callback) { maps->match(event, callback); } bool ircd::m::hook::base::site::add(base &hook) { assert(!hook.registered); assert(hook.site_name() == name()); assert(hook.matchers == 0); if(!hooks.emplace(&hook).second) { log::warning { log, "Hook %p already registered to site %s", &hook, name() }; return false; } assert(maps); const size_t matched { maps->add(hook, hook.matching) }; hook.matchers = matched; hook.registered = true; matchers += matched; ++count; log::debug { log, "Registered hook %p to site %s", &hook, name() }; return true; } bool ircd::m::hook::base::site::del(base &hook) { log::debug { log, "Removing hook %p from site %s", &hook, name() }; assert(hook.registered); assert(hook.site_name() == name()); const size_t matched { maps->del(hook, hook.matching) }; const auto erased { hooks.erase(&hook) }; hook.matchers -= matched; hook.registered = false; matchers -= matched; --count; assert(hook.matchers == 0); assert(erased); return true; } ircd::string_view ircd::m::hook::base::site::name() const try { return unquote(feature.at("name")); } catch(const std::out_of_range &e) { throw panic { "Hook site %p requires a name", this }; } // // hook // ircd::m::hook::hook::hook(const json::members &feature, decltype(function) function) :base{feature} ,function{std::move(function)} { } ircd::m::hook::hook::hook(decltype(function) function, const json::members &feature) :base{feature} ,function{std::move(function)} { } ircd::m::hook::site::site(const json::members &feature) :base::site{feature} { } void ircd::m::hook::site::operator()(const event &event) { match(event, [this, &event] (base &base) { call(dynamic_cast &>(base), event); return true; }); } void ircd::m::hook::site::call(hook &hfn, const event &event) try { // stats for site ++calls; const scope_count site_calling { calling }; // stats for hook ++hfn.calls; const scope_count hook_calling { hfn.calling }; // call hook hfn.function(event); } catch(const std::exception &e) { if(exceptions) throw; log::critical { log, "Unhandled hookfn(%p) %s error :%s", &hfn, string_view{hfn.feature}, e.what() }; } // // hook internal // /// Internal interface which manipulates the initializer supplied by the /// developer to the hook to create the proper JSON output. i.e They supply /// a "room_id" of "!config" which has no hostname, that is added here /// depending on my_host() in the deployment runtime... /// ircd::json::strung ircd::m::_hook_make_feature(const json::members &members) { const ctx::critical_assertion ca; std::vector copy { begin(members), end(members) }; for(auto &member : copy) switch(hash(member.first)) { case hash("room_id"): _hook_fix_room_id(members, member); continue; case hash("sender"): _hook_fix_sender(members, member); continue; case hash("state_key"): _hook_fix_state_key(members, member); continue; } return { copy.data(), copy.data() + copy.size() }; } void ircd::m::_hook_fix_sender(const json::members &members, json::member &member) { // Rewrite the sender if the supplied input has no hostname if(valid_local_only(id::USER, member.second)) { assert(my_host()); thread_local char buf[256]; member.second = id::user { buf, member.second, my_host() }; } validate(id::USER, member.second); } void ircd::m::_hook_fix_room_id(const json::members &members, json::member &member) { // Rewrite the room_id if the supplied input has no hostname if(valid_local_only(id::ROOM, member.second)) { assert(my_host()); thread_local char buf[256]; member.second = id::room { buf, member.second, my_host() }; } validate(id::ROOM, member.second); } void ircd::m::_hook_fix_state_key(const json::members &members, json::member &member) { const bool is_member_event { end(members) != std::find_if(begin(members), end(members), [] (const auto &member) { return member.first == "type" && member.second == "m.room.member"; }) }; if(!is_member_event) return; // Rewrite the sender if the supplied input has no hostname if(valid_local_only(id::USER, member.second)) { assert(my_host()); thread_local char buf[256]; member.second = id::user { buf, member.second, my_host() }; } validate(id::USER, member.second); } bool ircd::m::_hook_match(const m::event &matching, const m::event &event) { if(json::get<"origin"_>(matching)) if(at<"origin"_>(matching) != json::get<"origin"_>(event)) return false; if(json::get<"room_id"_>(matching)) if(at<"room_id"_>(matching) != json::get<"room_id"_>(event)) return false; if(json::get<"sender"_>(matching)) if(at<"sender"_>(matching) != json::get<"sender"_>(event)) return false; if(json::get<"type"_>(matching)) if(at<"type"_>(matching) != json::get<"type"_>(event)) return false; if(json::get<"state_key"_>(matching)) if(at<"state_key"_>(matching) != json::get<"state_key"_>(event)) return false; if(membership(matching)) if(membership(matching) != membership(event)) return false; if(json::get<"content"_>(matching)) if(json::get<"type"_>(event) == "m.room.message") if(at<"content"_>(matching).has("msgtype")) if(at<"content"_>(matching).get("msgtype") != json::get<"content"_>(event).get("msgtype")) return false; return true; } /////////////////////////////////////////////////////////////////////////////// // // m/error.h // namespace ircd::m { const std::array _error_headers {{ { "Content-Type", "application/json; charset=utf-8" }, }}; } thread_local decltype(ircd::m::error::fmtbuf) ircd::m::error::fmtbuf {}; // // error::error // ircd::m::error::error() :error { internal, http::INTERNAL_SERVER_ERROR, std::string{}, } {} ircd::m::error::error(std::string c) :error { internal, http::INTERNAL_SERVER_ERROR, std::move(c), } {} ircd::m::error::error(const http::code &c) :error { internal, c, std::string{}, } {} ircd::m::error::error(const http::code &c, const json::members &members) :error { internal, c, json::strung{members}, } {} ircd::m::error::error(const http::code &c, const json::iov &iov) :error { internal, c, json::strung{iov}, } {} ircd::m::error::error(const http::code &c, const json::object &object) :error { internal, c, json::strung{object}, } {} ircd::m::error::error(internal_t, const http::code &c, std::string object) :http::error { c, std::move(object), vector_view{_error_headers} } { if(!content.empty()) { strlcat(ircd::exception::buf, " "); strlcat(ircd::exception::buf, errcode()); strlcat(ircd::exception::buf, " :"); strlcat(ircd::exception::buf, errstr()); } } ircd::string_view ircd::m::error::errstr() const noexcept { const json::object &content { this->http::error::content }; return unquote(content.get("error")); } ircd::string_view ircd::m::error::errcode() const noexcept { const json::object &content { this->http::error::content }; return unquote(content.get("errcode", "M_UNKNOWN"_sv)); }