1
0
Fork 0
mirror of https://github.com/transmission/transmission synced 2024-12-22 07:42:37 +00:00

refactor: use evhttp public accessors in rpc server (#7112)

* refactor: use libevent public accessors in `handle_request()`

* chore: housekeeping in `handle_request()`

* refactor: use libevent public accessors in `make_response()`

* refactor: use libevent public accessors in `serve_file()`

* refactor: use libevent public accessors in `handle_web_client()`

* refactor: use libevent public accessors in `handle_rpc_from_json()`

* refactor: use libevent public accessors in `handle_rpc()`

* refactor: use libevent public accessors in `isHostnameAllowed()`

* refactor: use libevent public accessors in `test_session_id()`
This commit is contained in:
Yat Ho 2024-12-14 04:15:30 +08:00 committed by GitHub
parent 1e9a2986ee
commit b4bbc8744e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -26,7 +26,6 @@
#include <event2/buffer.h>
#include <event2/http.h>
#include <event2/http_struct.h> /* TODO: eventually remove this */
#include <event2/listener.h>
#include <fmt/core.h>
@ -224,9 +223,10 @@ void send_simple_response(struct evhttp_request* req, int code, char const* text
[[nodiscard]] evbuffer* make_response(struct evhttp_request* req, tr_rpc_server const* server, std::string_view content)
{
auto* const out = evbuffer_new();
auto const* const input_headers = evhttp_request_get_input_headers(req);
auto* const output_headers = evhttp_request_get_output_headers(req);
char const* key = "Accept-Encoding";
char const* encoding = evhttp_find_header(req->input_headers, key);
char const* encoding = evhttp_find_header(input_headers, "Accept-Encoding");
if (bool const do_compress = encoding != nullptr && tr_strv_contains(encoding, "gzip"sv); !do_compress)
{
@ -248,7 +248,7 @@ void send_simple_response(struct evhttp_request* req, int code, char const* text
if (0 < compressed_len && compressed_len < std::size(content))
{
iov.iov_len = compressed_len;
evhttp_add_header(req->output_headers, "Content-Encoding", "gzip");
evhttp_add_header(output_headers, "Content-Encoding", "gzip");
}
else
{
@ -270,9 +270,10 @@ void add_time_header(struct evkeyvalq* headers, char const* key, time_t now)
void serve_file(struct evhttp_request* req, tr_rpc_server const* server, std::string_view filename)
{
if (req->type != EVHTTP_REQ_GET)
auto* const output_headers = evhttp_request_get_output_headers(req);
if (auto const cmd = evhttp_request_get_command(req); cmd != EVHTTP_REQ_GET)
{
evhttp_add_header(req->output_headers, "Allow", "GET");
evhttp_add_header(output_headers, "Allow", "GET");
send_simple_response(req, HTTP_BADMETHOD);
return;
}
@ -286,9 +287,9 @@ void serve_file(struct evhttp_request* req, tr_rpc_server const* server, std::st
}
auto const now = tr_time();
add_time_header(req->output_headers, "Date", now);
add_time_header(req->output_headers, "Expires", now + (24 * 60 * 60));
evhttp_add_header(req->output_headers, "Content-Type", mimetype_guess(filename));
add_time_header(output_headers, "Date", now);
add_time_header(output_headers, "Expires", now + (24 * 60 * 60));
evhttp_add_header(output_headers, "Content-Type", mimetype_guess(filename));
auto* const response = make_response(req, server, std::string_view{ std::data(content), std::size(content) });
evhttp_send_reply(req, HTTP_OK, "OK", response);
@ -310,38 +311,43 @@ void handle_web_client(struct evhttp_request* req, tr_rpc_server const* server)
"<p>Package Builders: to set a custom default at compile time, "
"#define PACKAGE_DATA_DIR in libtransmission/platform.c "
"or tweak tr_getClutchDir() by hand.</p>");
return;
}
else
{
// convert `req->uri` (ex: "/transmission/web/images/favicon.png")
// convert the URL path component (ex: "/transmission/web/images/favicon.png")
// into a filesystem path (ex: "/usr/share/transmission/web/images/favicon.png")
// remove the "/transmission/web/" prefix
static auto constexpr Web = "web/"sv;
auto subpath = std::string_view{ req->uri }.substr(std::size(server->url()) + std::size(Web));
auto subpath = std::string_view{ evhttp_request_get_uri(req) }.substr(std::size(server->url()) + std::size(Web));
// remove any trailing query / fragment
subpath = subpath.substr(0, subpath.find_first_of("?#"sv));
// if the query is empty, use the default
static auto constexpr DefaultPage = "index.html"sv;
if (std::empty(subpath))
{
static auto constexpr DefaultPage = "index.html"sv;
subpath = DefaultPage;
}
if (tr_strv_contains(subpath, ".."sv))
{
if (auto* const con = evhttp_request_get_connection(req); con != nullptr)
{
char* remote_host = nullptr;
auto remote_port = ev_uint16_t{};
evhttp_connection_get_peer(con, &remote_host, &remote_port);
tr_logAddWarn(fmt::format(
fmt::runtime(_("Rejected request from {host} (possible directory traversal attack)")),
fmt::arg("host", req->remote_host)));
fmt::arg("host", remote_host)));
}
send_simple_response(req, HTTP_NOTFOUND);
}
else
{
serve_file(req, server, tr_pathbuf{ server->web_client_dir_, '/', subpath });
}
}
}
void handle_rpc_from_json(struct evhttp_request* req, tr_rpc_server* server, std::string_view json)
@ -353,8 +359,9 @@ void handle_rpc_from_json(struct evhttp_request* req, tr_rpc_server* server, std
*otop,
[req, server](tr_session* /*session*/, tr_variant&& content)
{
auto* const output_headers = evhttp_request_get_output_headers(req);
auto* const response = make_response(req, server, tr_variant_serde::json().compact().to_string(content));
evhttp_add_header(req->output_headers, "Content-Type", "application/json; charset=UTF-8");
evhttp_add_header(output_headers, "Content-Type", "application/json; charset=UTF-8");
evhttp_send_reply(req, HTTP_OK, "OK", response);
evbuffer_free(response);
});
@ -363,10 +370,11 @@ void handle_rpc_from_json(struct evhttp_request* req, tr_rpc_server* server, std
void handle_rpc(struct evhttp_request* req, tr_rpc_server* server)
{
if (req->type == EVHTTP_REQ_POST)
if (auto const cmd = evhttp_request_get_command(req); cmd == EVHTTP_REQ_POST)
{
auto json = std::string_view{ reinterpret_cast<char const*>(evbuffer_pullup(req->input_buffer, -1)),
evbuffer_get_length(req->input_buffer) };
auto* const input_buffer = evhttp_request_get_input_buffer(req);
auto json = std::string_view{ reinterpret_cast<char const*>(evbuffer_pullup(input_buffer, -1)),
evbuffer_get_length(input_buffer) };
handle_rpc_from_json(req, server, json);
return;
}
@ -394,7 +402,7 @@ bool isIPAddressWithOptionalPort(char const* host)
return evutil_parse_sockaddr_port(host, reinterpret_cast<sockaddr*>(&address), &address_len) != -1;
}
bool isHostnameAllowed(tr_rpc_server const* server, evhttp_request const* req)
bool isHostnameAllowed(tr_rpc_server const* server, evhttp_request* const req)
{
/* If password auth is enabled, any hostname is permitted. */
if (server->is_password_enabled())
@ -408,7 +416,7 @@ bool isHostnameAllowed(tr_rpc_server const* server, evhttp_request const* req)
return true;
}
char const* const host = evhttp_find_header(req->input_headers, "Host");
auto const* const host = evhttp_request_get_host(req);
/* No host header, invalid request. */
if (host == nullptr)
@ -423,10 +431,10 @@ bool isHostnameAllowed(tr_rpc_server const* server, evhttp_request const* req)
}
/* Host header might include the port. */
auto const hostname = std::string(host, strcspn(host, ":"));
auto const hostname = std::string_view{ host, strcspn(host, ":") };
/* localhost is always acceptable. */
if (hostname == "localhost" || hostname == "localhost.")
if (hostname == "localhost"sv || hostname == "localhost."sv)
{
return true;
}
@ -435,9 +443,10 @@ bool isHostnameAllowed(tr_rpc_server const* server, evhttp_request const* req)
return std::any_of(std::begin(src), std::end(src), [&hostname](auto const& str) { return tr_wildmat(hostname, str); });
}
bool test_session_id(tr_rpc_server const* server, evhttp_request const* req)
bool test_session_id(tr_rpc_server const* server, evhttp_request* const req)
{
char const* const session_id = evhttp_find_header(req->input_headers, TR_RPC_SESSION_ID_HEADER);
auto const* const input_headers = evhttp_request_get_input_headers(req);
char const* const session_id = evhttp_find_header(input_headers, TR_RPC_SESSION_ID_HEADER);
return session_id != nullptr && server->session->sessionId() == session_id;
}
@ -471,51 +480,64 @@ void handle_request(struct evhttp_request* req, void* arg)
auto constexpr HttpErrorUnauthorized = 401;
auto constexpr HttpErrorForbidden = 403;
if (req == nullptr)
{
return;
}
auto* const con = evhttp_request_get_connection(req);
if (con == nullptr)
{
return;
}
auto* server = static_cast<tr_rpc_server*>(arg);
if (req != nullptr && req->evcon != nullptr)
{
evhttp_add_header(req->output_headers, "Server", MY_REALM);
char* remote_host = nullptr;
auto remote_port = ev_uint16_t{};
evhttp_connection_get_peer(con, &remote_host, &remote_port);
auto* const output_headers = evhttp_request_get_output_headers(req);
evhttp_add_header(output_headers, "Server", MY_REALM);
if (server->is_anti_brute_force_enabled() && server->login_attempts_ >= server->settings().anti_brute_force_limit)
{
tr_logAddWarn(fmt::format(
fmt::runtime(_("Rejected request from {host} (brute force protection active)")),
fmt::arg("host", req->remote_host)));
fmt::arg("host", remote_host)));
send_simple_response(req, HttpErrorForbidden);
return;
}
if (!is_address_allowed(server, req->remote_host))
if (!is_address_allowed(server, remote_host))
{
tr_logAddWarn(fmt::format(
fmt::runtime(_("Rejected request from {host} (IP not whitelisted)")),
fmt::arg("host", req->remote_host)));
tr_logAddWarn(
fmt::format(fmt::runtime(_("Rejected request from {host} (IP not whitelisted)")), fmt::arg("host", remote_host)));
send_simple_response(req, HttpErrorForbidden);
return;
}
evhttp_add_header(req->output_headers, "Access-Control-Allow-Origin", "*");
evhttp_add_header(output_headers, "Access-Control-Allow-Origin", "*");
if (req->type == EVHTTP_REQ_OPTIONS)
auto const* const input_headers = evhttp_request_get_input_headers(req);
if (auto const cmd = evhttp_request_get_command(req); cmd == EVHTTP_REQ_OPTIONS)
{
if (char const* headers = evhttp_find_header(req->input_headers, "Access-Control-Request-Headers");
headers != nullptr)
if (char const* headers = evhttp_find_header(input_headers, "Access-Control-Request-Headers"); headers != nullptr)
{
evhttp_add_header(req->output_headers, "Access-Control-Allow-Headers", headers);
evhttp_add_header(output_headers, "Access-Control-Allow-Headers", headers);
}
evhttp_add_header(req->output_headers, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
evhttp_add_header(output_headers, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
send_simple_response(req, HTTP_OK);
return;
}
if (!is_authorized(server, evhttp_find_header(req->input_headers, "Authorization")))
if (!is_authorized(server, evhttp_find_header(input_headers, "Authorization")))
{
tr_logAddWarn(fmt::format(
fmt::runtime(_("Rejected request from {host} (failed authentication)")),
fmt::arg("host", req->remote_host)));
evhttp_add_header(req->output_headers, "WWW-Authenticate", "Basic realm=\"" MY_REALM "\"");
fmt::arg("host", remote_host)));
evhttp_add_header(output_headers, "WWW-Authenticate", "Basic realm=\"" MY_REALM "\"");
if (server->is_anti_brute_force_enabled())
{
++server->login_attempts_;
@ -527,13 +549,14 @@ void handle_request(struct evhttp_request* req, void* arg)
server->login_attempts_ = 0;
auto uri = std::string_view{ req->uri };
auto const location = tr_strv_starts_with(uri, server->url()) ? uri.substr(std::size(server->url())) : ""sv;
auto const* const uri = evhttp_request_get_uri(req);
auto const uri_sv = std::string_view{ uri };
auto const location = tr_strv_starts_with(uri_sv, server->url()) ? uri_sv.substr(std::size(server->url())) : ""sv;
if (std::empty(location) || location == "web"sv)
{
auto const new_location = fmt::format("{:s}web/", server->url());
evhttp_add_header(req->output_headers, "Location", new_location.c_str());
evhttp_add_header(output_headers, "Location", new_location.c_str());
send_simple_response(req, HTTP_MOVEPERM, nullptr);
}
else if (tr_strv_starts_with(location, "web/"sv))
@ -542,7 +565,7 @@ void handle_request(struct evhttp_request* req, void* arg)
}
else if (!isHostnameAllowed(server, req))
{
char const* const tmp =
static auto constexpr Body =
"<p>Transmission received your request, but the hostname was unrecognized.</p>"
"<p>To fix this, choose one of the following options:"
"<ul>"
@ -553,16 +576,15 @@ void handle_request(struct evhttp_request* req, void* arg)
"<p>This requirement has been added to help prevent "
"<a href=\"https://en.wikipedia.org/wiki/DNS_rebinding\">DNS Rebinding</a> "
"attacks.</p>";
tr_logAddWarn(fmt::format(
fmt::runtime(_("Rejected request from {host} (Host not whitelisted)")),
fmt::arg("host", req->remote_host)));
send_simple_response(req, 421, tmp);
tr_logAddWarn(
fmt::format(fmt::runtime(_("Rejected request from {host} (Host not whitelisted)")), fmt::arg("host", remote_host)));
send_simple_response(req, 421, Body);
}
#ifdef REQUIRE_SESSION_ID
else if (!test_session_id(server, req))
{
auto const session_id = std::string{ server->session->sessionId() };
auto const tmp = fmt::format(
auto const body = fmt::format(
"<p>Your request had an invalid session-id header.</p>"
"<p>To fix this, follow these steps:"
"<ol><li> When reading a response, get its X-Transmission-Session-Id header and remember it"
@ -575,9 +597,9 @@ void handle_request(struct evhttp_request* req, void* arg)
"<p><code>{:s}: {:s}</code></p>",
TR_RPC_SESSION_ID_HEADER,
session_id);
evhttp_add_header(req->output_headers, TR_RPC_SESSION_ID_HEADER, session_id.c_str());
evhttp_add_header(req->output_headers, "Access-Control-Expose-Headers", TR_RPC_SESSION_ID_HEADER);
send_simple_response(req, 409, tmp.c_str());
evhttp_add_header(output_headers, TR_RPC_SESSION_ID_HEADER, session_id.c_str());
evhttp_add_header(output_headers, "Access-Control-Expose-Headers", TR_RPC_SESSION_ID_HEADER);
send_simple_response(req, 409, body.c_str());
}
#endif
else if (tr_strv_starts_with(location, "rpc"sv))
@ -588,10 +610,9 @@ void handle_request(struct evhttp_request* req, void* arg)
{
tr_logAddWarn(fmt::format(
fmt::runtime(_("Unknown URI from {host}: '{uri}'")),
fmt::arg("host", req->remote_host),
fmt::arg("uri", uri)));
send_simple_response(req, HTTP_NOTFOUND, req->uri);
}
fmt::arg("host", remote_host),
fmt::arg("uri", uri_sv)));
send_simple_response(req, HTTP_NOTFOUND, uri);
}
}