// Matrix Construct // // Copyright (C) Matrix Construct Developers, Authors & Contributors // Copyright (C) 2016-2019 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 x_matrix_verify_origin; extern conf::item x_matrix_verify_destination; static string_view authenticate_bridge(const resource::method &, const client &, resource::request &); static user::id authenticate_user(const resource::method &, const client &, resource::request &); static string_view authenticate_node(const resource::method &, const client &, resource::request &); } decltype(ircd::m::resource::log) ircd::m::resource::log { "m.resource" }; decltype(ircd::m::x_matrix_verify_origin) ircd::m::x_matrix_verify_origin { { "name", "ircd.m.x_matrix.verify_origin" }, { "default", true }, }; decltype(ircd::m::x_matrix_verify_origin) ircd::m::x_matrix_verify_destination { { "name", "ircd.m.x_matrix.verify_destination" }, { "default", true }, }; // // m::resource // ircd::m::resource::resource(const string_view &path) :m::resource { path, {}, } { } ircd::m::resource::resource(const string_view &path, struct opts opts) :ircd::resource { path_canonize(path_buf, path), opts } { } ircd::m::resource::~resource() noexcept { } ircd::string_view ircd::m::resource::params(const string_view &path) const { const auto prefix_tokens { token_count(this->path, '/') }; const auto version { path_version(path) }; const auto params_after { prefix_tokens? prefix_tokens - 1 + bool(version): 0 }; const auto ret { tokens_after(path, '/', params_after) }; return ret; } ircd::resource & ircd::m::resource::route(const string_view &path) const { thread_local char canon_buf[1024]; const string_view canon_path { path_canonize(canon_buf, path) }; return mutable_cast(ircd::resource::route(canon_path)); } ircd::string_view ircd::m::resource::path_canonize(const mutable_buffer &buf, const string_view &path) { const auto version { path_version(path) }; if(!version) return path; const auto &[before, after] { tokens_split(path, '/', 2, 1) }; mutable_buffer out{buf}; consume(out, copy(out, '/')); consume(out, copy(out, before)); if(likely(after)) { consume(out, copy(out, '/')); consume(out, copy(out, after)); } return string_view { data(buf), data(out) }; } ircd::string_view ircd::m::resource::path_version(const string_view &path) { const auto version { token(path, '/', 2, {}) }; const bool pass { true && version.size() >= 2 && (version[0] == 'v' || version[0] == 'r') && (version[1] >= '0' && version[1] <= '9') }; return pass? version: string_view{}; } // // m::resource::method // ircd::m::resource::method::method(m::resource &resource, const string_view &name, handler function, struct opts opts) :ircd::resource::method { resource, name, std::bind(&method::handle, this, ph::_1, ph::_2), std::move(opts), } ,function { std::move(function) } { } ircd::m::resource::method::~method() noexcept { } ircd::resource::response ircd::m::resource::method::handle(client &client, ircd::resource::request &request_) try { m::resource::request request { *this, client, request_ }; const string_view &ident { request.bridge_id?: request.node_id?: request.user_id?: string_view{} }; if(ident) log::debug { log, "%s %s %s %s `%s'", client.loghead(), ident, request.head.method, request.version?: "??"_sv, request.head.path, }; const bool cached_error { request.node_id && fed::errant(request.node_id) }; // If we have an error cached from previously not being able to // contact this origin we can clear that now that they're alive. if(cached_error) { m::burst::opts opts; m::burst::burst { request.node_id, opts }; } return function(client, request); } catch(const json::print_error &e) { throw m::error { http::INTERNAL_SERVER_ERROR, "M_NOT_JSON", "Generator Protection: %s", e.what() }; } catch(const json::not_found &e) { throw m::error { http::BAD_REQUEST, "M_BAD_JSON", "Required JSON field: %s", e.what() }; } catch(const json::error &e) { throw m::error { http::BAD_REQUEST, "M_NOT_JSON", "%s", e.what() }; } catch(const ctx::timeout &e) { throw m::error { http::BAD_GATEWAY, "M_REQUEST_TIMEOUT", "%s", e.what() }; } // // resource::request // ircd::m::resource::request::request(const method &method, const client &client, ircd::resource::request &r) :ircd::resource::request { r } ,version { path_version(head.path) } ,authorization { split(head.authorization, ' ') } ,access_token { iequals(authorization.first, "Bearer"_sv)? authorization.second: query["access_token"] } ,x_matrix { !access_token && iequals(authorization.first, "X-Matrix"_sv)? m::request::x_matrix{authorization.first, authorization.second}: m::request::x_matrix{} } ,node_id { // Server X-Matrix header verified here. Similar to client auth, origin // which has been authed is referenced in the client.request. If the method // requires, and auth fails or not provided, this function throws. // Otherwise it returns a string_view of the origin name in // request.node_id, or an empty string_view if an origin was not // apropos for this request (i.e a client request rather than federation). authenticate_node(method, client, *this) } ,user_id { // Client access token verified here. On success, user_id owning the token // is copied into the request structure. On failure, the method is // checked to see if it requires authentication and if so, this throws. authenticate_user(method, client, *this) } ,bridge_id { // Application service access token verified here. Note that on success // this function will set the user_id as well as the bridge_id. authenticate_bridge(method, client, *this) } { } /// Authenticate a client based on access_token either in the query string or /// in the Authentication bearer header. If a token is found the user_id owning /// the token is copied into the request. If it is not found or it is invalid /// then the method being requested is checked to see if it is required. If so /// the appropriate exception is thrown. Note that if the access_token belongs /// to a bridge (application service), the bridge_id is set accordingly. ircd::m::user::id ircd::m::authenticate_user(const resource::method &method, const client &client, resource::request &request) { assert(method.opts); const auto requires_auth { method.opts->flags & resource::method::REQUIRES_AUTH }; if(!requires_auth && !request.access_token) return {}; // Note that we still try to auth a token and obtain a user_id here even // if the endpoint does not require auth; an auth'ed user may enjoy // additional functionality if credentials provided. if(requires_auth && !request.access_token) throw m::error { http::UNAUTHORIZED, "M_MISSING_TOKEN", "Credentials for this method are required but missing." }; // Belay authentication to authenticate_bridge(). if(startswith(request.access_token, "bridge_")) return {}; const m::room::id::buf tokens_room_id { "tokens", origin(my()) }; const m::room::state tokens { tokens_room_id }; const event::idx event_idx { tokens.get(std::nothrow, "ircd.access_token", request.access_token) }; // The sender of the token is the user being authenticated. const string_view sender { m::get(std::nothrow, event_idx, "sender", request.id_buf) }; // Note that if the endpoint does not require auth and we were not // successful in authenticating the provided token: we do not throw here; // instead we continue as if no token was provided, and no user_id will // be known to the requested endpoint. if(requires_auth && !sender) throw m::error { http::UNAUTHORIZED, "M_UNKNOWN_TOKEN", "Credentials for this method are required but invalid." }; return sender; } /// Authenticate an application service (bridge) ircd::string_view ircd::m::authenticate_bridge(const resource::method &method, const client &client, resource::request &request) { // Real user was already authenticated; not a bridge. if(request.user_id) return {}; // No attempt at authenticating as a bridge; not a bridge. if(!startswith(request.access_token, "bridge_")) return {}; const m::room::id::buf tokens_room_id { "tokens", origin(my()) }; const m::room::state tokens { tokens_room_id }; const event::idx event_idx { tokens.get(std::nothrow, "ircd.access_token", request.access_token) }; // The sender of the token is the bridge's user_id, where the bridge_id // is the localpart, but none of this is a puppetting/target user_id. const string_view sender { m::get(std::nothrow, event_idx, "sender", request.id_buf) }; // Note that unlike authenticate_user, if an as_token was proffered but is // not valid, there is no possible fallback to unauthenticated mode and // this must throw here. if(!sender) throw m::error { http::UNAUTHORIZED, "M_UNKNOWN_TOKEN", "Credentials for this method are required but invalid." }; // Find the user_id the bridge wants to masquerade as. const string_view puppet_user_id { request.query["user_id"] }; // Set the user credentials for the request at the discretion of the // bridge. If the bridge did not supply a user_id then we set the value // to the bridge's own agency. Note that we urldecode into the id_buf // after the bridge_user_id; care not to overwrite it. { mutable_buffer buf(request.id_buf); request.user_id = puppet_user_id? url::decode(buf + size(sender), puppet_user_id): sender; } // Return only the localname (that's the localpart not including sigil). return m::user::id(sender).localname(); } ircd::string_view ircd::m::authenticate_node(const resource::method &method, const client &client, resource::request &request) try { assert(method.opts); const auto required { method.opts->flags & resource::method::VERIFY_ORIGIN }; const bool supplied { !empty(request.x_matrix.origin) }; if(!required && !supplied) return {}; if(required && !supplied) throw m::error { http::UNAUTHORIZED, "M_MISSING_AUTHORIZATION", "Required X-Matrix Authorization was not supplied" }; if(x_matrix_verify_destination && !m::self::host(request.head.host)) throw m::error { http::UNAUTHORIZED, "M_NOT_MY_HOST", "The HTTP Host '%s' is not an authenticable destination here.", request.head.host, }; const auto head_host { rstrip(request.head.host, ":8448") }; const auto auth_dest { rstrip(request.x_matrix.destination, ":8448") }; if(x_matrix_verify_destination && auth_dest && head_host != auth_dest) throw m::error { http::UNAUTHORIZED, "M_NOT_MY_DESTINATION", "The X-Matrix Authorization destination '%s' is not recognized here.", auth_dest, }; const m::request object { request.x_matrix.origin, head_host, method.name, request.head.uri, request.content }; if(x_matrix_verify_origin && !object.verify(request.x_matrix.key, request.x_matrix.sig)) throw m::error { http::FORBIDDEN, "M_INVALID_SIGNATURE", "The X-Matrix Authorization is invalid." }; return request.x_matrix.origin; } catch(const m::error &) { throw; } catch(const std::exception &e) { thread_local char rembuf[128]; log::derror { resource::log, "X-Matrix Authorization from %s: %s", string(rembuf, remote(client)), e.what() }; throw m::error { http::UNAUTHORIZED, "M_UNKNOWN_ERROR", "An error has prevented authorization: %s", e.what() }; }