2018-02-04 03:22:01 +01:00
|
|
|
// Matrix Construct
|
|
|
|
//
|
|
|
|
// Copyright (C) Matrix Construct Developers, Authors & Contributors
|
|
|
|
// Copyright (C) 2016-2018 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.
|
2016-09-06 01:05:16 +02:00
|
|
|
|
2017-12-24 08:34:11 +01:00
|
|
|
decltype(ircd::resource::resources)
|
|
|
|
ircd::resource::resources
|
2016-11-29 16:23:38 +01:00
|
|
|
{};
|
2016-09-06 01:05:16 +02:00
|
|
|
|
2017-08-23 23:06:14 +02:00
|
|
|
ircd::resource &
|
2018-04-24 04:11:11 +02:00
|
|
|
ircd::resource::find(const string_view &path_)
|
2017-08-23 23:06:14 +02:00
|
|
|
{
|
2018-04-24 04:11:11 +02:00
|
|
|
const string_view path
|
|
|
|
{
|
|
|
|
rstrip(path_, '/')
|
|
|
|
};
|
|
|
|
|
|
|
|
auto it
|
|
|
|
{
|
|
|
|
resources.lower_bound(path)
|
|
|
|
};
|
|
|
|
|
2017-08-23 23:47:54 +02:00
|
|
|
if(it == end(resources)) try
|
|
|
|
{
|
2017-10-03 13:13:52 +02:00
|
|
|
--it;
|
|
|
|
if(it == begin(resources) || !startswith(path, rstrip(it->first, '/')))
|
2017-10-16 06:26:05 +02:00
|
|
|
return *resources.at("/");
|
2017-08-23 23:47:54 +02:00
|
|
|
}
|
|
|
|
catch(const std::out_of_range &e)
|
|
|
|
{
|
2018-01-22 09:25:08 +01:00
|
|
|
throw http::error
|
|
|
|
{
|
|
|
|
http::code::NOT_FOUND
|
|
|
|
};
|
2017-08-23 23:47:54 +02:00
|
|
|
}
|
2016-09-06 01:05:16 +02:00
|
|
|
|
2018-04-24 04:11:11 +02:00
|
|
|
auto rpath
|
|
|
|
{
|
|
|
|
rstrip(it->first, '/')
|
|
|
|
};
|
|
|
|
|
2017-08-23 23:06:14 +02:00
|
|
|
// Exact file or directory match
|
2018-04-24 04:11:11 +02:00
|
|
|
if(path == rpath)
|
2017-08-23 23:06:14 +02:00
|
|
|
return *it->second;
|
|
|
|
|
|
|
|
// Directories handle all paths under them.
|
2018-04-24 04:11:11 +02:00
|
|
|
if(!startswith(path, rpath))
|
2017-08-23 23:06:14 +02:00
|
|
|
{
|
|
|
|
// Walk the iterator back to find if there is a directory prefixing this path.
|
|
|
|
if(it == begin(resources))
|
2018-01-22 09:25:08 +01:00
|
|
|
throw http::error
|
|
|
|
{
|
|
|
|
http::code::NOT_FOUND
|
|
|
|
};
|
2017-08-23 23:06:14 +02:00
|
|
|
|
|
|
|
--it;
|
2018-04-24 04:11:11 +02:00
|
|
|
rpath = rstrip(it->first, '/');
|
|
|
|
if(!startswith(path, rpath))
|
2018-01-22 09:25:08 +01:00
|
|
|
throw http::error
|
|
|
|
{
|
|
|
|
http::code::NOT_FOUND
|
|
|
|
};
|
2017-08-23 23:06:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the resource is a directory; if not, it can only
|
|
|
|
// handle exact path matches.
|
2018-04-24 04:11:11 +02:00
|
|
|
if(~it->second->flags & it->second->DIRECTORY && path != rpath)
|
2018-01-22 09:25:08 +01:00
|
|
|
throw http::error
|
|
|
|
{
|
|
|
|
http::code::NOT_FOUND
|
|
|
|
};
|
2017-08-23 23:06:14 +02:00
|
|
|
|
2018-04-24 04:11:11 +02:00
|
|
|
if(it->second->flags & it->second->DIRECTORY)
|
|
|
|
{
|
|
|
|
const auto rem(lstrip(path, rpath));
|
|
|
|
if(!empty(rem) && !startswith(rem, '/'))
|
|
|
|
throw http::error
|
|
|
|
{
|
|
|
|
http::code::NOT_FOUND
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-08-23 23:06:14 +02:00
|
|
|
return *it->second;
|
|
|
|
}
|
|
|
|
|
2017-12-24 08:34:11 +01:00
|
|
|
//
|
|
|
|
// resource
|
|
|
|
//
|
|
|
|
|
2017-12-12 21:26:39 +01:00
|
|
|
ircd::resource::resource(const string_view &path)
|
2017-08-23 23:06:14 +02:00
|
|
|
:resource
|
|
|
|
{
|
2017-12-12 21:26:39 +01:00
|
|
|
path, opts{}
|
2017-08-23 23:06:14 +02:00
|
|
|
}
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::resource(const string_view &path,
|
2017-12-12 21:26:39 +01:00
|
|
|
const opts &opts)
|
2017-08-23 23:06:14 +02:00
|
|
|
:path{path}
|
2017-12-12 21:26:39 +01:00
|
|
|
,description{opts.description}
|
2017-08-23 23:06:14 +02:00
|
|
|
,flags{opts.flags}
|
|
|
|
,resources_it{[this, &path]
|
2016-09-06 01:05:16 +02:00
|
|
|
{
|
2018-01-22 09:25:08 +01:00
|
|
|
const auto iit
|
|
|
|
{
|
|
|
|
resources.emplace(this->path, this)
|
|
|
|
};
|
|
|
|
|
2016-11-29 16:23:38 +01:00
|
|
|
if(!iit.second)
|
2018-01-22 09:25:08 +01:00
|
|
|
throw error
|
|
|
|
{
|
|
|
|
"resource \"%s\" already registered", path
|
|
|
|
};
|
2016-11-29 16:23:38 +01:00
|
|
|
|
2017-08-23 23:06:14 +02:00
|
|
|
return unique_const_iterator<decltype(resources)>
|
|
|
|
{
|
|
|
|
resources, iit.first
|
|
|
|
};
|
2016-11-29 16:23:38 +01:00
|
|
|
}()}
|
|
|
|
{
|
2018-01-26 02:29:37 +01:00
|
|
|
log::debug
|
2018-01-22 09:25:08 +01:00
|
|
|
{
|
|
|
|
"Registered resource \"%s\"", path.empty()? string_view{"/"} : path
|
|
|
|
};
|
2016-09-06 01:05:16 +02:00
|
|
|
}
|
|
|
|
|
2016-11-29 16:23:38 +01:00
|
|
|
ircd::resource::~resource()
|
2016-09-06 01:05:16 +02:00
|
|
|
noexcept
|
|
|
|
{
|
2018-01-26 02:29:37 +01:00
|
|
|
log::debug
|
2018-01-22 09:25:08 +01:00
|
|
|
{
|
|
|
|
"Unregistered resource \"%s\"", path.empty()? string_view{"/"} : path
|
|
|
|
};
|
2016-09-06 01:05:16 +02:00
|
|
|
}
|
|
|
|
|
2017-10-16 06:29:38 +02:00
|
|
|
namespace ircd
|
|
|
|
{
|
2018-04-15 01:21:18 +02:00
|
|
|
static void cache_warm_origin(const string_view &origin);
|
2018-03-11 19:27:21 +01:00
|
|
|
static bool verify_origin(client &client, resource::method &method, resource::request &request);
|
|
|
|
static bool authenticate(client &client, resource::method &method, resource::request &request);
|
2017-10-16 06:29:38 +02:00
|
|
|
}
|
2017-08-23 23:06:14 +02:00
|
|
|
|
2018-03-11 19:27:21 +01:00
|
|
|
bool
|
2017-10-16 06:29:38 +02:00
|
|
|
ircd::authenticate(client &client,
|
|
|
|
resource::method &method,
|
|
|
|
resource::request &request)
|
2017-08-23 23:06:14 +02:00
|
|
|
{
|
2018-02-11 07:07:06 +01:00
|
|
|
request.access_token =
|
2017-09-17 00:22:54 +02:00
|
|
|
{
|
2018-02-11 07:03:39 +01:00
|
|
|
request.query["access_token"]
|
2017-09-17 00:22:54 +02:00
|
|
|
};
|
|
|
|
|
2018-02-11 07:07:06 +01:00
|
|
|
if(empty(request.access_token))
|
2018-02-11 07:03:39 +01:00
|
|
|
{
|
|
|
|
const auto authorization
|
|
|
|
{
|
|
|
|
split(request.head.authorization, ' ')
|
|
|
|
};
|
|
|
|
|
|
|
|
if(iequals(authorization.first, "bearer"_sv))
|
2018-02-11 07:07:06 +01:00
|
|
|
request.access_token = authorization.second;
|
2018-02-11 07:03:39 +01:00
|
|
|
}
|
|
|
|
|
2018-03-11 19:27:21 +01:00
|
|
|
if(!request.access_token)
|
2017-08-23 23:06:14 +02:00
|
|
|
throw m::error
|
|
|
|
{
|
2018-03-11 19:27:21 +01:00
|
|
|
http::UNAUTHORIZED, "M_MISSING_TOKEN",
|
|
|
|
"Credentials for this method are required but missing."
|
2017-08-23 23:06:14 +02:00
|
|
|
};
|
2018-03-11 19:27:21 +01:00
|
|
|
|
|
|
|
return m::user::tokens.get(std::nothrow, "ircd.access_token", request.access_token, [&request]
|
|
|
|
(const m::event &event)
|
2017-08-23 23:06:14 +02:00
|
|
|
{
|
2018-03-11 19:27:21 +01:00
|
|
|
// The user sent this access token to the tokens room
|
|
|
|
request.user_id = m::user::id
|
|
|
|
{
|
|
|
|
at<"sender"_>(event)
|
|
|
|
};
|
|
|
|
});
|
2017-08-23 23:06:14 +02:00
|
|
|
}
|
|
|
|
|
2018-03-11 19:27:21 +01:00
|
|
|
bool
|
2017-10-16 06:29:38 +02:00
|
|
|
ircd::verify_origin(client &client,
|
|
|
|
resource::method &method,
|
|
|
|
resource::request &request)
|
2018-01-18 07:04:05 +01:00
|
|
|
try
|
2017-10-16 06:29:38 +02:00
|
|
|
{
|
2018-01-20 16:01:50 +01:00
|
|
|
const m::request::x_matrix x_matrix
|
2017-10-16 06:29:38 +02:00
|
|
|
{
|
|
|
|
request.head.authorization
|
|
|
|
};
|
|
|
|
|
2018-01-20 16:01:50 +01:00
|
|
|
const m::request object
|
2017-10-16 06:29:38 +02:00
|
|
|
{
|
2018-01-21 09:38:47 +01:00
|
|
|
x_matrix.origin, my_host(), method.name, request.head.uri, request.content
|
2017-10-16 06:29:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const auto verified
|
|
|
|
{
|
2018-01-20 16:01:50 +01:00
|
|
|
object.verify(x_matrix.key, x_matrix.sig)
|
2017-10-16 06:29:38 +02:00
|
|
|
};
|
|
|
|
|
2018-04-03 07:58:12 +02:00
|
|
|
request.origin = x_matrix.origin;
|
2018-03-11 19:27:21 +01:00
|
|
|
return verified;
|
2018-01-18 07:04:05 +01:00
|
|
|
}
|
|
|
|
catch(const std::exception &e)
|
|
|
|
{
|
2018-03-15 20:26:29 +01:00
|
|
|
log::derror
|
2018-01-22 09:25:08 +01:00
|
|
|
{
|
|
|
|
"X-Matrix Authorization from %s: %s",
|
|
|
|
string(remote(client)),
|
|
|
|
e.what()
|
|
|
|
};
|
2018-01-20 16:01:50 +01:00
|
|
|
|
2018-01-18 07:04:05 +01:00
|
|
|
throw m::error
|
|
|
|
{
|
2018-01-20 16:01:50 +01:00
|
|
|
http::UNAUTHORIZED, "M_UNKNOWN_ERROR",
|
2018-01-18 07:04:05 +01:00
|
|
|
"An error has prevented authorization: %s",
|
|
|
|
e.what()
|
|
|
|
};
|
2017-10-16 06:29:38 +02:00
|
|
|
}
|
2017-08-23 23:06:14 +02:00
|
|
|
|
2018-04-15 01:21:18 +02:00
|
|
|
ircd::conf::item<ircd::seconds>
|
|
|
|
cache_warmup_time
|
|
|
|
{
|
|
|
|
{ "name", "ircd.cache_warmup_time" },
|
|
|
|
{ "default", 3600L },
|
|
|
|
};
|
|
|
|
|
|
|
|
/// We can smoothly warmup some memory caches after daemon startup as the
|
|
|
|
/// requests trickle in from remote servers. This function is invoked after
|
|
|
|
/// a remote contacts and reveals its identity with the X-Matrix verification.
|
|
|
|
///
|
|
|
|
/// This process helps us avoid cold caches for the first requests coming from
|
|
|
|
/// our server. Such requests are often parallel requests, for ex. to hundreds
|
|
|
|
/// of servers in a Matrix room at the same time.
|
|
|
|
///
|
|
|
|
/// This function does nothing after the cache warmup period has ended.
|
|
|
|
void
|
|
|
|
ircd::cache_warm_origin(const string_view &origin)
|
|
|
|
try
|
|
|
|
{
|
|
|
|
if(ircd::uptime() > seconds(cache_warmup_time))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Make a query through SRV and A records.
|
|
|
|
net::dns(origin, net::dns::prefetch_ipport);
|
|
|
|
}
|
|
|
|
catch(const std::exception &e)
|
|
|
|
{
|
|
|
|
log::derror
|
|
|
|
{
|
|
|
|
"Cache warming for '%s' :%s", origin, e.what()
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-09-06 01:05:16 +02:00
|
|
|
void
|
2016-11-29 16:23:38 +01:00
|
|
|
ircd::resource::operator()(client &client,
|
2018-02-12 20:58:40 +01:00
|
|
|
const http::request::head &head,
|
|
|
|
const string_view &content_partial)
|
2016-09-06 01:05:16 +02:00
|
|
|
{
|
2018-01-12 03:45:25 +01:00
|
|
|
// Find the method or METHOD_NOT_ALLOWED
|
|
|
|
auto &method
|
|
|
|
{
|
|
|
|
operator[](head.method)
|
|
|
|
};
|
|
|
|
|
|
|
|
// Bail out if the method limited the amount of content and it was exceeded.
|
|
|
|
if(head.content_length > method.opts.payload_max)
|
|
|
|
throw http::error
|
|
|
|
{
|
|
|
|
http::PAYLOAD_TOO_LARGE
|
|
|
|
};
|
|
|
|
|
2018-04-16 01:42:13 +02:00
|
|
|
// This timer will keep the request from hanging forever for whatever
|
|
|
|
// reason. The resource method may want to do its own timing and can
|
|
|
|
// disable this in its options structure.
|
|
|
|
const net::scope_timeout timeout
|
|
|
|
{
|
|
|
|
*client.sock, method.opts.timeout, [&client, &head]
|
|
|
|
(const bool &timed_out)
|
|
|
|
{
|
|
|
|
if(!timed_out)
|
|
|
|
return;
|
|
|
|
|
|
|
|
log::derror
|
|
|
|
{
|
|
|
|
"socket(%p) local[%s] remote[%s] Timed out in %s `%s'",
|
|
|
|
client.sock.get(),
|
|
|
|
string(local(client)),
|
|
|
|
string(remote(client)),
|
|
|
|
head.method,
|
|
|
|
head.path
|
|
|
|
};
|
|
|
|
|
|
|
|
//TODO: If we know that no response has been sent yet
|
|
|
|
//TODO: we can respond with http::REQUEST_TIMEOUT instead.
|
|
|
|
client.close(net::dc::RST, net::close_ignore);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-02-17 23:28:06 +01:00
|
|
|
// Content that hasn't yet arrived is remaining
|
2018-01-12 03:45:25 +01:00
|
|
|
const size_t content_remain
|
|
|
|
{
|
2018-02-12 20:58:40 +01:00
|
|
|
head.content_length - client.content_consumed
|
2018-01-12 03:45:25 +01:00
|
|
|
};
|
|
|
|
|
2018-02-17 23:28:06 +01:00
|
|
|
// View of the content that will be passed to the resource handler. Starts
|
|
|
|
// with the content received so far which is actually in the head's buffer.
|
|
|
|
// One of three things can happen now:
|
|
|
|
//
|
|
|
|
// - There is no more content so we pass this as-is right to the resource.
|
|
|
|
// - There is more content, so we allocate a content buffer, copy what we
|
|
|
|
// have to it, read the rest off the socket, and then reassign this view.
|
|
|
|
// - There is more content, but the resource wants to read it off the
|
|
|
|
// socket on its own terms, so we pass this as-is.
|
|
|
|
string_view content
|
|
|
|
{
|
|
|
|
content_partial
|
|
|
|
};
|
|
|
|
|
2018-02-17 23:32:11 +01:00
|
|
|
if(content_remain && ~method.opts.flags & method.CONTENT_DISCRETION)
|
2018-01-12 03:45:25 +01:00
|
|
|
{
|
|
|
|
// Copy any partial content to the final contiguous allocated buffer;
|
2018-02-17 23:28:06 +01:00
|
|
|
client.content_buffer = unique_buffer<mutable_buffer>{head.content_length};
|
|
|
|
memcpy(data(client.content_buffer), data(content_partial), size(content_partial));
|
2018-01-12 03:45:25 +01:00
|
|
|
|
|
|
|
// Setup a window inside the buffer for the remaining socket read.
|
|
|
|
const mutable_buffer content_remain_buffer
|
|
|
|
{
|
2018-02-17 23:28:06 +01:00
|
|
|
data(client.content_buffer) + size(content_partial), content_remain
|
2018-01-12 03:45:25 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// Read the remaining content off the socket.
|
2018-02-12 20:58:40 +01:00
|
|
|
client.content_consumed += read_all(*client.sock, content_remain_buffer);
|
|
|
|
assert(client.content_consumed == head.content_length);
|
2018-01-12 03:45:25 +01:00
|
|
|
content = string_view
|
|
|
|
{
|
2018-02-17 23:28:06 +01:00
|
|
|
data(client.content_buffer), head.content_length
|
2018-01-12 03:45:25 +01:00
|
|
|
};
|
|
|
|
}
|
2017-10-12 05:52:33 +02:00
|
|
|
|
2018-04-20 06:45:39 +02:00
|
|
|
client.request = resource::request
|
|
|
|
{
|
|
|
|
head, content
|
|
|
|
};
|
|
|
|
|
2018-04-14 01:52:38 +02:00
|
|
|
// We take the extra step here to clear the assignment to client.request
|
|
|
|
// when this request stack has finished for two reasons:
|
|
|
|
// - It allows other ctxs to peep at the client::list to see what this
|
|
|
|
// client/ctx/request is currently working on with some more safety.
|
|
|
|
// - It prevents an easy source for stale refs wrt the longpoll thing.
|
|
|
|
const unwind clear_request{[&client]
|
|
|
|
{
|
|
|
|
client.request = {};
|
|
|
|
}};
|
|
|
|
|
2018-02-18 00:44:53 +01:00
|
|
|
const auto pathparm
|
2017-10-12 05:52:33 +02:00
|
|
|
{
|
2018-02-18 00:44:53 +01:00
|
|
|
lstrip(head.path, this->path)
|
2017-10-12 05:52:33 +02:00
|
|
|
};
|
|
|
|
|
2018-02-18 00:44:53 +01:00
|
|
|
client.request.parv =
|
2017-03-11 02:46:25 +01:00
|
|
|
{
|
2018-02-18 00:44:53 +01:00
|
|
|
client.request.param, tokens(pathparm, '/', client.request.param)
|
2017-03-11 02:46:25 +01:00
|
|
|
};
|
|
|
|
|
2018-01-12 03:45:25 +01:00
|
|
|
if(method.opts.flags & method.REQUIRES_AUTH)
|
2018-03-11 19:27:21 +01:00
|
|
|
if(!authenticate(client, method, client.request))
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::UNAUTHORIZED, "M_UNKNOWN_TOKEN",
|
|
|
|
"Credentials for this method are required but invalid."
|
|
|
|
};
|
2017-08-23 23:06:14 +02:00
|
|
|
|
2018-01-12 03:45:25 +01:00
|
|
|
if(method.opts.flags & method.VERIFY_ORIGIN)
|
2018-04-15 01:21:18 +02:00
|
|
|
{
|
2018-03-11 19:27:21 +01:00
|
|
|
if(!verify_origin(client, method, client.request))
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::UNAUTHORIZED, "M_INVALID_SIGNATURE",
|
|
|
|
"The X-Matrix Authorization is invalid."
|
|
|
|
};
|
2017-08-23 23:06:14 +02:00
|
|
|
|
2018-05-02 22:16:40 +02:00
|
|
|
// If we have an error cached from previously not being able to
|
|
|
|
// contact this origin we can clear that now that they're alive.
|
|
|
|
server::errclear(client.request.origin);
|
2018-04-15 01:21:18 +02:00
|
|
|
|
|
|
|
// The origin was verified so we can invoke the cache warming now.
|
|
|
|
cache_warm_origin(client.request.origin);
|
|
|
|
}
|
|
|
|
|
2018-02-18 00:44:53 +01:00
|
|
|
handle_request(client, method, client.request);
|
2017-03-21 03:30:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2017-08-23 23:06:14 +02:00
|
|
|
ircd::resource::handle_request(client &client,
|
|
|
|
method &method,
|
|
|
|
resource::request &request)
|
2017-03-21 03:30:07 +01:00
|
|
|
try
|
|
|
|
{
|
2018-03-11 18:17:37 +01:00
|
|
|
method(client, request);
|
2017-03-21 03:30:07 +01:00
|
|
|
}
|
2017-09-15 21:07:07 +02:00
|
|
|
catch(const json::not_found &e)
|
2017-03-21 03:30:07 +01:00
|
|
|
{
|
|
|
|
throw m::error
|
|
|
|
{
|
2018-03-08 16:40:41 +01:00
|
|
|
http::NOT_FOUND, "M_BAD_JSON", "Required JSON field: %s", e.what()
|
2017-03-21 03:30:07 +01:00
|
|
|
};
|
2016-09-06 01:05:16 +02:00
|
|
|
}
|
2017-09-15 21:07:07 +02:00
|
|
|
catch(const json::print_error &e)
|
|
|
|
{
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::INTERNAL_SERVER_ERROR, "M_NOT_JSON", "Generator Protection: %s", e.what()
|
|
|
|
};
|
|
|
|
}
|
|
|
|
catch(const json::error &e)
|
|
|
|
{
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::BAD_REQUEST, "M_NOT_JSON", "%s", e.what()
|
|
|
|
};
|
|
|
|
}
|
2017-09-25 03:05:42 +02:00
|
|
|
catch(const std::out_of_range &e)
|
|
|
|
{
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::NOT_FOUND, "M_NOT_FOUND", "%s", e.what()
|
|
|
|
};
|
|
|
|
}
|
2018-03-09 19:54:29 +01:00
|
|
|
catch(const ctx::timeout &e)
|
|
|
|
{
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::BAD_GATEWAY, "M_REQUEST_TIMEOUT", "%s", e.what()
|
|
|
|
};
|
|
|
|
}
|
2016-09-06 01:05:16 +02:00
|
|
|
|
2017-10-16 06:29:38 +02:00
|
|
|
ircd::resource::method &
|
|
|
|
ircd::resource::operator[](const string_view &name)
|
|
|
|
try
|
|
|
|
{
|
|
|
|
return *methods.at(name);
|
|
|
|
}
|
|
|
|
catch(const std::out_of_range &e)
|
2018-03-11 18:17:37 +01:00
|
|
|
{
|
|
|
|
char buf[128];
|
|
|
|
const http::header headers[]
|
|
|
|
{
|
|
|
|
{ "Allow", allow_methods_list(buf) }
|
|
|
|
};
|
|
|
|
|
|
|
|
throw http::error
|
|
|
|
{
|
|
|
|
http::METHOD_NOT_ALLOWED, {}, headers
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::string_view
|
|
|
|
ircd::resource::allow_methods_list(const mutable_buffer &buf)
|
2017-10-16 06:29:38 +02:00
|
|
|
{
|
2017-12-24 22:35:36 +01:00
|
|
|
size_t len(0);
|
2018-03-11 18:17:37 +01:00
|
|
|
if(likely(size(buf)))
|
|
|
|
buf[len] = '\0';
|
|
|
|
|
2017-12-24 22:35:36 +01:00
|
|
|
auto it(begin(methods));
|
|
|
|
if(it != end(methods))
|
|
|
|
{
|
|
|
|
len = strlcat(buf, it->first);
|
|
|
|
for(++it; it != end(methods); ++it)
|
|
|
|
{
|
|
|
|
len = strlcat(buf, " ");
|
|
|
|
len = strlcat(buf, it->first);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-11 18:17:37 +01:00
|
|
|
return { data(buf), len };
|
2017-10-16 06:29:38 +02:00
|
|
|
}
|
|
|
|
|
2018-01-12 03:45:25 +01:00
|
|
|
ircd::resource::method::method(struct resource &resource,
|
|
|
|
const string_view &name,
|
|
|
|
const handler &handler)
|
|
|
|
:method
|
|
|
|
{
|
|
|
|
resource, name, handler, {}
|
|
|
|
}
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2016-11-29 16:23:38 +01:00
|
|
|
ircd::resource::method::method(struct resource &resource,
|
2017-08-23 23:06:14 +02:00
|
|
|
const string_view &name,
|
2016-11-29 16:23:38 +01:00
|
|
|
const handler &handler,
|
2018-01-12 03:45:25 +01:00
|
|
|
const struct opts &opts)
|
2017-08-23 23:06:14 +02:00
|
|
|
:name{name}
|
2016-11-29 16:23:38 +01:00
|
|
|
,resource{&resource}
|
2017-08-23 23:06:14 +02:00
|
|
|
,function{handler}
|
2018-01-12 03:45:25 +01:00
|
|
|
,opts{opts}
|
2016-11-29 16:23:38 +01:00
|
|
|
,methods_it{[this, &name]
|
2016-09-06 01:05:16 +02:00
|
|
|
{
|
2018-01-22 09:25:08 +01:00
|
|
|
const auto iit
|
|
|
|
{
|
|
|
|
this->resource->methods.emplace(this->name, this)
|
|
|
|
};
|
|
|
|
|
2016-09-06 01:05:16 +02:00
|
|
|
if(!iit.second)
|
2018-01-22 09:25:08 +01:00
|
|
|
throw error
|
|
|
|
{
|
|
|
|
"resource \"%s\" already registered", name
|
|
|
|
};
|
2016-09-06 01:05:16 +02:00
|
|
|
|
2017-08-23 23:06:14 +02:00
|
|
|
return unique_const_iterator<decltype(resource::methods)>
|
|
|
|
{
|
|
|
|
this->resource->methods,
|
|
|
|
iit.first
|
|
|
|
};
|
2016-11-29 16:23:38 +01:00
|
|
|
}()}
|
|
|
|
{
|
2016-09-06 01:05:16 +02:00
|
|
|
}
|
|
|
|
|
2016-11-29 16:23:38 +01:00
|
|
|
ircd::resource::method::~method()
|
|
|
|
noexcept
|
2016-09-06 01:05:16 +02:00
|
|
|
{
|
2017-08-23 23:06:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response
|
|
|
|
ircd::resource::method::operator()(client &client,
|
|
|
|
request &request)
|
|
|
|
try
|
|
|
|
{
|
|
|
|
return function(client, request);
|
|
|
|
}
|
|
|
|
catch(const std::bad_function_call &e)
|
|
|
|
{
|
|
|
|
throw http::error
|
|
|
|
{
|
|
|
|
http::SERVICE_UNAVAILABLE
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-04-14 08:19:43 +02:00
|
|
|
//
|
|
|
|
// resource::response::chunked
|
|
|
|
//
|
|
|
|
|
|
|
|
ircd::resource::response::chunked::chunked(chunked &&other)
|
|
|
|
noexcept
|
|
|
|
:c{std::move(other.c)}
|
|
|
|
{
|
|
|
|
other.c = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::chunked::chunked(client &client,
|
|
|
|
const http::code &code)
|
|
|
|
:chunked
|
|
|
|
{
|
|
|
|
client, code, "application/json; charset=utf-8"_sv, string_view{}
|
|
|
|
}
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::chunked::chunked(client &client,
|
|
|
|
const http::code &code,
|
|
|
|
const vector_view<const http::header> &headers)
|
|
|
|
:chunked
|
|
|
|
{
|
|
|
|
client, code, "application/json; charset=utf-8"_sv, headers
|
|
|
|
}
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::chunked::chunked(client &client,
|
|
|
|
const http::code &code,
|
|
|
|
const string_view &content_type,
|
|
|
|
const vector_view<const http::header> &headers)
|
|
|
|
:c{&client}
|
|
|
|
{
|
|
|
|
assert(!empty(content_type));
|
|
|
|
|
|
|
|
thread_local char buffer[4_KiB];
|
|
|
|
window_buffer sb{buffer};
|
|
|
|
{
|
|
|
|
const critical_assertion ca;
|
|
|
|
http::write(sb, headers);
|
|
|
|
}
|
|
|
|
|
|
|
|
response
|
|
|
|
{
|
|
|
|
client, code, content_type, size_t(-1), string_view{sb.completed()}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::chunked::chunked(client &client,
|
|
|
|
const http::code &code,
|
|
|
|
const string_view &content_type,
|
|
|
|
const string_view &headers)
|
|
|
|
:c{&client}
|
|
|
|
{
|
|
|
|
response
|
|
|
|
{
|
|
|
|
client, code, content_type, size_t(-1), headers
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::chunked::~chunked()
|
|
|
|
noexcept try
|
|
|
|
{
|
|
|
|
if(!c)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if(!std::uncaught_exceptions())
|
|
|
|
finish();
|
|
|
|
else
|
|
|
|
c->close(net::dc::RST, net::close_ignore);
|
|
|
|
}
|
|
|
|
catch(...)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
ircd::resource::response::chunked::finish()
|
|
|
|
{
|
|
|
|
if(!c)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
write(const_buffer{});
|
|
|
|
c = nullptr;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t
|
|
|
|
ircd::resource::response::chunked::write(const const_buffer &chunk)
|
|
|
|
try
|
|
|
|
{
|
|
|
|
size_t ret{0};
|
|
|
|
|
|
|
|
if(!c)
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
//TODO: bring iov from net::socket -> net::write_() -> client::write_()
|
|
|
|
char headbuf[32];
|
|
|
|
ret += c->write_all(http::writechunk(headbuf, size(chunk)));
|
|
|
|
ret += size(chunk)? c->write_all(chunk) : 0UL;
|
|
|
|
ret += c->write_all("\r\n"_sv);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
catch(...)
|
|
|
|
{
|
|
|
|
this->c = nullptr;
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// resource::response
|
|
|
|
//
|
|
|
|
|
2017-09-25 02:00:05 +02:00
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const http::code &code)
|
2018-04-05 03:40:06 +02:00
|
|
|
:response{client, json::object{json::empty_object}, code}
|
2017-09-25 02:00:05 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2017-08-23 23:06:14 +02:00
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const http::code &code,
|
2017-09-09 21:20:00 +02:00
|
|
|
const json::iov &members)
|
2017-09-08 17:15:14 +02:00
|
|
|
:response{client, members, code}
|
2017-08-23 23:06:14 +02:00
|
|
|
{
|
2016-09-06 01:05:16 +02:00
|
|
|
}
|
2016-11-29 16:23:38 +01:00
|
|
|
|
2017-09-12 22:34:21 +02:00
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const json::members &members,
|
|
|
|
const http::code &code)
|
|
|
|
:response{client, code, members}
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2017-09-25 02:00:05 +02:00
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const json::value &value,
|
|
|
|
const http::code &code)
|
|
|
|
:response{client, code, value}
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const http::code &code,
|
|
|
|
const json::value &value)
|
|
|
|
try
|
|
|
|
{
|
|
|
|
const auto size
|
|
|
|
{
|
|
|
|
serialized(value)
|
|
|
|
};
|
|
|
|
|
2017-10-28 21:31:26 +02:00
|
|
|
const unique_buffer<mutable_buffer> buffer
|
2017-09-25 02:00:05 +02:00
|
|
|
{
|
2017-10-28 21:31:26 +02:00
|
|
|
size
|
2017-09-25 02:00:05 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
switch(type(value))
|
|
|
|
{
|
|
|
|
case json::ARRAY:
|
|
|
|
{
|
2017-10-28 21:31:26 +02:00
|
|
|
response(client, json::array{stringify(mutable_buffer{buffer}, value)}, code);
|
2017-09-25 02:00:05 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
case json::OBJECT:
|
|
|
|
{
|
2017-10-28 21:31:26 +02:00
|
|
|
response(client, json::object{stringify(mutable_buffer{buffer}, value)}, code);
|
2017-09-25 02:00:05 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
default: throw m::error
|
|
|
|
{
|
|
|
|
http::INTERNAL_SERVER_ERROR, "M_NOT_JSON", "Cannot send json::%s as response content", type(value)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch(const json::error &e)
|
|
|
|
{
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::INTERNAL_SERVER_ERROR, "M_NOT_JSON", "Generator Protection: %s", e.what()
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-09-12 22:34:21 +02:00
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const http::code &code,
|
|
|
|
const json::members &members)
|
2017-09-25 02:00:05 +02:00
|
|
|
try
|
2017-09-12 22:34:21 +02:00
|
|
|
{
|
2017-09-15 21:07:07 +02:00
|
|
|
const auto size
|
|
|
|
{
|
|
|
|
serialized(members)
|
|
|
|
};
|
|
|
|
|
2017-10-28 21:31:26 +02:00
|
|
|
const unique_buffer<mutable_buffer> buffer
|
|
|
|
{
|
|
|
|
size
|
|
|
|
};
|
|
|
|
|
2017-09-15 21:07:07 +02:00
|
|
|
const json::object object
|
|
|
|
{
|
2017-10-28 21:31:26 +02:00
|
|
|
stringify(mutable_buffer{buffer}, members)
|
2017-09-15 21:07:07 +02:00
|
|
|
};
|
2017-09-12 22:34:21 +02:00
|
|
|
|
2017-09-15 21:07:07 +02:00
|
|
|
response(client, object, code);
|
2017-09-12 22:34:21 +02:00
|
|
|
}
|
2017-09-25 02:00:05 +02:00
|
|
|
catch(const json::error &e)
|
2017-09-09 21:20:00 +02:00
|
|
|
{
|
2017-09-25 02:00:05 +02:00
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::INTERNAL_SERVER_ERROR, "M_NOT_JSON", "Generator Protection: %s", e.what()
|
|
|
|
};
|
2017-09-09 21:20:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const json::iov &members,
|
2017-03-11 04:31:20 +01:00
|
|
|
const http::code &code)
|
2017-03-21 03:30:07 +01:00
|
|
|
try
|
2016-09-06 01:05:16 +02:00
|
|
|
{
|
2017-09-15 21:07:07 +02:00
|
|
|
const auto size
|
|
|
|
{
|
|
|
|
serialized(members)
|
|
|
|
};
|
|
|
|
|
2017-10-28 21:31:26 +02:00
|
|
|
const unique_buffer<mutable_buffer> buffer
|
|
|
|
{
|
|
|
|
size
|
|
|
|
};
|
|
|
|
|
2017-09-15 21:07:07 +02:00
|
|
|
const json::object object
|
|
|
|
{
|
2017-10-28 21:31:26 +02:00
|
|
|
stringify(mutable_buffer{buffer}, members)
|
2017-09-15 21:07:07 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
response(client, object, code);
|
2017-03-21 03:30:07 +01:00
|
|
|
}
|
|
|
|
catch(const json::error &e)
|
|
|
|
{
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::INTERNAL_SERVER_ERROR, "M_NOT_JSON", "Generator Protection: %s", e.what()
|
|
|
|
};
|
2017-03-11 04:31:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::response(client &client,
|
2017-08-23 23:48:28 +02:00
|
|
|
const json::object &object,
|
2017-03-11 04:31:20 +01:00
|
|
|
const http::code &code)
|
|
|
|
{
|
2018-02-22 02:41:28 +01:00
|
|
|
static const string_view content_type
|
2017-03-11 04:31:20 +01:00
|
|
|
{
|
2017-08-23 23:06:14 +02:00
|
|
|
"application/json; charset=utf-8"
|
2017-03-11 04:31:20 +01:00
|
|
|
};
|
2016-09-06 01:05:16 +02:00
|
|
|
|
2017-11-30 19:31:13 +01:00
|
|
|
assert(json::valid(object, std::nothrow));
|
2017-08-23 23:48:28 +02:00
|
|
|
response(client, object, content_type, code);
|
2016-09-06 01:05:16 +02:00
|
|
|
}
|
|
|
|
|
2017-09-25 02:00:05 +02:00
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const json::array &array,
|
|
|
|
const http::code &code)
|
|
|
|
{
|
2018-02-22 02:41:28 +01:00
|
|
|
static const string_view content_type
|
2017-09-25 02:00:05 +02:00
|
|
|
{
|
|
|
|
"application/json; charset=utf-8"
|
|
|
|
};
|
|
|
|
|
2017-11-30 19:31:13 +01:00
|
|
|
assert(json::valid(array, std::nothrow));
|
2017-09-25 02:00:05 +02:00
|
|
|
response(client, array, content_type, code);
|
|
|
|
}
|
|
|
|
|
2017-08-23 23:06:14 +02:00
|
|
|
ircd::resource::response::response(client &client,
|
2017-12-23 01:40:44 +01:00
|
|
|
const string_view &content,
|
2017-08-23 23:06:14 +02:00
|
|
|
const string_view &content_type,
|
2017-12-24 22:25:09 +01:00
|
|
|
const http::code &code,
|
|
|
|
const vector_view<const http::header> &headers)
|
|
|
|
{
|
2018-03-09 20:35:55 +01:00
|
|
|
assert(empty(content) || !empty(content_type));
|
|
|
|
|
2018-01-12 03:41:27 +01:00
|
|
|
// contents of this buffer get copied again when further passed to
|
|
|
|
// response{}; we can get this off the stack if that remains true.
|
2018-03-05 11:41:24 +01:00
|
|
|
thread_local char buffer[4_KiB];
|
2018-02-08 06:30:20 +01:00
|
|
|
window_buffer sb{buffer};
|
2018-01-12 03:41:27 +01:00
|
|
|
{
|
|
|
|
const critical_assertion ca;
|
|
|
|
http::write(sb, headers);
|
|
|
|
}
|
|
|
|
|
2018-03-09 22:15:32 +01:00
|
|
|
response
|
2017-12-24 22:25:09 +01:00
|
|
|
{
|
2018-03-05 11:41:24 +01:00
|
|
|
client, content, content_type, code, string_view{sb.completed()}
|
2017-12-24 22:25:09 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const string_view &content,
|
|
|
|
const string_view &content_type,
|
|
|
|
const http::code &code,
|
|
|
|
const string_view &headers)
|
2018-03-08 20:42:43 +01:00
|
|
|
{
|
2018-03-09 20:35:55 +01:00
|
|
|
assert(empty(content) || !empty(content_type));
|
|
|
|
|
2018-03-08 20:42:43 +01:00
|
|
|
// Head gets sent
|
|
|
|
response
|
|
|
|
{
|
2018-03-09 22:15:32 +01:00
|
|
|
client, code, content_type, size(content), headers
|
2018-03-08 20:42:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// All content gets sent
|
2018-03-11 18:48:58 +01:00
|
|
|
const size_t written
|
2018-03-08 20:42:43 +01:00
|
|
|
{
|
2018-03-11 18:48:58 +01:00
|
|
|
client.write_all(content)
|
2018-03-08 20:42:43 +01:00
|
|
|
};
|
|
|
|
|
2018-03-11 18:48:58 +01:00
|
|
|
assert(written == size(content));
|
2018-03-08 20:42:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
ircd::resource::response::response(client &client,
|
|
|
|
const http::code &code,
|
2018-03-09 22:15:32 +01:00
|
|
|
const string_view &content_type,
|
|
|
|
const size_t &content_length,
|
2018-03-08 20:42:43 +01:00
|
|
|
const string_view &headers)
|
2016-09-06 01:05:16 +02:00
|
|
|
{
|
2018-03-09 20:35:55 +01:00
|
|
|
assert(!content_length || !empty(content_type));
|
|
|
|
|
2017-09-25 02:00:05 +02:00
|
|
|
const auto request_time
|
|
|
|
{
|
2018-02-12 20:58:40 +01:00
|
|
|
client.timer.at<microseconds>().count()
|
2017-09-30 08:09:03 +02:00
|
|
|
};
|
|
|
|
|
2017-12-24 22:25:09 +01:00
|
|
|
const fmt::bsprintf<64> rtime
|
2017-09-30 08:09:03 +02:00
|
|
|
{
|
2018-01-12 03:46:04 +01:00
|
|
|
"%zd$us", request_time
|
2017-12-24 22:25:09 +01:00
|
|
|
};
|
|
|
|
|
2018-01-12 03:41:27 +01:00
|
|
|
// This buffer will be passed to the socket and sent out;
|
|
|
|
// cannot be static/tls.
|
2018-03-05 11:41:24 +01:00
|
|
|
char head_buf[4_KiB];
|
2018-02-08 06:30:20 +01:00
|
|
|
window_buffer head{head_buf};
|
2017-08-23 23:06:14 +02:00
|
|
|
http::response
|
|
|
|
{
|
2017-12-23 01:40:44 +01:00
|
|
|
head,
|
|
|
|
code,
|
2018-03-08 20:42:43 +01:00
|
|
|
content_length,
|
2017-12-23 01:40:44 +01:00
|
|
|
content_type,
|
2017-12-24 22:25:09 +01:00
|
|
|
headers,
|
2017-08-23 23:06:14 +02:00
|
|
|
{
|
2018-01-12 03:46:04 +01:00
|
|
|
{ "Access-Control-Allow-Origin", "*" }, //TODO: XXX
|
|
|
|
{ "X-IRCd-Request-Timer", rtime, },
|
2017-12-24 22:25:09 +01:00
|
|
|
},
|
2017-08-23 23:06:14 +02:00
|
|
|
};
|
|
|
|
|
2018-03-08 20:42:43 +01:00
|
|
|
// Maximum size is is realistically ok but ideally a small
|
2018-01-12 03:46:04 +01:00
|
|
|
// maximum; this exception should hit the developer in testing.
|
|
|
|
if(unlikely(!head.remaining()))
|
|
|
|
throw assertive
|
|
|
|
{
|
|
|
|
"HTTP headers too large for buffer of %zu", sizeof(head_buf)
|
|
|
|
};
|
|
|
|
|
2018-03-11 18:48:58 +01:00
|
|
|
const size_t written
|
2017-12-23 01:40:44 +01:00
|
|
|
{
|
2018-03-11 18:48:58 +01:00
|
|
|
client.write_all(head.completed())
|
2017-12-23 01:40:44 +01:00
|
|
|
};
|
|
|
|
|
2018-04-24 02:47:10 +02:00
|
|
|
#ifdef RB_DEBUG
|
|
|
|
const log::facility facility
|
|
|
|
{
|
|
|
|
ushort(code) >= 200 && ushort(code) < 300?
|
|
|
|
log::facility::DEBUG:
|
|
|
|
ushort(code) >= 300 && ushort(code) < 400?
|
|
|
|
log::facility::DWARNING:
|
|
|
|
ushort(code) >= 400 && ushort(code) < 500?
|
|
|
|
log::facility::DERROR:
|
|
|
|
|
|
|
|
log::facility::ERROR
|
|
|
|
};
|
|
|
|
|
|
|
|
log::logf
|
2018-01-22 09:25:08 +01:00
|
|
|
{
|
2018-04-24 02:47:10 +02:00
|
|
|
log::general, facility,
|
2018-04-05 03:40:06 +02:00
|
|
|
"socket(%p) local[%s] remote[%s] HTTP %d %s in %ld$us; %s %zd content",
|
2018-01-22 09:25:08 +01:00
|
|
|
client.sock.get(),
|
|
|
|
string(local(client)),
|
|
|
|
string(remote(client)),
|
2018-03-11 18:23:06 +01:00
|
|
|
uint(code),
|
2018-01-22 09:25:08 +01:00
|
|
|
http::status(code),
|
|
|
|
request_time,
|
|
|
|
content_type,
|
2018-04-05 03:40:06 +02:00
|
|
|
ssize_t(content_length),
|
2018-01-22 09:25:08 +01:00
|
|
|
};
|
2018-04-24 02:47:10 +02:00
|
|
|
#endif
|
2018-03-11 18:48:58 +01:00
|
|
|
|
|
|
|
assert(written == size(head.completed()));
|
2016-09-06 01:05:16 +02:00
|
|
|
}
|