// This file Copyright © 2008-2022 Mnemosyne LLC. // It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. #include #include #include #include /* memcpy */ #include #include #include #include #include #ifndef _WIN32 #include #include #include #endif #include #include #include #include /* TODO: eventually remove this */ #include #include #include #include "transmission.h" #include "crypto-utils.h" /* tr_rand_buffer() */ #include "crypto.h" /* tr_ssha1_matches() */ #include "error.h" #include "fdlimit.h" #include "log.h" #include "net.h" #include "platform.h" /* tr_getWebClientDir() */ #include "quark.h" #include "rpc-server.h" #include "rpcimpl.h" #include "session-id.h" #include "session.h" #include "tr-assert.h" #include "tr-strbuf.h" #include "trevent.h" #include "utils.h" #include "variant.h" #include "web-utils.h" #include "web.h" using namespace std::literals; /* session-id is used to make cross-site request forgery attacks difficult. * Don't disable this feature unless you really know what you're doing! * https://en.wikipedia.org/wiki/Cross-site_request_forgery * https://shiflett.org/articles/cross-site-request-forgeries * http://www.webappsec.org/lists/websecurity/archive/2008-04/msg00037.html */ #define REQUIRE_SESSION_ID static auto constexpr TrUnixSocketPrefix = "unix:"sv; /* 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 an ipv6 address. Subtracting one at the end is for * double counting null terminators from sun_path and TrUnixSocketPrefix. */ #ifdef _WIN32 auto inline constexpr TrUnixAddrStrLen = size_t{ INET6_ADDRSTRLEN }; #else auto inline constexpr TrUnixAddrStrLen = size_t{ sizeof(((struct sockaddr_un*)nullptr)->sun_path) + std::size(TrUnixSocketPrefix) }; #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 in_addr addr4; struct in6_addr addr6; char unixSocketPath[TrUnixAddrStrLen]; } addr; void set_inaddr_any() { type = TR_RPC_AF_INET; addr.addr4 = { INADDR_ANY }; } }; #define MY_REALM "Transmission" static int constexpr DeflateLevel = 6; // medium / default static bool constexpr tr_rpc_address_is_valid(tr_rpc_address const& a) { return a.type == TR_RPC_AF_INET || a.type == TR_RPC_AF_INET6 || a.type == TR_RPC_AF_UNIX; } /*** **** ***/ static char const* get_current_session_id(tr_rpc_server* server) { return tr_session_id_get_current(server->session->session_id); } /** *** **/ static void send_simple_response(struct evhttp_request* req, int code, char const* text) { char const* code_text = tr_webGetResponseStr(code); struct evbuffer* body = evbuffer_new(); evbuffer_add_printf(body, "

%d: %s

", code, code_text); if (text != nullptr) { evbuffer_add_printf(body, "%s", text); } evhttp_send_reply(req, code, code_text, body); evbuffer_free(body); } /*** **** ***/ static char const* mimetype_guess(std::string_view path) { // these are the ones we need for serving the web client's files... static auto constexpr Types = std::array, 7>{ { { ".css"sv, "text/css" }, { ".gif"sv, "image/gif" }, { ".html"sv, "text/html" }, { ".ico"sv, "image/vnd.microsoft.icon" }, { ".js"sv, "application/javascript" }, { ".png"sv, "image/png" }, { ".svg"sv, "image/svg+xml" }, } }; for (auto const& [suffix, mime_type] : Types) { if (tr_strvEndsWith(path, suffix)) { return mime_type; } } return "application/octet-stream"; } static void add_response(struct evhttp_request* req, tr_rpc_server* server, struct evbuffer* out, struct evbuffer* content) { char const* key = "Accept-Encoding"; char const* encoding = evhttp_find_header(req->input_headers, key); bool const do_compress = encoding != nullptr && strstr(encoding, "gzip") != nullptr; if (!do_compress) { evbuffer_add_buffer(out, content); } else { auto const* const content_ptr = evbuffer_pullup(content, -1); size_t const content_len = evbuffer_get_length(content); auto const max_compressed_len = libdeflate_deflate_compress_bound(server->compressor.get(), content_len); struct evbuffer_iovec iovec[1]; evbuffer_reserve_space(out, std::max(content_len, max_compressed_len), iovec, 1); auto const compressed_len = libdeflate_gzip_compress( server->compressor.get(), content_ptr, content_len, iovec[0].iov_base, iovec[0].iov_len); if (0 < compressed_len && compressed_len < content_len) { iovec[0].iov_len = compressed_len; evhttp_add_header(req->output_headers, "Content-Encoding", "gzip"); } else { std::copy_n(content_ptr, content_len, static_cast(iovec[0].iov_base)); iovec[0].iov_len = content_len; } evbuffer_commit_space(out, iovec, 1); } } static void add_time_header(struct evkeyvalq* headers, char const* key, time_t value) { char buf[128]; struct tm tm; /* According to RFC 2616 this must follow RFC 1123's date format, so use gmtime instead of localtime... */ tr_gmtime_r(&value, &tm); strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm); evhttp_add_header(headers, key, buf); } static void evbuffer_ref_cleanup_tr_free(void const* /*data*/, size_t /*datalen*/, void* extra) { tr_free(extra); } static void serve_file(struct evhttp_request* req, tr_rpc_server* server, std::string_view filename) { if (req->type != EVHTTP_REQ_GET) { evhttp_add_header(req->output_headers, "Allow", "GET"); send_simple_response(req, 405, nullptr); } else { auto file_len = size_t{}; tr_error* error = nullptr; void* const file = tr_loadFile(filename, &file_len, &error); if (file == nullptr) { auto const tmp = fmt::format(FMT_STRING("{:s} ({:s})"), filename, error->message); send_simple_response(req, HTTP_NOTFOUND, tmp.c_str()); tr_error_free(error); } else { auto const now = tr_time(); auto* const content = evbuffer_new(); evbuffer_add_reference(content, file, file_len, evbuffer_ref_cleanup_tr_free, file); auto* const out = evbuffer_new(); evhttp_add_header(req->output_headers, "Content-Type", mimetype_guess(filename)); add_time_header(req->output_headers, "Date", now); add_time_header(req->output_headers, "Expires", now + (24 * 60 * 60)); add_response(req, server, out, content); evhttp_send_reply(req, HTTP_OK, "OK", out); evbuffer_free(out); evbuffer_free(content); } } } static void handle_web_client(struct evhttp_request* req, tr_rpc_server* server) { char const* webClientDir = tr_getWebClientDir(server->session); if (tr_str_is_empty(webClientDir)) { send_simple_response( req, HTTP_NOTFOUND, "

Couldn't find Transmission's web interface files!

" "

Users: to tell Transmission where to look, " "set the TRANSMISSION_WEB_HOME environment " "variable to the folder where the web interface's " "index.html is located.

" "

Package Builders: to set a custom default at compile time, " "#define PACKAGE_DATA_DIR in libtransmission/platform.c " "or tweak tr_getClutchDir() by hand.

"); } else { // TODO: string_view char* const subpath = tr_strdup(req->uri + std::size(server->url) + 4); if (char* pch = strchr(subpath, '?'); pch != nullptr) { *pch = '\0'; } if (strstr(subpath, "..") != nullptr) { send_simple_response(req, HTTP_NOTFOUND, "

Tsk, tsk.

"); } else { auto const filename = tr_pathbuf{ webClientDir, "/"sv, tr_str_is_empty(subpath) ? "index.html" : subpath }; serve_file(req, server, filename.sv()); } tr_free(subpath); } } struct rpc_response_data { struct evhttp_request* req; tr_rpc_server* server; }; static void rpc_response_func(tr_session* /*session*/, tr_variant* response, void* user_data) { auto* data = static_cast(user_data); struct evbuffer* response_buf = tr_variantToBuf(response, TR_VARIANT_FMT_JSON_LEAN); struct evbuffer* buf = evbuffer_new(); add_response(data->req, data->server, buf, response_buf); evhttp_add_header(data->req->output_headers, "Content-Type", "application/json; charset=UTF-8"); evhttp_send_reply(data->req, HTTP_OK, "OK", buf); evbuffer_free(buf); evbuffer_free(response_buf); tr_free(data); } static void handle_rpc_from_json(struct evhttp_request* req, tr_rpc_server* server, std::string_view json) { auto top = tr_variant{}; auto const have_content = tr_variantFromBuf(&top, TR_VARIANT_PARSE_JSON | TR_VARIANT_PARSE_INPLACE, json); auto* const data = tr_new0(struct rpc_response_data, 1); data->req = req; data->server = server; tr_rpc_request_exec_json(server->session, have_content ? &top : nullptr, rpc_response_func, data); if (have_content) { tr_variantFree(&top); } } static void handle_rpc(struct evhttp_request* req, tr_rpc_server* server) { if (req->type == EVHTTP_REQ_POST) { auto json = std::string_view{ reinterpret_cast(evbuffer_pullup(req->input_buffer, -1)), evbuffer_get_length(req->input_buffer) }; handle_rpc_from_json(req, server, json); return; } if (req->type == EVHTTP_REQ_GET) { char const* q = strchr(req->uri, '?'); if (q != nullptr) { auto* const data = tr_new0(struct rpc_response_data, 1); data->req = req; data->server = server; tr_rpc_request_exec_uri(server->session, q + 1, rpc_response_func, data); return; } } send_simple_response(req, 405, nullptr); } static bool isAddressAllowed(tr_rpc_server const* server, char const* address) { auto const& src = server->whitelist; return !server->isWhitelistEnabled || std::any_of(std::begin(src), std::end(src), [&address](auto const& s) { return tr_wildmat(address, s); }); } static bool isIPAddressWithOptionalPort(char const* host) { struct sockaddr_storage address; int address_len = sizeof(address); /* TODO: move to net.{c,h} */ return evutil_parse_sockaddr_port(host, (struct sockaddr*)&address, &address_len) != -1; } static bool isHostnameAllowed(tr_rpc_server const* server, struct evhttp_request* req) { /* If password auth is enabled, any hostname is permitted. */ if (server->isPasswordEnabled) { return true; } /* If whitelist is disabled, no restrictions. */ if (!server->isHostWhitelistEnabled) { return true; } char const* const host = evhttp_find_header(req->input_headers, "Host"); /* No host header, invalid request. */ if (host == nullptr) { return false; } /* IP address is always acceptable. */ if (isIPAddressWithOptionalPort(host)) { return true; } /* Host header might include the port. */ auto const hostname = std::string(host, strcspn(host, ":")); /* localhost is always acceptable. */ if (hostname == "localhost" || hostname == "localhost.") { return true; } auto const& src = server->hostWhitelist; return std::any_of( std::begin(src), std::end(src), [&hostname](auto const& str) { return tr_wildmat(hostname.c_str(), str.c_str()); }); } static bool test_session_id(tr_rpc_server* server, evhttp_request const* req) { char const* ours = get_current_session_id(server); char const* theirs = evhttp_find_header(req->input_headers, TR_RPC_SESSION_ID_HEADER); bool const success = theirs != nullptr && strcmp(theirs, ours) == 0; return success; } static bool isAuthorized(tr_rpc_server const* server, char const* auth_header) { if (!server->isPasswordEnabled) { return true; } // https://datatracker.ietf.org/doc/html/rfc7617 // `Basic ${base64(username)}:${base64(password)}` auto constexpr Prefix = "Basic "sv; auto auth = std::string_view{ auth_header != nullptr ? auth_header : "" }; if (!tr_strvStartsWith(auth, Prefix)) { return false; } auth.remove_prefix(std::size(Prefix)); auto const decoded_str = tr_base64_decode(auth); auto decoded = std::string_view{ decoded_str }; auto const username = tr_strvSep(&decoded, ':'); auto const password = decoded; return server->username == username && tr_ssha1_matches(server->salted_password, password); } static void handle_request(struct evhttp_request* req, void* arg) { auto* server = static_cast(arg); if (req != nullptr && req->evcon != nullptr) { evhttp_add_header(req->output_headers, "Server", MY_REALM); if (server->isAntiBruteForceEnabled && server->loginattempts >= server->antiBruteForceThreshold) { send_simple_response(req, 403, "

Too many unsuccessful login attempts. Please restart transmission-daemon.

"); return; } if (!isAddressAllowed(server, req->remote_host)) { send_simple_response( req, 403, "

Unauthorized IP Address.

" "

Either disable the IP address whitelist or add your address to it.

" "

If you're editing settings.json, see the 'rpc-whitelist' and 'rpc-whitelist-enabled' entries.

" "

If you're still using ACLs, use a whitelist instead. See the transmission-daemon manpage for details.

"); return; } evhttp_add_header(req->output_headers, "Access-Control-Allow-Origin", "*"); if (req->type == EVHTTP_REQ_OPTIONS) { char const* headers = evhttp_find_header(req->input_headers, "Access-Control-Request-Headers"); if (headers != nullptr) { evhttp_add_header(req->output_headers, "Access-Control-Allow-Headers", headers); } evhttp_add_header(req->output_headers, "Access-Control-Allow-Methods", "GET, POST, OPTIONS"); send_simple_response(req, 200, ""); return; } if (!isAuthorized(server, evhttp_find_header(req->input_headers, "Authorization"))) { evhttp_add_header(req->output_headers, "WWW-Authenticate", "Basic realm=\"" MY_REALM "\""); if (server->isAntiBruteForceEnabled) { ++server->loginattempts; } auto const unauthuser = fmt::format( FMT_STRING("

Unauthorized User. {:d} unsuccessful login attempts.

"), server->loginattempts); send_simple_response(req, 401, unauthuser.c_str()); return; } server->loginattempts = 0; auto uri = std::string_view{ req->uri }; auto const location = tr_strvStartsWith(uri, server->url) ? uri.substr(std::size(server->url)) : ""sv; if (std::empty(location) || location == "web"sv) { auto const new_location = fmt::format(FMT_STRING("{:s}web/"), server->url); evhttp_add_header(req->output_headers, "Location", new_location.c_str()); send_simple_response(req, HTTP_MOVEPERM, nullptr); } else if (tr_strvStartsWith(location, "web/"sv)) { handle_web_client(req, server); } else if (!isHostnameAllowed(server, req)) { char const* const tmp = "

Transmission received your request, but the hostname was unrecognized.

" "

To fix this, choose one of the following options:" "

    " "
  • Enable password authentication, then any hostname is allowed.
  • " "
  • Add the hostname you want to use to the whitelist in settings.
  • " "

" "

If you're editing settings.json, see the 'rpc-host-whitelist' and 'rpc-host-whitelist-enabled' entries.

" "

This requirement has been added to help prevent " "DNS Rebinding " "attacks.

"; send_simple_response(req, 421, tmp); } #ifdef REQUIRE_SESSION_ID else if (!test_session_id(server, req)) { char const* sessionId = get_current_session_id(server); auto const tmp = fmt::format( FMT_STRING("

Your request had an invalid session-id header.

" "

To fix this, follow these steps:" "

  1. When reading a response, get its X-Transmission-Session-Id header and remember it" "
  2. Add the updated header to your outgoing requests" "
  3. When you get this 409 error message, resend your request with the updated header" "

" "

This requirement has been added to help prevent " "CSRF " "attacks.

" "

{:s}: {:s}

"), TR_RPC_SESSION_ID_HEADER, sessionId); evhttp_add_header(req->output_headers, TR_RPC_SESSION_ID_HEADER, sessionId); evhttp_add_header(req->output_headers, "Access-Control-Expose-Headers", TR_RPC_SESSION_ID_HEADER); send_simple_response(req, 409, tmp.c_str()); } #endif else if (tr_strvStartsWith(location, "rpc"sv)) { handle_rpc(req, server); } else { send_simple_response(req, HTTP_NOTFOUND, req->uri); } } } static auto constexpr ServerStartRetryCount = int{ 10 }; 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) { char addr_buf[TrUnixAddrStrLen]; tr_rpc_address_to_string(*server->bindAddress, addr_buf, sizeof(addr_buf)); std::string addr_port_str{ addr_buf }; if (server->bindAddress->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 (std::size(src) >= TrUnixAddrStrLen) { tr_logAddError(fmt::format( _("Unix socket path must be fewer than {count} characters (including '{prefix}' prefix)"), fmt::arg("count", TrUnixAddrStrLen - 1), fmt::arg("prefix", 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( [[maybe_unused]] struct event_base* base, [[maybe_unused]] struct evhttp* httpd, [[maybe_unused]] char const* path, [[maybe_unused]] int socket_mode) { #ifdef _WIN32 tr_logAddError(fmt::format( _("Unix sockets are unsupported on Windows. Please change '{key}' in your settings."), fmt::arg("key", tr_quark_get_string(TR_KEY_rpc_bind_address)))); return false; #else struct sockaddr_un addr; addr.sun_family = AF_UNIX; tr_strlcpy(addr.sun_path, path + std::size(TrUnixSocketPrefix), sizeof(addr.sun_path)); unlink(addr.sun_path); struct evconnlistener* lev = evconnlistener_new_bind( base, nullptr, nullptr, LEV_OPT_CLOSE_ON_FREE, -1, reinterpret_cast(&addr), sizeof(addr)); if (lev == nullptr) { return false; } if (chmod(addr.sun_path, (mode_t)socket_mode) != 0) { tr_logAddWarn( fmt::format(_("Couldn't set RPC socket mode to {mode:#o}, defaulting to 0755"), fmt::arg("mode", socket_mode))); } return evhttp_bind_listener(httpd, lev) != nullptr; #endif } static void startServer(tr_rpc_server* server); static void rpc_server_on_start_retry(evutil_socket_t /*fd*/, short /*type*/, void* context) { startServer(static_cast(context)); } static int rpc_server_start_retry(tr_rpc_server* server) { int retry_delay = (server->start_retry_counter / ServerStartRetryDelayStep + 1) * ServerStartRetryDelayIncrement; retry_delay = std::min(retry_delay, int{ ServerStartRetryMaxDelay }); if (server->start_retry_timer == nullptr) { server->start_retry_timer = evtimer_new(server->session->event_base, rpc_server_on_start_retry, server); } tr_timerAdd(*server->start_retry_timer, retry_delay, 0); ++server->start_retry_counter; return retry_delay; } static void rpc_server_start_retry_cancel(tr_rpc_server* server) { if (server->start_retry_timer != nullptr) { event_free(server->start_retry_timer); server->start_retry_timer = nullptr; } server->start_retry_counter = 0; } static void startServer(tr_rpc_server* server) { if (server->httpd != nullptr) { return; } 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; 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); if (server->start_retry_counter < ServerStartRetryCount) { int const retry_delay = rpc_server_start_retry(server); tr_logAddDebug(fmt::format("Couldn't bind to {}, retrying in {} seconds", addr_port_str, retry_delay)); return; } tr_logAddError(fmt::format( ngettext( "Couldn't bind to {address} after {count} attempt, giving up", "Couldn't bind to {address} after {count} attempts, giving up", ServerStartRetryCount), fmt::arg("address", addr_port_str), fmt::arg("count", ServerStartRetryCount))); } else { evhttp_set_gencb(httpd, handle_request, server); server->httpd = httpd; tr_logAddInfo(fmt::format(_("Listening for RPC and Web requests on '{address}'"), fmt::arg("address", addr_port_str))); } rpc_server_start_retry_cancel(server); } static void stopServer(tr_rpc_server* server) { auto const lock = server->session->unique_lock(); rpc_server_start_retry_cancel(server); struct evhttp* httpd = server->httpd; if (httpd == nullptr) { return; } char const* address = tr_rpcGetBindAddress(server); server->httpd = nullptr; evhttp_free(httpd); if (server->bindAddress->type == TR_RPC_AF_UNIX) { unlink(address + std::size(TrUnixSocketPrefix)); } tr_logAddInfo(fmt::format( _("Stopped listening for RPC and Web requests on '{address}'"), fmt::arg("address", tr_rpc_address_with_port(server)))); } static void onEnabledChanged(tr_rpc_server* const server) { if (!server->isEnabled) { stopServer(server); } else { startServer(server); } } void tr_rpcSetEnabled(tr_rpc_server* server, bool isEnabled) { server->isEnabled = isEnabled; tr_runInEventThread(server->session, onEnabledChanged, server); } bool tr_rpcIsEnabled(tr_rpc_server const* server) { return server->isEnabled; } static void restartServer(tr_rpc_server* const server) { if (server->isEnabled) { stopServer(server); startServer(server); } } void tr_rpcSetPort(tr_rpc_server* server, tr_port port) { TR_ASSERT(server != nullptr); if (server->port != port) { server->port = port; if (server->isEnabled) { tr_runInEventThread(server->session, restartServer, server); } } } tr_port tr_rpcGetPort(tr_rpc_server const* server) { return server->port; } void tr_rpcSetUrl(tr_rpc_server* server, std::string_view url) { server->url = url; tr_logAddDebug(fmt::format("setting our URL to '{}'", server->url)); } std::string const& tr_rpcGetUrl(tr_rpc_server const* server) { return server->url; } static auto parseWhitelist(std::string_view whitelist) { auto list = std::list{}; while (!std::empty(whitelist)) { auto const pos = whitelist.find_first_of(" ,;"sv); auto const token = tr_strvStrip(whitelist.substr(0, pos)); list.emplace_back(token); whitelist = pos == std::string_view::npos ? ""sv : whitelist.substr(pos + 1); if (token.find_first_of("+-"sv) != std::string_view::npos) { tr_logAddWarn(fmt::format( _("Added '{entry}' to host whitelist and it has a '+' or '-'! Are you using an old ACL by mistake?"), fmt::arg("entry", token))); } else { tr_logAddInfo(fmt::format(_("Added '{entry}' to host whitelist"), fmt::arg("entry", token))); } } return list; } static void tr_rpcSetHostWhitelist(tr_rpc_server* server, std::string_view whitelist) { server->hostWhitelist = parseWhitelist(whitelist); } void tr_rpcSetWhitelist(tr_rpc_server* server, std::string_view whitelist) { server->whitelistStr = whitelist; server->whitelist = parseWhitelist(whitelist); } std::string const& tr_rpcGetWhitelist(tr_rpc_server const* server) { return server->whitelistStr; } void tr_rpcSetWhitelistEnabled(tr_rpc_server* server, bool isEnabled) { server->isWhitelistEnabled = isEnabled; } bool tr_rpcGetWhitelistEnabled(tr_rpc_server const* server) { return server->isWhitelistEnabled; } 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 ****/ void tr_rpcSetUsername(tr_rpc_server* server, std::string_view username) { server->username = username; tr_logAddDebug(fmt::format("setting our username to '{}'", server->username)); } std::string const& tr_rpcGetUsername(tr_rpc_server const* server) { return server->username; } static bool isSalted(std::string_view password) { return tr_ssha1_test(password); } void tr_rpcSetPassword(tr_rpc_server* server, std::string_view password) { server->salted_password = isSalted(password) ? password : tr_ssha1(password); tr_logAddDebug(fmt::format("setting our salted password to '{}'", server->salted_password)); } std::string const& tr_rpcGetPassword(tr_rpc_server const* server) { return server->salted_password; } void tr_rpcSetPasswordEnabled(tr_rpc_server* server, bool isEnabled) { server->isPasswordEnabled = isEnabled; tr_logAddDebug(fmt::format("setting password-enabled to '{}'", isEnabled)); } bool tr_rpcIsPasswordEnabled(tr_rpc_server const* server) { return server->isPasswordEnabled; } char const* tr_rpcGetBindAddress(tr_rpc_server const* server) { 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) { return server->isAntiBruteForceEnabled; } void tr_rpcSetAntiBruteForceEnabled(tr_rpc_server* server, bool isEnabled) { server->isAntiBruteForceEnabled = isEnabled; if (!isEnabled) { server->loginattempts = 0; } } int tr_rpcGetAntiBruteForceThreshold(tr_rpc_server const* server) { return server->antiBruteForceThreshold; } void tr_rpcSetAntiBruteForceThreshold(tr_rpc_server* server, int badRequests) { server->antiBruteForceThreshold = badRequests; } /**** ***** LIFE CYCLE ****/ static void missing_settings_key(tr_quark const q) { tr_logAddDebug(fmt::format("Couldn't find settings key '{}'", tr_quark_get_string(q))); } tr_rpc_server::tr_rpc_server(tr_session* session_in, tr_variant* settings) : compressor{ libdeflate_alloc_compressor(DeflateLevel), libdeflate_free_compressor } , bindAddress(std::make_unique()) , session{ session_in } { auto boolVal = bool{}; auto i = int64_t{}; auto sv = std::string_view{}; auto key = TR_KEY_rpc_enabled; if (!tr_variantDictFindBool(settings, key, &boolVal)) { missing_settings_key(key); } else { this->isEnabled = boolVal; } key = TR_KEY_rpc_port; if (!tr_variantDictFindInt(settings, key, &i)) { missing_settings_key(key); } else { this->port = (tr_port)i; } key = TR_KEY_rpc_url; if (!tr_variantDictFindStrView(settings, key, &sv)) { missing_settings_key(key); } else if (std::empty(sv) || sv.back() != '/') { this->url = fmt::format(FMT_STRING("{:s}/"), sv); } else { this->url = sv; } key = TR_KEY_rpc_whitelist_enabled; if (!tr_variantDictFindBool(settings, key, &boolVal)) { missing_settings_key(key); } else { tr_rpcSetWhitelistEnabled(this, boolVal); } key = TR_KEY_rpc_host_whitelist_enabled; if (!tr_variantDictFindBool(settings, key, &boolVal)) { missing_settings_key(key); } else { tr_rpcSetHostWhitelistEnabled(this, boolVal); } key = TR_KEY_rpc_host_whitelist; if (!tr_variantDictFindStrView(settings, key, &sv) && !std::empty(sv)) { missing_settings_key(key); } else { tr_rpcSetHostWhitelist(this, sv); } key = TR_KEY_rpc_authentication_required; if (!tr_variantDictFindBool(settings, key, &boolVal)) { missing_settings_key(key); } else { tr_rpcSetPasswordEnabled(this, boolVal); } key = TR_KEY_rpc_whitelist; if (!tr_variantDictFindStrView(settings, key, &sv) && !std::empty(sv)) { missing_settings_key(key); } else { tr_rpcSetWhitelist(this, sv); } key = TR_KEY_rpc_username; if (!tr_variantDictFindStrView(settings, key, &sv)) { missing_settings_key(key); } else { tr_rpcSetUsername(this, sv); } key = TR_KEY_rpc_password; if (!tr_variantDictFindStrView(settings, key, &sv)) { missing_settings_key(key); } else { tr_rpcSetPassword(this, sv); } key = TR_KEY_anti_brute_force_enabled; if (!tr_variantDictFindBool(settings, key, &boolVal)) { missing_settings_key(key); } else { tr_rpcSetAntiBruteForceEnabled(this, boolVal); } key = TR_KEY_anti_brute_force_threshold; if (!tr_variantDictFindInt(settings, key, &i)) { missing_settings_key(key); } else { 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); bindAddress->set_inaddr_any(); } else if (!tr_rpc_address_from_string(*bindAddress, sv)) { tr_logAddWarn(fmt::format( _("The '{key}' setting is '{value}' but must be an IPv4 or IPv6 address or a Unix socket path. Using default value '0.0.0.0'"), fmt::format("key", tr_quark_get_string(key)), fmt::format("value", sv))); bindAddress->set_inaddr_any(); } if (bindAddress->type == TR_RPC_AF_UNIX) { this->isWhitelistEnabled = false; this->isHostWhitelistEnabled = false; } if (this->isEnabled) { auto const rpc_uri = tr_rpc_address_with_port(this) + this->url; tr_logAddInfo(fmt::format(_("Serving RPC and Web requests on {address}"), fmt::arg("address", rpc_uri))); tr_runInEventThread(session, startServer, this); if (this->isWhitelistEnabled) { tr_logAddInfo(_("Whitelist enabled")); } if (this->isPasswordEnabled) { tr_logAddInfo(_("Password required")); } } char const* webClientDir = tr_getWebClientDir(this->session); if (!tr_str_is_empty(webClientDir)) { tr_logAddInfo(fmt::format(_("Serving RPC and Web requests from '{path}'"), fmt::arg("path", webClientDir))); } } tr_rpc_server::~tr_rpc_server() { stopServer(this); }