From 706735ca88e547dcda2306fb448b694f7f6fb6c0 Mon Sep 17 00:00:00 2001 From: LaserEyess <16581533+LaserEyess@users.noreply.github.com> Date: Wed, 23 Feb 2022 16:09:54 -0500 Subject: [PATCH] Support binding the RPC to a Unix socket on *nix platforms (#2574) * Support binding the RPC to a Unix socket on *nix This commit adds unix socket support for a RPC. Some refactoring was required in order to split out the RPC server's address struct from the normal network address struct used for peers. It would cause unacceptable overhead to add the unix socket length to the union. Co-authored-by: Malte Voos * add RPC socket mode to control unix socket perms Unix socket permissions are important to control for security reasons, and libevent defaults to 0755, which may not be what users want. Co-authored-by: LaserEyess Co-authored-by: Malte Voos --- libtransmission/quark.cc | 3 +- libtransmission/quark.h | 1 + libtransmission/rpc-server.cc | 233 ++++++++++++++++++++++++++++++---- libtransmission/rpc-server.h | 7 +- libtransmission/session.cc | 2 + 5 files changed, 222 insertions(+), 24 deletions(-) diff --git a/libtransmission/quark.cc b/libtransmission/quark.cc index 7e60c886a..a042d10d1 100644 --- a/libtransmission/quark.cc +++ b/libtransmission/quark.cc @@ -18,7 +18,7 @@ using namespace std::literals; namespace { -auto constexpr my_static = std::array{ ""sv, +auto constexpr my_static = std::array{ ""sv, "activeTorrentCount"sv, "activity-date"sv, "activityDate"sv, @@ -291,6 +291,7 @@ auto constexpr my_static = std::array{ ""sv, "rpc-host-whitelist-enabled"sv, "rpc-password"sv, "rpc-port"sv, + "rpc-socket-mode"sv, "rpc-url"sv, "rpc-username"sv, "rpc-version"sv, diff --git a/libtransmission/quark.h b/libtransmission/quark.h index a9e76a30e..03b3a11d4 100644 --- a/libtransmission/quark.h +++ b/libtransmission/quark.h @@ -294,6 +294,7 @@ enum TR_KEY_rpc_host_whitelist_enabled, TR_KEY_rpc_password, TR_KEY_rpc_port, + TR_KEY_rpc_socket_mode, TR_KEY_rpc_url, TR_KEY_rpc_username, TR_KEY_rpc_version, diff --git a/libtransmission/rpc-server.cc b/libtransmission/rpc-server.cc index 4323141f9..da62fe36b 100644 --- a/libtransmission/rpc-server.cc +++ b/libtransmission/rpc-server.cc @@ -14,10 +14,17 @@ #include +#ifndef _WIN32 +#include +#include +#include +#endif + #include #include #include #include /* TODO: eventually remove this */ +#include #include "transmission.h" @@ -51,12 +58,49 @@ using namespace std::literals; static char constexpr MyName[] = "RPC Server"; +static char constexpr TrUnixSocketPrefix[] = "unix:"; + +/* The maximum size of a unix socket path is defined per-platform based on sockaddr_un.sun_path. + * On Windows the fallback is the length of the unix prefix. Subtracting one at the end is for + * double counting null terminators from sun_path and TrUnixSocketPrefix. */ +#ifdef _WIN32 +auto inline constexpr TrUnixAddrStrLen = size_t{ sizeof(TrUnixSocketPrefix) }; +#else +auto inline constexpr TrUnixAddrStrLen = size_t{ sizeof(((struct sockaddr_un*)nullptr)->sun_path) + sizeof(TrUnixSocketPrefix) - + 1 }; +#endif + +enum tr_rpc_address_type +{ + TR_RPC_AF_INET, + TR_RPC_AF_INET6, + TR_RPC_AF_UNIX +}; + +struct tr_rpc_address +{ + tr_rpc_address_type type; + union + { + struct in6_addr; + struct in_addr; + char unixSocketPath[TrUnixAddrStrLen]; + } addr; +}; + +static tr_rpc_address constexpr tr_rpc_inaddr_any = { TR_RPC_AF_INET, { INADDR_ANY } }; + #define MY_REALM "Transmission" #define dbgmsg(...) tr_logAddDeepNamed(MyName, __VA_ARGS__) static int constexpr DeflateLevel = 6; // medium / default +static bool constexpr tr_rpc_address_is_valid(tr_rpc_address const* a) +{ + return a != nullptr && (a->type == TR_RPC_AF_INET || a->type == TR_RPC_AF_INET6 || a->type == TR_RPC_AF_UNIX); +} + /*** **** ***/ @@ -544,6 +588,108 @@ static auto constexpr ServerStartRetryDelayIncrement = int{ 5 }; static auto constexpr ServerStartRetryDelayStep = int{ 3 }; static auto constexpr ServerStartRetryMaxDelay = int{ 60 }; +static char const* tr_rpc_address_to_string(tr_rpc_address const* addr, char* buf, size_t buflen) +{ + TR_ASSERT(tr_rpc_address_is_valid(addr)); + + switch (addr->type) + { + case TR_RPC_AF_INET: + return evutil_inet_ntop(AF_INET, &addr->addr, buf, buflen); + + case TR_RPC_AF_INET6: + return evutil_inet_ntop(AF_INET6, &addr->addr, buf, buflen); + + case TR_RPC_AF_UNIX: + tr_strlcpy(buf, addr->addr.unixSocketPath, buflen); + return buf; + + default: + return nullptr; + } +} + +static std::string tr_rpc_address_with_port(tr_rpc_server const* server) +{ + tr_rpc_address const* addr = server->bindAddress; + + char addr_buf[TrUnixAddrStrLen]; + tr_rpc_address_to_string(addr, addr_buf, sizeof(addr_buf)); + + std::string addr_port_str{ addr_buf }; + if (addr->type != TR_RPC_AF_UNIX) + { + addr_port_str.append(":" + std::to_string(tr_rpcGetPort(server))); + } + return addr_port_str; +} + +static bool tr_rpc_address_from_string(tr_rpc_address* dst, std::string_view src) +{ + if (tr_strvStartsWith(src, TrUnixSocketPrefix)) + { + if (src.length() >= TrUnixAddrStrLen) + { + tr_logAddNamedError( + MyName, + _("Unix socket path must be fewer than %zu characters (including \"%s\" prefix)"), + TrUnixAddrStrLen - 1, + TrUnixSocketPrefix); + return false; + } + + dst->type = TR_RPC_AF_UNIX; + tr_strlcpy(dst->addr.unixSocketPath, std::string{ src }.c_str(), TrUnixAddrStrLen); + return true; + } + + if (evutil_inet_pton(AF_INET, std::string{ src }.c_str(), &dst->addr) == 1) + { + dst->type = TR_RPC_AF_INET; + return true; + } + + if (evutil_inet_pton(AF_INET6, std::string{ src }.c_str(), &dst->addr) == 1) + { + dst->type = TR_RPC_AF_INET6; + return true; + } + + return false; +} + +static bool bindUnixSocket(struct event_base* base, struct evhttp* httpd, char const* path, int socket_mode) +{ +#ifdef _WIN32 + tr_logAddNamedError( + MyName, + _("Unix sockets are not supported on Windows. Please change \"%s\" in your configuration file."), + TR_KEY_rpc_bind_address); + return false; +#else + struct sockaddr_un addr; + addr.sun_family = AF_UNIX; + tr_strlcpy(addr.sun_path, path + strlen(TrUnixSocketPrefix), sizeof(addr.sun_path)); + + unlink(addr.sun_path); + + struct evconnlistener* + lev = evconnlistener_new_bind(base, nullptr, nullptr, LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr*)&addr, sizeof(addr)); + + if (lev == nullptr) + { + return false; + } + + if (chmod(addr.sun_path, (mode_t)socket_mode) != 0) + { + tr_logAddNamedError(MyName, _("Could not set RPC socket mode to %o, defaulting to 755"), socket_mode); + } + + return evhttp_bind_listener(httpd, lev) != nullptr; +#endif +} + static void startServer(void* vserver); static void rpc_server_on_start_retry(evutil_socket_t /*fd*/, short /*type*/, void* context) @@ -587,14 +733,22 @@ static void startServer(void* vserver) return; } - struct evhttp* httpd = evhttp_new(server->session->event_base); + struct event_base* base = server->session->event_base; + struct evhttp* httpd = evhttp_new(base); + evhttp_set_allowed_methods(httpd, EVHTTP_REQ_GET | EVHTTP_REQ_POST | EVHTTP_REQ_OPTIONS); char const* address = tr_rpcGetBindAddress(server); tr_port const port = server->port; - if (evhttp_bind_socket(httpd, address, port) == -1) + bool const success = server->bindAddress->type == TR_RPC_AF_UNIX ? + bindUnixSocket(base, httpd, address, server->rpc_socket_mode) : + (evhttp_bind_socket(httpd, address, port) != -1); + + auto const addr_port_str = tr_rpc_address_with_port(server); + + if (!success) { evhttp_free(httpd); @@ -602,15 +756,14 @@ static void startServer(void* vserver) { int const retry_delay = rpc_server_start_retry(server); - tr_logAddNamedDbg(MyName, "Unable to bind to %s:%d, retrying in %d seconds", address, port, retry_delay); + tr_logAddNamedDbg(MyName, "Unable to bind to %s, retrying in %d seconds", addr_port_str.c_str(), retry_delay); return; } tr_logAddNamedError( MyName, - "Unable to bind to %s:%d after %d attempts, giving up", - address, - port, + "Unable to bind to %s after %d attempts, giving up", + addr_port_str.c_str(), ServerStartRetryCount); } else @@ -618,7 +771,7 @@ static void startServer(void* vserver) evhttp_set_gencb(httpd, handle_request, server); server->httpd = httpd; - tr_logAddNamedDbg(MyName, "Started listening on %s:%d", address, port); + tr_logAddNamedDbg(MyName, "Started listening on %s", addr_port_str.c_str()); } rpc_server_start_retry_cancel(server); @@ -638,12 +791,16 @@ static void stopServer(tr_rpc_server* server) } char const* address = tr_rpcGetBindAddress(server); - int const port = server->port; server->httpd = nullptr; evhttp_free(httpd); - tr_logAddNamedDbg(MyName, "Stopped listening on %s:%d", address, port); + if (server->bindAddress->type == TR_RPC_AF_UNIX) + { + unlink(address + strlen(TrUnixSocketPrefix)); + } + + tr_logAddNamedDbg(MyName, "Stopped listening on %s", tr_rpc_address_with_port(server).c_str()); } static void onEnabledChanged(void* vserver) @@ -772,6 +929,16 @@ static void tr_rpcSetHostWhitelistEnabled(tr_rpc_server* server, bool isEnabled) server->isHostWhitelistEnabled = isEnabled; } +int tr_rpcGetRPCSocketMode(tr_rpc_server const* server) +{ + return server->rpc_socket_mode; +} + +static void tr_rpcSetRPCSocketMode(tr_rpc_server* server, int socket_mode) +{ + server->rpc_socket_mode = socket_mode; +} + /**** ***** PASSWORD ****/ @@ -817,7 +984,8 @@ bool tr_rpcIsPasswordEnabled(tr_rpc_server const* server) char const* tr_rpcGetBindAddress(tr_rpc_server const* server) { - return tr_address_to_string(&server->bindAddress); + static char addr_buf[TrUnixAddrStrLen]; + return tr_rpc_address_to_string(server->bindAddress, addr_buf, sizeof(addr_buf)); } bool tr_rpcGetAntiBruteForceEnabled(tr_rpc_server const* server) @@ -858,7 +1026,7 @@ tr_rpc_server::tr_rpc_server(tr_session* session_in, tr_variant* settings) : compressor{ libdeflate_alloc_compressor(DeflateLevel), libdeflate_free_compressor } , session{ session_in } { - auto address = tr_address{}; + auto address = tr_rpc_address{}; auto boolVal = bool{}; auto i = int64_t{}; auto sv = std::string_view{}; @@ -999,39 +1167,57 @@ tr_rpc_server::tr_rpc_server(tr_session* session_in, tr_variant* settings) tr_rpcSetAntiBruteForceThreshold(this, i); } + key = TR_KEY_rpc_socket_mode; + + if (!tr_variantDictFindInt(settings, key, &i)) + { + missing_settings_key(key); + } + else + { + tr_rpcSetRPCSocketMode(this, i); + } + key = TR_KEY_rpc_bind_address; if (!tr_variantDictFindStrView(settings, key, &sv)) { missing_settings_key(key); - address = tr_inaddr_any; + address = tr_rpc_inaddr_any; } else { - if (!tr_address_from_string(&address, std::string{ sv }.c_str())) + if (!tr_rpc_address_from_string(&address, sv)) { - tr_logAddNamedError(MyName, _("%" TR_PRIsv " is not a valid address"), TR_PRIsv_ARG(sv)); - address = tr_inaddr_any; + tr_logAddNamedError(MyName, _("%" TR_PRIsv " is not a valid address, falling back to 0.0.0.0."), TR_PRIsv_ARG(sv)); + address = tr_rpc_inaddr_any; } - else if (address.type != TR_AF_INET && address.type != TR_AF_INET6) + else if (address.type != TR_RPC_AF_INET && address.type != TR_RPC_AF_INET6 && address.type != TR_RPC_AF_UNIX) { tr_logAddNamedError( MyName, - _("%" TR_PRIsv " is not an IPv4 or IPv6 address. RPC listeners must be IPv4 or IPv6"), + _("%" TR_PRIsv + " is not an IPv4 address, an IPv6 address, or a unix socket path. RPC listeners must be one of the previously mentioned types. Falling back to 0.0.0.0."), TR_PRIsv_ARG(sv)); - address = tr_inaddr_any; + address = tr_rpc_inaddr_any; } } - this->bindAddress = address; + if (address.type == TR_RPC_AF_UNIX) + { + this->isWhitelistEnabled = false; + this->isHostWhitelistEnabled = false; + } + + this->bindAddress = (tr_rpc_address*)tr_memdup(&address, sizeof(tr_rpc_address)); if (this->isEnabled) { + tr_logAddNamedInfo( MyName, - _("Serving RPC and Web requests on %s:%d%s"), - tr_rpcGetBindAddress(this), - (int)this->port, + _("Serving RPC and Web requests on %s%s"), + tr_rpc_address_with_port(this).c_str(), this->url.c_str()); tr_runInEventThread(session, startServer, this); @@ -1058,4 +1244,7 @@ tr_rpc_server::~tr_rpc_server() TR_ASSERT(tr_amInEventThread(this->session)); stopServer(this); + + tr_free(this->bindAddress); + this->bindAddress = nullptr; } diff --git a/libtransmission/rpc-server.h b/libtransmission/rpc-server.h index 56e7b9d1e..3614150ea 100644 --- a/libtransmission/rpc-server.h +++ b/libtransmission/rpc-server.h @@ -21,6 +21,7 @@ struct event; struct evhttp; struct tr_variant; +struct tr_rpc_address; struct libdeflate_compressor; class tr_rpc_server @@ -43,7 +44,7 @@ public: std::string whitelistStr; std::string url; - struct tr_address bindAddress; + struct tr_rpc_address* bindAddress; struct event* start_retry_timer = nullptr; struct evhttp* httpd = nullptr; @@ -52,6 +53,8 @@ public: int antiBruteForceThreshold = 0; int loginattempts = 0; int start_retry_counter = 0; + static int constexpr DefaultRpcSocketMode = 0750; + int rpc_socket_mode = DefaultRpcSocketMode; tr_port port = 0; @@ -82,6 +85,8 @@ bool tr_rpcGetWhitelistEnabled(tr_rpc_server const* server); void tr_rpcSetWhitelist(tr_rpc_server* server, std::string_view whitelist); +int tr_rpcGetRPCSocketMode(tr_rpc_server const* server); + std::string const& tr_rpcGetWhitelist(tr_rpc_server const* server); void tr_rpcSetPassword(tr_rpc_server* server, std::string_view password); diff --git a/libtransmission/session.cc b/libtransmission/session.cc index 8d7264fa6..389b22860 100644 --- a/libtransmission/session.cc +++ b/libtransmission/session.cc @@ -372,6 +372,7 @@ void tr_sessionGetDefaultSettings(tr_variant* d) tr_variantDictAddBool(d, TR_KEY_rpc_host_whitelist_enabled, true); tr_variantDictAddInt(d, TR_KEY_rpc_port, TR_DEFAULT_RPC_PORT); tr_variantDictAddStrView(d, TR_KEY_rpc_url, TR_DEFAULT_RPC_URL_STR); + tr_variantDictAddInt(d, TR_KEY_rpc_socket_mode, tr_rpc_server::DefaultRpcSocketMode); tr_variantDictAddBool(d, TR_KEY_scrape_paused_torrents_enabled, true); tr_variantDictAddStrView(d, TR_KEY_script_torrent_added_filename, ""); tr_variantDictAddBool(d, TR_KEY_script_torrent_added_enabled, false); @@ -446,6 +447,7 @@ void tr_sessionGetSettings(tr_session const* s, tr_variant* d) tr_variantDictAddBool(d, TR_KEY_rpc_enabled, tr_sessionIsRPCEnabled(s)); tr_variantDictAddStr(d, TR_KEY_rpc_password, tr_sessionGetRPCPassword(s)); tr_variantDictAddInt(d, TR_KEY_rpc_port, tr_sessionGetRPCPort(s)); + tr_variantDictAddInt(d, TR_KEY_rpc_socket_mode, tr_rpcGetRPCSocketMode(s->rpc_server_.get())); tr_variantDictAddStr(d, TR_KEY_rpc_url, tr_sessionGetRPCUrl(s)); tr_variantDictAddStr(d, TR_KEY_rpc_username, tr_sessionGetRPCUsername(s)); tr_variantDictAddStr(d, TR_KEY_rpc_whitelist, tr_sessionGetRPCWhitelist(s));