httprpc.cpp raw
1 // Copyright (c) 2015-2022 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <httprpc.h>
6
7 #include <common/args.h>
8 #include <crypto/hmac_sha256.h>
9 #include <httpserver.h>
10 #include <logging.h>
11 #include <netaddress.h>
12 #include <rpc/protocol.h>
13 #include <rpc/server.h>
14 #include <util/fs.h>
15 #include <util/fs_helpers.h>
16 #include <util/strencodings.h>
17 #include <util/string.h>
18 #include <walletinitinterface.h>
19
20 #include <algorithm>
21 #include <atomic>
22 #include <chrono>
23 #include <fstream>
24 #include <iterator>
25 #include <map>
26 #include <memory>
27 #include <optional>
28 #include <set>
29 #include <string>
30 #include <utility>
31 #include <vector>
32
33 using util::SplitString;
34 using util::TrimStringView;
35
36 /** WWW-Authenticate to present with 401 Unauthorized response */
37 static const char* WWW_AUTH_HEADER_DATA = "Basic realm=\"jsonrpc\"";
38
39 /** Simple one-shot callback timer to be used by the RPC mechanism to e.g.
40 * re-lock the wallet.
41 */
42 class HTTPRPCTimer : public RPCTimerBase
43 {
44 public:
45 HTTPRPCTimer(struct event_base* eventBase, std::function<void()>& func, int64_t millis) :
46 ev(eventBase, false, func)
47 {
48 struct timeval tv;
49 tv.tv_sec = millis/1000;
50 tv.tv_usec = (millis%1000)*1000;
51 ev.trigger(&tv);
52 }
53 private:
54 HTTPEvent ev;
55 };
56
57 class HTTPRPCTimerInterface : public RPCTimerInterface
58 {
59 public:
60 explicit HTTPRPCTimerInterface(struct event_base* _base) : base(_base)
61 {
62 }
63 const char* Name() override
64 {
65 return "HTTP";
66 }
67 RPCTimerBase* NewTimer(std::function<void()>& func, int64_t millis) override
68 {
69 return new HTTPRPCTimer(base, func, millis);
70 }
71 private:
72 struct event_base* base;
73 };
74
75
76 /* Stored RPC timer interface (for unregistration) */
77 static std::unique_ptr<HTTPRPCTimerInterface> httpRPCTimerInterface;
78 /* List of -rpcauth values */
79 static std::vector<std::vector<std::string>> g_rpcauth;
80 /* RPC Auth Whitelist */
81 static std::map<std::string, std::set<std::string>> g_rpc_whitelist;
82 static bool g_rpc_whitelist_default = false;
83
84 static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCRequest& jreq)
85 {
86 // Sending HTTP errors is a legacy JSON-RPC behavior.
87 Assume(jreq.m_json_version != JSONRPCVersion::V2);
88
89 // Send error reply from json-rpc error object
90 int nStatus = HTTP_INTERNAL_SERVER_ERROR;
91 int code = objError.find_value("code").getInt<int>();
92
93 if (code == RPC_INVALID_REQUEST)
94 nStatus = HTTP_BAD_REQUEST;
95 else if (code == RPC_METHOD_NOT_FOUND)
96 nStatus = HTTP_NOT_FOUND;
97
98 std::string strReply = JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version).write() + "\n";
99
100 req->WriteHeader("Content-Type", "application/json");
101 req->WriteReply(nStatus, strReply);
102 }
103
104 //This function checks username and password against -rpcauth
105 //entries from config file.
106 static bool multiUserAuthorized(std::string strUserPass, std::string& out_wallet_restriction)
107 {
108 if (strUserPass.find(':') == std::string::npos) {
109 return false;
110 }
111 std::string strUser = strUserPass.substr(0, strUserPass.find(':'));
112 std::string strPass = strUserPass.substr(strUserPass.find(':') + 1);
113
114 for (const auto& vFields : g_rpcauth) {
115 std::string strName = vFields[0];
116 if (!TimingResistantEqual(strName, strUser)) {
117 continue;
118 }
119
120 std::string strSalt = vFields[1];
121 std::string strHash = vFields[2];
122
123 static const unsigned int KEY_SIZE = 32;
124 unsigned char out[KEY_SIZE];
125
126 CHMAC_SHA256(reinterpret_cast<const unsigned char*>(strSalt.data()), strSalt.size()).Write(reinterpret_cast<const unsigned char*>(strPass.data()), strPass.size()).Finalize(out);
127 std::vector<unsigned char> hexvec(out, out+KEY_SIZE);
128 std::string strHashFromPass = HexStr(hexvec);
129
130 if (TimingResistantEqual(strHashFromPass, strHash)) {
131 out_wallet_restriction = (vFields.size() > 3) ? vFields[3] : "";
132 return true;
133 }
134 }
135 return false;
136 }
137
138 static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUsernameOut, std::string& out_wallet_restriction)
139 {
140 if (strAuth.substr(0, 6) != "Basic ")
141 return false;
142 std::string_view strUserPass64 = TrimStringView(std::string_view{strAuth}.substr(6));
143 auto userpass_data = DecodeBase64(strUserPass64);
144 std::string strUserPass;
145 if (!userpass_data) return false;
146 strUserPass.assign(userpass_data->begin(), userpass_data->end());
147
148 if (strUserPass.find(':') != std::string::npos)
149 strAuthUsernameOut = strUserPass.substr(0, strUserPass.find(':'));
150
151 return multiUserAuthorized(strUserPass, out_wallet_restriction);
152 }
153
154 static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
155 {
156 // JSONRPC handles only POST
157 if (req->GetRequestMethod() != HTTPRequest::POST) {
158 req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests");
159 return false;
160 }
161 // Check authorization
162 std::pair<bool, std::string> authHeader = req->GetHeader("authorization");
163 if (!authHeader.first) {
164 req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
165 req->WriteReply(HTTP_UNAUTHORIZED);
166 return false;
167 }
168
169 JSONRPCRequest jreq;
170 jreq.context = context;
171 jreq.peerAddr = req->GetPeer().ToStringAddrPort();
172
173 // Exponential backoff for failed authentication attempts (shared across all worker threads)
174 static std::atomic<std::chrono::milliseconds::rep> auth_delay_ms{250};
175
176 if (!RPCAuthorized(authHeader.second, jreq.authUser, jreq.m_wallet_restriction)) {
177 LogWarning("ThreadRPCServer incorrect password attempt from %s", jreq.peerAddr);
178
179 auto delay = std::chrono::milliseconds{auth_delay_ms.load(std::memory_order_relaxed)};
180 UninterruptibleSleep(delay);
181 auth_delay_ms.store(std::min<std::chrono::milliseconds::rep>(
182 delay.count() * 2, 8000), std::memory_order_relaxed);
183
184 req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
185 req->WriteReply(HTTP_UNAUTHORIZED);
186 return false;
187 }
188
189 // Reset backoff on successful authentication
190 auth_delay_ms.store(250, std::memory_order_relaxed);
191
192 try {
193 // Parse request
194 UniValue valRequest;
195 if (!valRequest.read(req->ReadBody()))
196 throw JSONRPCError(RPC_PARSE_ERROR, "Parse error");
197
198 // Set the URI
199 jreq.URI = req->GetURI();
200
201 UniValue reply;
202 bool user_has_whitelist = g_rpc_whitelist.count(jreq.authUser);
203 if (!user_has_whitelist && g_rpc_whitelist_default) {
204 LogWarning("RPC User %s not allowed to call any methods", jreq.authUser);
205 req->WriteReply(HTTP_FORBIDDEN);
206 return false;
207
208 // singleton request
209 } else if (valRequest.isObject()) {
210 jreq.parse(valRequest);
211 if (user_has_whitelist && !g_rpc_whitelist[jreq.authUser].count(jreq.strMethod)) {
212 LogWarning("RPC User %s not allowed to call method %s", jreq.authUser, jreq.strMethod);
213 req->WriteReply(HTTP_FORBIDDEN);
214 return false;
215 }
216
217 // Legacy 1.0/1.1 behavior is for failed requests to throw
218 // exceptions which return HTTP errors and RPC errors to the client.
219 // 2.0 behavior is to catch exceptions and return HTTP success with
220 // RPC errors, as long as there is not an actual HTTP server error.
221 const bool catch_errors{jreq.m_json_version == JSONRPCVersion::V2};
222 reply = JSONRPCExec(jreq, catch_errors);
223
224 if (jreq.IsNotification()) {
225 // Even though we do execute notifications, we do not respond to them
226 req->WriteReply(HTTP_NO_CONTENT);
227 return true;
228 }
229
230 // array of requests
231 } else if (valRequest.isArray()) {
232 // Check authorization for each request's method
233 if (user_has_whitelist) {
234 for (unsigned int reqIdx = 0; reqIdx < valRequest.size(); reqIdx++) {
235 if (!valRequest[reqIdx].isObject()) {
236 throw JSONRPCError(RPC_INVALID_REQUEST, "Invalid Request object");
237 } else {
238 const UniValue& request = valRequest[reqIdx].get_obj();
239 // Parse method
240 std::string strMethod = request.find_value("method").get_str();
241 if (!g_rpc_whitelist[jreq.authUser].count(strMethod)) {
242 LogWarning("RPC User %s not allowed to call method %s", jreq.authUser, strMethod);
243 req->WriteReply(HTTP_FORBIDDEN);
244 return false;
245 }
246 }
247 }
248 }
249
250 // Execute each request
251 static constexpr size_t MAX_BATCH_SIZE = 100;
252 if (valRequest.size() > MAX_BATCH_SIZE) {
253 req->WriteReply(HTTP_BAD_REQUEST, "Batch request exceeds maximum size");
254 return false;
255 }
256 reply = UniValue::VARR;
257 for (size_t i{0}; i < valRequest.size(); ++i) {
258 // Batches never throw HTTP errors, they are always just included
259 // in "HTTP OK" responses. Notifications never get any response.
260 UniValue response;
261 try {
262 jreq.parse(valRequest[i]);
263 response = JSONRPCExec(jreq, /*catch_errors=*/true);
264 } catch (UniValue& e) {
265 response = JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
266 } catch (const std::exception& e) {
267 response = JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version);
268 }
269 if (!jreq.IsNotification()) {
270 reply.push_back(std::move(response));
271 }
272 }
273 // Return no response for an all-notification batch, but only if the
274 // batch request is non-empty. Technically according to the JSON-RPC
275 // 2.0 spec, an empty batch request should also return no response,
276 // However, if the batch request is empty, it means the request did
277 // not contain any JSON-RPC version numbers, so returning an empty
278 // response could break backwards compatibility with old RPC clients
279 // relying on previous behavior. Return an empty array instead of an
280 // empty response in this case to favor being backwards compatible
281 // over complying with the JSON-RPC 2.0 spec in this case.
282 if (reply.size() == 0 && valRequest.size() > 0) {
283 req->WriteReply(HTTP_NO_CONTENT);
284 return true;
285 }
286 }
287 else
288 throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
289
290 req->WriteHeader("Content-Type", "application/json");
291 req->WriteReply(HTTP_OK, reply.write() + "\n");
292 } catch (UniValue& e) {
293 JSONErrorReply(req, std::move(e), jreq);
294 return false;
295 } catch (const std::exception& e) {
296 JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq);
297 return false;
298 }
299 return true;
300 }
301
302 static bool InitRPCAuthentication()
303 {
304 std::string strRPCUserColonPass;
305
306 if (gArgs.GetArg("-rpcpassword", "") == "")
307 {
308 std::optional<fs::perms> cookie_perms{std::nullopt};
309 auto cookie_perms_arg{gArgs.GetArg("-rpccookieperms")};
310 if (cookie_perms_arg) {
311 if (*cookie_perms_arg == "0") {
312 cookie_perms = std::nullopt;
313 } else if (cookie_perms_arg->empty() || *cookie_perms_arg == "1") {
314 // leave at default
315 } else {
316 auto perm_opt = InterpretPermString(*cookie_perms_arg);
317 if (!perm_opt) {
318 LogError("Invalid -rpccookieperms=%s; must be one of 'owner', 'group', or 'all'.", *cookie_perms_arg);
319 return false;
320 }
321 cookie_perms = *perm_opt;
322 }
323 }
324
325 if (!GenerateAuthCookie(&strRPCUserColonPass, std::make_pair(cookie_perms, bool(cookie_perms_arg)))) {
326 return false;
327 }
328 if (strRPCUserColonPass.empty()) {
329 LogInfo("RPC authentication cookie file generation is disabled.");
330 } else {
331 LogInfo("Using random cookie authentication.");
332 }
333 } else {
334 LogInfo("Using rpcuser/rpcpassword authentication.");
335 LogWarning("The use of rpcuser/rpcpassword is less secure, because credentials are configured in plain text. It is recommended that locally-run instances switch to cookie-based auth, or otherwise to use hashed rpcauth credentials. See share/rpcauth in the source directory for more information.");
336 strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
337 }
338
339 // If there is a plaintext credential, hash it with a random salt before storage.
340 if (!strRPCUserColonPass.empty()) {
341 std::vector<std::string> fields{SplitString(strRPCUserColonPass, ':')};
342 if (fields.size() != 2) {
343 LogError("Unable to parse RPC credentials. The configured rpcuser or rpcpassword cannot contain a \":\".");
344 return false;
345 }
346 const std::string& user = fields[0];
347 const std::string& pass = fields[1];
348
349 // Generate a random 16 byte hex salt.
350 std::array<unsigned char, 16> raw_salt;
351 GetStrongRandBytes(raw_salt);
352 std::string salt = HexStr(raw_salt);
353
354 // Compute HMAC.
355 std::array<unsigned char, CHMAC_SHA256::OUTPUT_SIZE> out;
356 CHMAC_SHA256(UCharCast(salt.data()), salt.size()).Write(UCharCast(pass.data()), pass.size()).Finalize(out.data());
357 std::string hash = HexStr(out);
358
359 g_rpcauth.push_back({user, salt, hash});
360 }
361
362 constexpr auto AddRPCAuth = [](const std::string& rpcauth) {
363 std::vector<std::string> fields{SplitString(rpcauth, ':')};
364 if (fields.size() < 2 || fields.size() > 3) {
365 return false;
366 }
367 const std::vector<std::string> salt_hmac{SplitString(fields[1], '$')};
368 if (salt_hmac.size() == 2) {
369 fields.erase(fields.begin() + 1);
370 fields.insert(fields.begin() + 1, salt_hmac.begin(), salt_hmac.end());
371 g_rpcauth.push_back(fields);
372 } else {
373 return false;
374 }
375 return true;
376 };
377 if (!(gArgs.IsArgNegated("-rpcauth") || (gArgs.GetArgs("-rpcauth").empty() && gArgs.GetArgs("-rpcauthfile").empty()))) {
378 LogInfo("Using rpcauth authentication.\n");
379 for (const std::string& rpcauth : gArgs.GetArgs("-rpcauth")) {
380 if (rpcauth.empty()) continue;
381 if (!AddRPCAuth(rpcauth)) {
382 LogWarning("Invalid -rpcauth argument.");
383 return false;
384 }
385 }
386 for (const std::string& path : gArgs.GetArgs("-rpcauthfile")) {
387 std::ifstream file;
388 file.open(path);
389 if (!file.is_open()) continue;
390 std::string rpcauth;
391 size_t lineno = 0;
392 while (std::getline(file, rpcauth)) {
393 ++lineno;
394 if (!AddRPCAuth(rpcauth)) {
395 LogPrintf("WARNING: Invalid line %s in -rpcauthfile=%s; ignoring\n", lineno, path);
396 }
397 }
398 }
399 }
400
401 g_rpc_whitelist_default = gArgs.GetBoolArg("-rpcwhitelistdefault", !gArgs.GetArgs("-rpcwhitelist").empty());
402 for (const std::string& strRPCWhitelist : gArgs.GetArgs("-rpcwhitelist")) {
403 auto pos = strRPCWhitelist.find(':');
404 std::string strUser = strRPCWhitelist.substr(0, pos);
405 bool intersect = g_rpc_whitelist.count(strUser);
406 std::set<std::string>& whitelist = g_rpc_whitelist[strUser];
407 if (pos != std::string::npos) {
408 std::string strWhitelist = strRPCWhitelist.substr(pos + 1);
409 std::vector<std::string> whitelist_split = SplitString(strWhitelist, ", ");
410 std::set<std::string> new_whitelist{
411 std::make_move_iterator(whitelist_split.begin()),
412 std::make_move_iterator(whitelist_split.end())};
413 if (intersect) {
414 std::set<std::string> tmp_whitelist;
415 std::set_intersection(new_whitelist.begin(), new_whitelist.end(),
416 whitelist.begin(), whitelist.end(), std::inserter(tmp_whitelist, tmp_whitelist.end()));
417 new_whitelist = std::move(tmp_whitelist);
418 }
419 whitelist = std::move(new_whitelist);
420 }
421 }
422
423 return true;
424 }
425
426 bool StartHTTPRPC(const std::any& context)
427 {
428 LogDebug(BCLog::RPC, "Starting HTTP RPC server\n");
429 if (!InitRPCAuthentication())
430 return false;
431
432 auto handle_rpc = [context](HTTPRequest* req, const std::string&) { return HTTPReq_JSONRPC(context, req); };
433 RegisterHTTPHandler("/", true, handle_rpc);
434 if (g_wallet_init_interface.HasWalletSupport()) {
435 RegisterHTTPHandler("/wallet/", false, handle_rpc);
436 }
437 struct event_base* eventBase = EventBase();
438 assert(eventBase);
439 httpRPCTimerInterface = std::make_unique<HTTPRPCTimerInterface>(eventBase);
440 RPCSetTimerInterface(httpRPCTimerInterface.get());
441 return true;
442 }
443
444 void InterruptHTTPRPC()
445 {
446 LogDebug(BCLog::RPC, "Interrupting HTTP RPC server\n");
447 }
448
449 void StopHTTPRPC()
450 {
451 LogDebug(BCLog::RPC, "Stopping HTTP RPC server\n");
452 UnregisterHTTPHandler("/", true);
453 if (g_wallet_init_interface.HasWalletSupport()) {
454 UnregisterHTTPHandler("/wallet/", false);
455 }
456 if (httpRPCTimerInterface) {
457 RPCUnsetTimerInterface(httpRPCTimerInterface.get());
458 httpRPCTimerInterface.reset();
459 }
460 }
461
462 std::set<std::string> GetWhitelistedRpcs(const std::string& user_name)
463 {
464 if (auto it = g_rpc_whitelist.find(user_name); it != g_rpc_whitelist.end()) {
465 return it->second;
466 }
467 if (g_rpc_whitelist_default) {
468 return std::set<std::string>();
469 }
470
471 // Build a list of every method
472 std::set<std::string> allowed_methods;
473 for (const auto& method_name : tableRPC.listCommands()) {
474 allowed_methods.insert(method_name);
475 }
476 return allowed_methods;
477 }
478