/* * Copyright (C) 2016 Charybdis Development Team * Copyright (C) 2016 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. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include /////////////////////////////////////////////////////////////////////////////// // // m/session.h // ircd::m::session::session(const host_port &host_port) :client{host_port} { } ircd::json::object ircd::m::session::operator()(parse::buffer &pb, request &r) { parse::capstan pc { pb, read_closure(*this) }; http::request { host(remote_addr(*this)), r.method, r.path, r.query, std::string(r), write_closure(*this), { { "Content-Type"s, "application/json"s } } }; http::code status; json::object object; http::response { pc, nullptr, [&pc, &status, &object](const http::response::head &head) { status = http::status(head.status); object = http::response::content{pc, head}; } }; if(status < 200 || status >= 300) throw m::error(status, object); return object; } /////////////////////////////////////////////////////////////////////////////// // // m.h // namespace ircd { namespace m { std::map modules; ircd::listener *listener; void bootstrap(); } // namespace m } // namespace ircd ircd::m::init::init() { if(db::sequence(*events::events) == 0) bootstrap(); for(const auto &name : mods::available()) if(startswith(name, "client_")) modules.emplace(name, name); modules.emplace("root.so"s, "root.so"s); //TODO: conf obviously listener = new ircd::listener { json::string(json::members { { "name", "Chat Matrix" }, { "host", "0.0.0.0" }, { "port", 8447 }, { "ssl_certificate_file", "/home/jason/zemos.net.tls.crt" }, { "ssl_certificate_chain_file", "/home/jason/zemos.net.tls.crt" }, { "ssl_tmp_dh_file", "/home/jason/zemos.net.tls.dh" }, { "ssl_private_key_file_pem", "/home/jason/zemos.net.tls.key" }, }) }; } ircd::m::init::~init() noexcept { delete listener; modules.clear(); } void ircd::m::bootstrap() { assert(events::events); assert(db::sequence(*events::events) == 0); ircd::log::notice ( "This appears to be your first time running IRCd because the events " "database is empty. I will be bootstrapping it with initial events now..." ); // ircd.create event const m::id::id::room::buf room_id{"ircd", "localhost"}; const m::id::id::user::buf user_id{"ircd", "localhost"}; { const auto type{"m.room.create"}; char content[512]; print(content, sizeof(content), json::members { { "creator", user_id }, }); m::event event { { "room_id", room_id }, { "type", type }, { "state_key", "" }, { "sender", user_id }, { "content", content }, }; m::events::insert(event); } { const auto type{"m.room.member"}; char content[512]; print(content, sizeof(content), json::members { { "membership", "join" }, }); m::event event { { "room_id", room_id }, { "type", type }, { "state_key", user_id }, { "sender", user_id }, { "content", content }, }; m::events::insert(event); } } /////////////////////////////////////////////////////////////////////////////// // // m/db.h // namespace ircd::m::dbs { std::map modules; std::map> databases; void init_modules(); void init_databases(); } ircd::m::dbs::init::init() { init_modules(); init_databases(); ircd::m::events::events = databases.at("events").get(); } ircd::m::dbs::init::~init() noexcept { ircd::m::events::events = nullptr; databases.clear(); modules.clear(); } void ircd::m::dbs::init_databases() { for(const auto &pair : modules) { const auto &name(pair.first); const auto dbname(mods::unpostfixed(name)); const std::string shortname(lstrip(dbname, "db_")); const std::string symname(shortname + "_database"s); databases.emplace(shortname, import_shared { dbname, symname }); } } void ircd::m::dbs::init_modules() { for(const auto &name : mods::available()) if(startswith(name, "db_")) modules.emplace(name, name); } /////////////////////////////////////////////////////////////////////////////// // // m/events.h // namespace ircd::m::events { void write(const event &); } ircd::database *ircd::m::events::events; void ircd::m::events::insert(json::builder &builder) { const id::event::buf generated_event_id { builder.has("event_id")? id::event::buf{} : id::event::buf{id::generate, "cdc.z"} }; const json::builder::add event_id { builder, { "event_id", generated_event_id } }; const json::builder::set event_id2 { builder, { "event_id", generated_event_id } }; const json::builder::add origin_server_ts { builder, { "origin_server_ts", time() } }; insert(event{builder}); } void ircd::m::events::insert(const event &event) { if(!json::val(event)) throw BAD_JSON("Required event field: '%s'", name::type); if(!json::val(event)) throw BAD_JSON("Required event field: '%s'", name::sender); if(!json::val(event)) throw BAD_JSON("Required event field: '%s'", name::event_id); if(!json::val(event)) throw BAD_JSON("Required event field: '%s'", name::origin_server_ts); for(const auto &transition : events::transitions) if(!transition->valid(event)) throw INVALID_TRANSITION("Event insertion refused: '%s'", transition->name); write(event); } ircd::m::events::const_iterator ircd::m::events::find(const id &id) { cursor cur{"!room_id$event_id"}; return cur.find(id); } ircd::m::events::const_iterator ircd::m::events::begin() { cursor cur{"!room_id$event_id"}; return cur.begin(); } ircd::m::events::const_iterator ircd::m::events::end() { cursor cur{"!room_id$event_id"}; return cur.end(); } ircd::m::events::transition_list ircd::m::events::transitions {}; ircd::m::events::transition::transition(const char *const &name) :name{name} ,it { events::transitions, events::transitions.emplace(events::transitions.end(), this) } { } ircd::m::events::transition::transition(const char *const &name, struct method method) :name{name} ,method{std::move(method)} ,it { events::transitions, events::transitions.emplace(events::transitions.end(), this) } { } ircd::m::events::transition::~transition() noexcept { } bool ircd::m::events::transition::valid(const event &event) const { return method.valid(event); } void ircd::m::events::transition::effects(const event &event) { method.effects(event); } void ircd::m::events::write(const event &event) { const auto &master_index { at(event) }; constexpr const size_t num_indexes(1); constexpr const size_t num_deltas { event.size() + num_indexes }; size_t i(0); db::delta deltas[num_deltas]; for_each(event, [&i, &deltas, &master_index] (const auto &key, const auto &val) { deltas[i++] = { key, // col master_index, // key byte_view<>{val}, // val }; }); char buf[id::event::buf::SIZE + id::room::buf::SIZE]; if(!json::val(event).empty()) { strlcpy(buf, json::val(event).data(), sizeof(buf)); strlcat(buf, json::val(event).data(), sizeof(buf)); deltas[i++] = db::delta { "!room_id$event_id", // col buf, // key }; } (*events)(begin(deltas), begin(deltas) + i); } /////////////////////////////////////////////////////////////////////////////// // // m/room.h // void ircd::m::room::join(const m::id::user &user_id, json::builder &content) { json::builder::set membership_join { content, { "membership", "join" } }; membership(user_id, content); } void ircd::m::room::membership(const m::id::user &user_id, json::builder &content) { if(is_member(user_id, content.at("membership"))) throw m::ALREADY_MEMBER { "Already a member with this membership." }; char buffer[512]; const auto printed_content { stringify(buffer, content) }; json::builder event; json::builder::set fields{event, { { "room_id", room_id }, { "type", "m.room.member" }, { "state_key", user_id }, { "sender", user_id }, { "content", printed_content } }}; m::events::insert(event); } bool ircd::m::room::is_member(const m::id::user &user_id, const string_view &membership) { const m::events::where::equal query { { "type", "m.room.member" }, { "state_key", user_id } }; return count(query, [](const auto &event) { const json::object &content { json::val(event) }; const auto &membership { unquote(content["membership"]) }; return membership == membership; }); } bool ircd::m::room::any(const events::where &where) const { events::cursor cursor{"!room_id$event_id"}; cursor.where = &where; return bool(cursor.find(room_id)); } bool ircd::m::room::any(const events::where &where, const event_closure_bool &closure) const { events::cursor cursor{"!room_id$event_id"}; cursor.where = &where; for(auto it(cursor.find(room_id)); bool(it); ++it) if(closure(*it)) return true; return false; } size_t ircd::m::room::count(const events::where &where) const { events::cursor cursor{"!room_id$event_id"}; cursor.where = &where; size_t i(0); for(auto it(cursor.find(room_id)); bool(it); ++it, i++) return i; } size_t ircd::m::room::count(const events::where &where, const event_closure_bool &closure) const { events::cursor cursor{"!room_id$event_id"}; cursor.where = &where; size_t i(0); for(auto it(cursor.find(room_id)); bool(it); ++it) { const m::event &e(*it); i += closure(e); } return i; } void ircd::m::room::for_each(const events::where &where, const event_closure &closure) const { events::cursor cursor{"!room_id$event_id"}; cursor.where = &where; auto it(cursor.find(room_id)); if(!it) return; for(; bool(it); ++it) closure(*it); } void ircd::m::room::for_each(const event_closure &closure) const { const m::events::where::noop where{}; for_each(where, closure); } /////////////////////////////////////////////////////////////////////////////// // // m/id.h // ircd::m::id::id(const string_view &id) :string_view{id} ,sigil{m::sigil(id)} { } ircd::m::id::id(const enum sigil &sigil, const string_view &id) :string_view{id} ,sigil{sigil} { if(!valid()) throw INVALID_MXID("Not a valid '%s' mxid", reflect(sigil)); } ircd::m::id::id(const enum sigil &sigil, char *const &buf, const size_t &max, const string_view &id) :string_view{buf} ,sigil{sigil} { strlcpy(buf, id, max); } ircd::m::id::id(const enum sigil &sigil, char *const &buf, const size_t &max, const string_view &name, const string_view &host) :string_view{[&]() -> string_view { if(!max) return {}; size_t len(0); if(!startswith(name, sigil)) buf[len++] = char(sigil); const auto has_sep { std::count(std::begin(name), std::end(name), ':') }; if(!has_sep && host.empty()) { len += strlcpy(buf + len, name, max - len); } else if(!has_sep && !host.empty()) { len += fmt::snprintf(buf + len, max - len, "%s:%s", name, host); } else if(has_sep == 1 && !host.empty() && !split(name, ':').second.empty()) { len += strlcpy(buf + len, name, max - len); } else if(has_sep && !host.empty()) { throw INVALID_MXID("Not a valid '%s' mxid", reflect(sigil)); } return { buf, len }; }()} ,sigil{sigil} { } ircd::m::id::id(const enum sigil &sigil, char *const &buf, const size_t &max, const generate_t &, const string_view &host) :string_view{[&] { char name[64]; switch(sigil) { case sigil::USER: generate_random_prefixed(sigil::USER, "guest", name, sizeof(name)); break; case sigil::ALIAS: generate_random_prefixed(sigil::ALIAS, "", name, sizeof(name)); break; default: generate_random_timebased(sigil, name, sizeof(name)); break; }; const size_t len { fmt::snprintf(buf, max, "%s:%s", name, host) }; return string_view { buf, len }; }()} ,sigil{sigil} { } bool ircd::m::id::valid() const { const auto parts(split(*this, ':')); const auto &local(parts.first); const auto &host(parts.second); // this valid() requires a full canonical mxid with a host if(host.empty()) return false; // local requires a sigil plus at least one character if(local.size() < 2) return false; // local requires the correct sigil type if(!startswith(local, sigil)) return false; return true; } bool ircd::m::id::valid_local() const { const auto parts(split(*this, ':')); const auto &local(parts.first); // local requires a sigil plus at least one character if(local.size() < 2) return false; // local requires the correct sigil type if(!startswith(local, sigil)) return false; return true; } ircd::string_view ircd::m::id::generate_random_prefixed(const enum sigil &sigil, const string_view &prefix, char *const &buf, const size_t &max) { const uint32_t num(rand::integer()); const size_t len(fmt::snprintf(buf, max, "%c%s%u", char(sigil), prefix, num)); return { buf, len }; } ircd::string_view ircd::m::id::generate_random_timebased(const enum sigil &sigil, char *const &buf, const size_t &max) { const auto utime(microtime()); const size_t len(snprintf(buf, max, "%c%zd%06d", char(sigil), utime.first, utime.second)); return { buf, len }; } enum ircd::m::id::sigil ircd::m::sigil(const string_view &s) try { return sigil(s.at(0)); } catch(const std::out_of_range &e) { throw BAD_SIGIL("sigil undefined"); } enum ircd::m::id::sigil ircd::m::sigil(const char &c) { switch(c) { case '$': return id::EVENT; case '@': return id::USER; case '#': return id::ALIAS; case '!': return id::ROOM; } throw BAD_SIGIL("'%c' is not a valid sigil", c); } const char * ircd::m::reflect(const enum id::sigil &c) { switch(c) { case id::EVENT: return "EVENT"; case id::USER: return "USER"; case id::ALIAS: return "ALIAS"; case id::ROOM: return "ROOM"; } return "?????"; } bool ircd::m::valid_sigil(const string_view &s) try { return valid_sigil(s.at(0)); } catch(const std::out_of_range &e) { return false; } bool ircd::m::valid_sigil(const char &c) { switch(c) { case id::EVENT: case id::USER: case id::ALIAS: case id::ROOM: return true; } return false; }