mirror of
https://github.com/matrix-construct/construct
synced 2024-12-26 15:33:54 +01:00
613 lines
12 KiB
C++
613 lines
12 KiB
C++
// The Construct
|
|
//
|
|
// Copyright (C) The Construct Developers, Authors & Contributors
|
|
// Copyright (C) 2016-2020 Jason Volk <jason@zemos.net>
|
|
//
|
|
// 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::fed::well_known
|
|
{
|
|
static net::hostport make_remote(const string_view &);
|
|
static void submit(request &);
|
|
static void receive(request &);
|
|
static void finish(request &);
|
|
static bool redirect(request &);
|
|
static bool handle(request &);
|
|
static void worker();
|
|
|
|
static server::request request_skip;
|
|
extern ctx::dock worker_dock;
|
|
extern ctx::context worker_context;
|
|
extern run::changed handle_quit;
|
|
extern log::log log;
|
|
}
|
|
|
|
template<>
|
|
decltype(ircd::util::instance_list<ircd::m::fed::well_known::request>::allocator)
|
|
ircd::util::instance_list<ircd::m::fed::well_known::request>::allocator
|
|
{};
|
|
|
|
template<>
|
|
decltype(ircd::util::instance_list<ircd::m::fed::well_known::request>::list)
|
|
ircd::util::instance_list<ircd::m::fed::well_known::request>::list
|
|
{
|
|
allocator
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::log)
|
|
ircd::m::fed::well_known::log
|
|
{
|
|
"m.well-known"
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::cache_default)
|
|
ircd::m::fed::well_known::cache_default
|
|
{
|
|
{ "name", "ircd.m.fed.well-known.cache.default" },
|
|
{ "default", 24 * 60 * 60L },
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::cache_error)
|
|
ircd::m::fed::well_known::cache_error
|
|
{
|
|
{ "name", "ircd.m.fed.well-known.cache.error" },
|
|
{ "default", 36 * 60 * 60L },
|
|
};
|
|
|
|
///NOTE: not yet used until HTTP cache headers in response are respected.
|
|
decltype(ircd::m::fed::well_known::cache_max)
|
|
ircd::m::fed::well_known::cache_max
|
|
{
|
|
{ "name", "ircd.m.fed.well-known.cache.max" },
|
|
{ "default", 48 * 60 * 60L },
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::request::path)
|
|
ircd::m::fed::well_known::request::path
|
|
{
|
|
"/.well-known/matrix/server"
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::request::type)
|
|
ircd::m::fed::well_known::request::type
|
|
{
|
|
"well-known.matrix.server"
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::request::sopts)
|
|
ircd::m::fed::well_known::request::sopts
|
|
{
|
|
false // http_exceptions
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::request::timeout)
|
|
ircd::m::fed::well_known::request::timeout
|
|
{
|
|
{ "name", "ircd.m.fed.well-known.request.timeout" },
|
|
{ "default", 15L },
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::request::redirects_max)
|
|
ircd::m::fed::well_known::request::redirects_max
|
|
{
|
|
{ "name", "ircd.m.fed.well-known.request.redirects.max" },
|
|
{ "default", 2L },
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::request::id_ctr)
|
|
ircd::m::fed::well_known::request::id_ctr;
|
|
|
|
decltype(ircd::m::fed::well_known::request::mutex)
|
|
ircd::m::fed::well_known::request::mutex;
|
|
|
|
decltype(ircd::m::fed::well_known::worker_dock)
|
|
ircd::m::fed::well_known::worker_dock;
|
|
|
|
decltype(ircd::m::fed::well_known::worker_context)
|
|
ircd::m::fed::well_known::worker_context
|
|
{
|
|
"m.fed.well_known",
|
|
512_KiB,
|
|
&worker,
|
|
context::POST
|
|
};
|
|
|
|
decltype(ircd::m::fed::well_known::handle_quit)
|
|
ircd::m::fed::well_known::handle_quit
|
|
{
|
|
run::level::QUIT, []() noexcept
|
|
{
|
|
worker_dock.notify_all();
|
|
}
|
|
};
|
|
|
|
ircd::ctx::future<ircd::string_view>
|
|
ircd::m::fed::well_known::get(const mutable_buffer &buf,
|
|
const string_view &target,
|
|
const opts &opts)
|
|
try
|
|
{
|
|
const m::room::id::buf cache_room_id
|
|
{
|
|
"dns", m::my_host()
|
|
};
|
|
|
|
const m::room cache_room
|
|
{
|
|
cache_room_id
|
|
};
|
|
|
|
const m::event::idx event_idx
|
|
{
|
|
likely(opts.cache_check)?
|
|
cache_room.get(std::nothrow, request::type, target):
|
|
0UL
|
|
};
|
|
|
|
const milliseconds origin_server_ts
|
|
{
|
|
m::get<time_t>(std::nothrow, event_idx, "origin_server_ts", time_t(0))
|
|
};
|
|
|
|
const json::object content
|
|
{
|
|
m::get(std::nothrow, event_idx, "content", buf)
|
|
};
|
|
|
|
const seconds ttl
|
|
{
|
|
content.get<time_t>("ttl", time_t(86400))
|
|
};
|
|
|
|
const system_point expires
|
|
{
|
|
origin_server_ts + ttl
|
|
};
|
|
|
|
const bool expired
|
|
{
|
|
ircd::now<system_point>() > expires
|
|
};
|
|
|
|
const json::string cached
|
|
{
|
|
content["m.server"]
|
|
};
|
|
|
|
const bool valid
|
|
{
|
|
// entry must not be blank
|
|
!empty(cached)
|
|
|
|
// entry must not be expired unless options allow expired hits
|
|
&& (!expired || opts.expired)
|
|
};
|
|
|
|
// Branch to return cache hit
|
|
if(likely(valid))
|
|
return ctx::future<string_view>
|
|
{
|
|
ctx::already, string_view
|
|
{
|
|
data(buf), move(buf, cached)
|
|
}
|
|
};
|
|
|
|
const net::hostport remote
|
|
{
|
|
make_remote(target)
|
|
};
|
|
|
|
const bool fetch
|
|
{
|
|
// options must allow network request
|
|
opts.request
|
|
|
|
// check if the peer already has a cached error in server::
|
|
&& !server::errant(remote)
|
|
};
|
|
|
|
// Branch if won't fetch; return target itself as result
|
|
if(!fetch)
|
|
return ctx::future<string_view>
|
|
{
|
|
ctx::already, string_view
|
|
{
|
|
data(buf), move(buf, target)
|
|
}
|
|
};
|
|
|
|
if(opts.cache_check)
|
|
{
|
|
char tmbuf[48];
|
|
log::dwarning
|
|
{
|
|
log, "%s cache invalid %s event_idx:%u expires %s",
|
|
target,
|
|
cached?: json::string{"<not found>"},
|
|
event_idx,
|
|
cached? timef(tmbuf, expires, localtime): "<never>"_sv,
|
|
};
|
|
}
|
|
|
|
// Synchronize modification of the request::list
|
|
const std::lock_guard request_lock
|
|
{
|
|
request::mutex
|
|
};
|
|
|
|
// Start request
|
|
auto req
|
|
{
|
|
std::make_unique<request>()
|
|
};
|
|
|
|
// req->target is a copy in the request struct so the caller can disappear.
|
|
req->target = string_view
|
|
{
|
|
req->tgtbuf[0], copy(req->tgtbuf[0], target)
|
|
};
|
|
|
|
// req->m_server is a copy in the request struct so the caller can disappear.
|
|
req->m_server = string_view
|
|
{
|
|
req->tgtbuf[1], copy(req->tgtbuf[1], cached)
|
|
};
|
|
|
|
// but req->out is not safe once the caller destroys their future, which
|
|
// is indicated by req->promise's invalidation. Do not write to this
|
|
// unless the promise is valid.
|
|
req->out = buf;
|
|
|
|
// all other properties are independent of the caller
|
|
req->opts = opts;
|
|
req->expires = expires;
|
|
req->uri.path = request::path;
|
|
req->uri.remote = req->target;
|
|
ctx::future<string_view> ret{req->promise}; try
|
|
{
|
|
submit(*req);
|
|
req.release();
|
|
}
|
|
catch(const std::exception &e)
|
|
{
|
|
log::derror
|
|
{
|
|
log, "request submit for %s :%s",
|
|
target,
|
|
e.what(),
|
|
};
|
|
|
|
const ctx::exception_handler eh;
|
|
finish(*req);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
catch(const ctx::interrupted &)
|
|
{
|
|
throw;
|
|
}
|
|
catch(const std::exception &e)
|
|
{
|
|
log::error
|
|
{
|
|
log, "get %s :%s",
|
|
target,
|
|
e.what(),
|
|
};
|
|
|
|
return ctx::future<string_view>
|
|
{
|
|
ctx::already, string_view
|
|
{
|
|
data(buf), move(buf, target)
|
|
}
|
|
};
|
|
}
|
|
|
|
void
|
|
ircd::m::fed::well_known::worker()
|
|
try
|
|
{
|
|
// Wait for runlevel RUN before proceeding...
|
|
run::barrier<ctx::interrupted>{};
|
|
while(!request::list.empty() || run::level == run::level::RUN)
|
|
{
|
|
worker_dock.wait([]() noexcept
|
|
{
|
|
return !request::list.empty() || run::level != run::level::RUN;
|
|
});
|
|
|
|
if(request::list.empty())
|
|
break;
|
|
|
|
auto next
|
|
{
|
|
ctx::when_any(std::begin(request::list), std::end(request::list), []
|
|
(auto &it) -> server::request &
|
|
{
|
|
return !(*it)->req? request_skip: (*it)->req;
|
|
})
|
|
};
|
|
|
|
const ctx::uninterruptible::nothrow ui;
|
|
if(!next.wait(milliseconds(250), std::nothrow))
|
|
continue;
|
|
|
|
const auto it
|
|
{
|
|
next.get()
|
|
};
|
|
|
|
assert(it != std::end(request::list));
|
|
if(unlikely(it == std::end(request::list)))
|
|
continue;
|
|
|
|
const std::lock_guard request_lock
|
|
{
|
|
request::mutex
|
|
};
|
|
|
|
std::unique_ptr<request> req
|
|
{
|
|
*it
|
|
};
|
|
|
|
if(!handle(*req)) // redirect
|
|
req.release();
|
|
else
|
|
finish(*req);
|
|
}
|
|
|
|
assert(request::list.empty());
|
|
}
|
|
catch(const ctx::interrupted &)
|
|
{
|
|
throw;
|
|
}
|
|
catch(const std::exception &e)
|
|
{
|
|
log::critical
|
|
{
|
|
log, "Worker unhandled :%s",
|
|
e.what(),
|
|
};
|
|
}
|
|
|
|
/// Returns true if the request is done with either success or error; false
|
|
/// if the request resubmitted itself.
|
|
bool
|
|
ircd::m::fed::well_known::handle(request &req)
|
|
try
|
|
{
|
|
receive(req);
|
|
|
|
// Handle redirection.
|
|
if(http::category(req.code) == http::category::REDIRECT)
|
|
return redirect(req);
|
|
|
|
return true;
|
|
}
|
|
catch(const std::exception &e)
|
|
{
|
|
log::derror
|
|
{
|
|
log, "%s handling :%s",
|
|
req.target,
|
|
e.what(),
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ircd::m::fed::well_known::redirect(request &req)
|
|
{
|
|
// Indirection code, but no location response header
|
|
if(!req.location)
|
|
return true;
|
|
|
|
// Redirection; carry over the new target by copying it because it's
|
|
// in the buffer which we'll be overwriting for the new request.
|
|
req.carry = unique_mutable_buffer{req.location};
|
|
req.uri = string_view{req.carry};
|
|
|
|
// Indirection code, bad location header.
|
|
if(!req.uri.path || !req.uri.remote)
|
|
return true;
|
|
|
|
if(req.redirects++ >= request::redirects_max)
|
|
return true;
|
|
|
|
// Redirect
|
|
submit(req);
|
|
return false;
|
|
}
|
|
|
|
void
|
|
ircd::m::fed::well_known::finish(request &req)
|
|
try
|
|
{
|
|
json::string result;
|
|
const unwind resolve{[&req, &result]
|
|
{
|
|
if(req.promise.valid())
|
|
req.promise.set_value(string_view
|
|
{
|
|
data(req.out), move(req.out, result?: req.target)
|
|
});
|
|
}};
|
|
|
|
if(req.code == 200 && json::valid(req.response, std::nothrow))
|
|
result = req.response["m.server"];
|
|
|
|
if(!result)
|
|
result = req.m_server;
|
|
|
|
if(!result)
|
|
result = req.target;
|
|
|
|
if(!rfc3986::valid_remote(std::nothrow, result))
|
|
result = req.target;
|
|
|
|
if(result != req.target)
|
|
log::debug
|
|
{
|
|
log, "query to %s for %s resolved delegation to %s",
|
|
req.uri.remote,
|
|
req.target,
|
|
result,
|
|
};
|
|
|
|
const bool cache_expired
|
|
{
|
|
req.expires < now<system_point>()
|
|
};
|
|
|
|
const bool cache_result
|
|
{
|
|
result
|
|
&& req.opts.cache_result
|
|
&& req.opts.request
|
|
&& (cache_expired || result != req.m_server)
|
|
};
|
|
|
|
req.m_server = result;
|
|
if(!cache_result)
|
|
return;
|
|
|
|
// Any time the well-known result is the same as the req.target (that
|
|
// includes legitimate errors where fetch_well_known() returns the
|
|
// req.target to default) we consider that an error and use the error
|
|
// TTL value. Sorry, no exponential backoff implemented yet.
|
|
const auto cache_ttl
|
|
{
|
|
req.target == req.m_server?
|
|
seconds(cache_error).count():
|
|
seconds(cache_default).count()
|
|
};
|
|
|
|
const m::room::id::buf cache_room_id
|
|
{
|
|
"dns", m::my_host()
|
|
};
|
|
|
|
// Write our record to the cache room; note that this doesn't really
|
|
// match the format of other DNS records in this room since it's a bit
|
|
// simpler, but we don't share the ircd.dns.rr type prefix anyway.
|
|
const auto cache_id
|
|
{
|
|
m::send(cache_room_id, m::me(), request::type, req.target, json::members
|
|
{
|
|
{ "ttl", cache_ttl },
|
|
{ "m.server", req.m_server },
|
|
})
|
|
};
|
|
|
|
log::debug
|
|
{
|
|
log, "%s cached delegation to %s with %s ttl:%ld",
|
|
req.target,
|
|
req.m_server,
|
|
string_view{cache_id},
|
|
cache_ttl,
|
|
};
|
|
}
|
|
catch(const std::exception &e)
|
|
{
|
|
log::error
|
|
{
|
|
log, "%s completion :%s",
|
|
req.target,
|
|
e.what(),
|
|
};
|
|
}
|
|
|
|
void
|
|
ircd::m::fed::well_known::receive(request &req)
|
|
{
|
|
assert(req.sopts.http_exceptions == false);
|
|
req.code = req.req.get(seconds(request::timeout));
|
|
req.head = req.req.in.gethead(req.req);
|
|
req.location = req.head.location;
|
|
req.response = json::object
|
|
{
|
|
req.req.in.content
|
|
};
|
|
|
|
log::debug
|
|
{
|
|
log, "fetch to %s %s :%u %s",
|
|
req.uri.remote,
|
|
req.uri.path,
|
|
uint(req.code),
|
|
http::status(req.code),
|
|
};
|
|
}
|
|
|
|
void
|
|
ircd::m::fed::well_known::submit(request &req)
|
|
{
|
|
const net::hostport target
|
|
{
|
|
make_remote(req.uri.remote)
|
|
};
|
|
|
|
const http::header headers[]
|
|
{
|
|
{ "User-Agent", info::user_agent },
|
|
};
|
|
|
|
window_buffer window
|
|
{
|
|
req.buf
|
|
};
|
|
|
|
http::request
|
|
{
|
|
window, host(target), "GET", req.uri.path, 0, {}, headers
|
|
};
|
|
|
|
server::out out;
|
|
out.head = window.completed();
|
|
|
|
// Remaining space in buffer is used for received head; note that below
|
|
// we specify this same buffer for in.content, but that's a trick
|
|
// recognized by ircd::server to place received content directly after
|
|
// head in this buffer without any additional dynamic allocation.
|
|
server::in in;
|
|
in.head = mutable_buffer{req.buf} + size(out.head);
|
|
in.content = in.head;
|
|
|
|
req.code = http::code(0);
|
|
req.head = {};
|
|
req.response = {};
|
|
req.location = {};
|
|
req.req = server::request
|
|
{
|
|
target, std::move(out), std::move(in), &req.sopts
|
|
};
|
|
|
|
worker_dock.notify();
|
|
}
|
|
|
|
ircd::net::hostport
|
|
ircd::m::fed::well_known::make_remote(const string_view &target)
|
|
{
|
|
const net::hostport remote
|
|
{
|
|
target
|
|
};
|
|
|
|
// Hard target https service; do not inherit any matrix service from remote.
|
|
const net::hostport ret
|
|
{
|
|
net::host(remote), "https", net::port(remote)
|
|
};
|
|
|
|
return ret;
|
|
}
|