71cbeaad9a
When no `-rpcpassword` is specified, use a special 'cookie' file for authentication. This file is generated with random content when the daemon starts, and deleted when it exits. Read access to this file controls who can access through RPC. By default this file is stored in the data directory but it be overriden with `-rpccookiefile`. This is similar to Tor CookieAuthentication: see https://www.torproject.org/docs/tor-manual.html.en Alternative to #6258. Like that pull, this allows running bitcoind without any manual configuration. However, daemons should ideally never write to their configuration files, so I prefer this solution.
357 lines
10 KiB
C++
357 lines
10 KiB
C++
// Copyright (c) 2010 Satoshi Nakamoto
|
|
// Copyright (c) 2009-2014 The Bitcoin Core developers
|
|
// Distributed under the MIT software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#include "rpcprotocol.h"
|
|
|
|
#include "clientversion.h"
|
|
#include "random.h"
|
|
#include "tinyformat.h"
|
|
#include "util.h"
|
|
#include "utilstrencodings.h"
|
|
#include "utiltime.h"
|
|
#include "version.h"
|
|
|
|
#include <stdint.h>
|
|
#include <fstream>
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/asio.hpp>
|
|
#include <boost/asio/ssl.hpp>
|
|
#include <boost/bind.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/foreach.hpp>
|
|
#include <boost/iostreams/concepts.hpp>
|
|
#include <boost/iostreams/stream.hpp>
|
|
#include <boost/shared_ptr.hpp>
|
|
|
|
#include "univalue/univalue.h"
|
|
|
|
using namespace std;
|
|
|
|
//! Number of bytes to allocate and read at most at once in post data
|
|
const size_t POST_READ_SIZE = 256 * 1024;
|
|
|
|
/**
|
|
* HTTP protocol
|
|
*
|
|
* This ain't Apache. We're just using HTTP header for the length field
|
|
* and to be compatible with other JSON-RPC implementations.
|
|
*/
|
|
|
|
string HTTPPost(const string& strMsg, const map<string,string>& mapRequestHeaders)
|
|
{
|
|
ostringstream s;
|
|
s << "POST / HTTP/1.1\r\n"
|
|
<< "User-Agent: bitcoin-json-rpc/" << FormatFullVersion() << "\r\n"
|
|
<< "Host: 127.0.0.1\r\n"
|
|
<< "Content-Type: application/json\r\n"
|
|
<< "Content-Length: " << strMsg.size() << "\r\n"
|
|
<< "Connection: close\r\n"
|
|
<< "Accept: application/json\r\n";
|
|
BOOST_FOREACH(const PAIRTYPE(string, string)& item, mapRequestHeaders)
|
|
s << item.first << ": " << item.second << "\r\n";
|
|
s << "\r\n" << strMsg;
|
|
|
|
return s.str();
|
|
}
|
|
|
|
static string rfc1123Time()
|
|
{
|
|
return DateTimeStrFormat("%a, %d %b %Y %H:%M:%S +0000", GetTime());
|
|
}
|
|
|
|
static const char *httpStatusDescription(int nStatus)
|
|
{
|
|
switch (nStatus) {
|
|
case HTTP_OK: return "OK";
|
|
case HTTP_BAD_REQUEST: return "Bad Request";
|
|
case HTTP_FORBIDDEN: return "Forbidden";
|
|
case HTTP_NOT_FOUND: return "Not Found";
|
|
case HTTP_INTERNAL_SERVER_ERROR: return "Internal Server Error";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
string HTTPError(int nStatus, bool keepalive, bool headersOnly)
|
|
{
|
|
if (nStatus == HTTP_UNAUTHORIZED)
|
|
return strprintf("HTTP/1.0 401 Authorization Required\r\n"
|
|
"Date: %s\r\n"
|
|
"Server: bitcoin-json-rpc/%s\r\n"
|
|
"WWW-Authenticate: Basic realm=\"jsonrpc\"\r\n"
|
|
"Content-Type: text/html\r\n"
|
|
"Content-Length: 296\r\n"
|
|
"\r\n"
|
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\r\n"
|
|
"\"http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd\">\r\n"
|
|
"<HTML>\r\n"
|
|
"<HEAD>\r\n"
|
|
"<TITLE>Error</TITLE>\r\n"
|
|
"<META HTTP-EQUIV='Content-Type' CONTENT='text/html; charset=ISO-8859-1'>\r\n"
|
|
"</HEAD>\r\n"
|
|
"<BODY><H1>401 Unauthorized.</H1></BODY>\r\n"
|
|
"</HTML>\r\n", rfc1123Time(), FormatFullVersion());
|
|
|
|
return HTTPReply(nStatus, httpStatusDescription(nStatus), keepalive,
|
|
headersOnly, "text/plain");
|
|
}
|
|
|
|
string HTTPReplyHeader(int nStatus, bool keepalive, size_t contentLength, const char *contentType)
|
|
{
|
|
return strprintf(
|
|
"HTTP/1.1 %d %s\r\n"
|
|
"Date: %s\r\n"
|
|
"Connection: %s\r\n"
|
|
"Content-Length: %u\r\n"
|
|
"Content-Type: %s\r\n"
|
|
"Server: bitcoin-json-rpc/%s\r\n"
|
|
"\r\n",
|
|
nStatus,
|
|
httpStatusDescription(nStatus),
|
|
rfc1123Time(),
|
|
keepalive ? "keep-alive" : "close",
|
|
contentLength,
|
|
contentType,
|
|
FormatFullVersion());
|
|
}
|
|
|
|
string HTTPReply(int nStatus, const string& strMsg, bool keepalive,
|
|
bool headersOnly, const char *contentType)
|
|
{
|
|
if (headersOnly)
|
|
{
|
|
return HTTPReplyHeader(nStatus, keepalive, 0, contentType);
|
|
} else {
|
|
return HTTPReplyHeader(nStatus, keepalive, strMsg.size(), contentType) + strMsg;
|
|
}
|
|
}
|
|
|
|
bool ReadHTTPRequestLine(std::basic_istream<char>& stream, int &proto,
|
|
string& http_method, string& http_uri)
|
|
{
|
|
string str;
|
|
getline(stream, str);
|
|
|
|
// HTTP request line is space-delimited
|
|
vector<string> vWords;
|
|
boost::split(vWords, str, boost::is_any_of(" "));
|
|
if (vWords.size() < 2)
|
|
return false;
|
|
|
|
// HTTP methods permitted: GET, POST
|
|
http_method = vWords[0];
|
|
if (http_method != "GET" && http_method != "POST")
|
|
return false;
|
|
|
|
// HTTP URI must be an absolute path, relative to current host
|
|
http_uri = vWords[1];
|
|
if (http_uri.size() == 0 || http_uri[0] != '/')
|
|
return false;
|
|
|
|
// parse proto, if present
|
|
string strProto = "";
|
|
if (vWords.size() > 2)
|
|
strProto = vWords[2];
|
|
|
|
proto = 0;
|
|
const char *ver = strstr(strProto.c_str(), "HTTP/1.");
|
|
if (ver != NULL)
|
|
proto = atoi(ver+7);
|
|
|
|
return true;
|
|
}
|
|
|
|
int ReadHTTPStatus(std::basic_istream<char>& stream, int &proto)
|
|
{
|
|
string str;
|
|
getline(stream, str);
|
|
vector<string> vWords;
|
|
boost::split(vWords, str, boost::is_any_of(" "));
|
|
if (vWords.size() < 2)
|
|
return HTTP_INTERNAL_SERVER_ERROR;
|
|
proto = 0;
|
|
const char *ver = strstr(str.c_str(), "HTTP/1.");
|
|
if (ver != NULL)
|
|
proto = atoi(ver+7);
|
|
return atoi(vWords[1].c_str());
|
|
}
|
|
|
|
int ReadHTTPHeaders(std::basic_istream<char>& stream, map<string, string>& mapHeadersRet)
|
|
{
|
|
int nLen = 0;
|
|
while (true)
|
|
{
|
|
string str;
|
|
std::getline(stream, str);
|
|
if (str.empty() || str == "\r")
|
|
break;
|
|
string::size_type nColon = str.find(":");
|
|
if (nColon != string::npos)
|
|
{
|
|
string strHeader = str.substr(0, nColon);
|
|
boost::trim(strHeader);
|
|
boost::to_lower(strHeader);
|
|
string strValue = str.substr(nColon+1);
|
|
boost::trim(strValue);
|
|
mapHeadersRet[strHeader] = strValue;
|
|
if (strHeader == "content-length")
|
|
nLen = atoi(strValue.c_str());
|
|
}
|
|
}
|
|
return nLen;
|
|
}
|
|
|
|
|
|
int ReadHTTPMessage(std::basic_istream<char>& stream, map<string,
|
|
string>& mapHeadersRet, string& strMessageRet,
|
|
int nProto, size_t max_size)
|
|
{
|
|
mapHeadersRet.clear();
|
|
strMessageRet = "";
|
|
|
|
// Read header
|
|
int nLen = ReadHTTPHeaders(stream, mapHeadersRet);
|
|
if (nLen < 0 || (size_t)nLen > max_size)
|
|
return HTTP_INTERNAL_SERVER_ERROR;
|
|
|
|
// Read message
|
|
if (nLen > 0)
|
|
{
|
|
vector<char> vch;
|
|
size_t ptr = 0;
|
|
while (ptr < (size_t)nLen)
|
|
{
|
|
size_t bytes_to_read = std::min((size_t)nLen - ptr, POST_READ_SIZE);
|
|
vch.resize(ptr + bytes_to_read);
|
|
stream.read(&vch[ptr], bytes_to_read);
|
|
if (!stream) // Connection lost while reading
|
|
return HTTP_INTERNAL_SERVER_ERROR;
|
|
ptr += bytes_to_read;
|
|
}
|
|
strMessageRet = string(vch.begin(), vch.end());
|
|
}
|
|
|
|
string sConHdr = mapHeadersRet["connection"];
|
|
|
|
if ((sConHdr != "close") && (sConHdr != "keep-alive"))
|
|
{
|
|
if (nProto >= 1)
|
|
mapHeadersRet["connection"] = "keep-alive";
|
|
else
|
|
mapHeadersRet["connection"] = "close";
|
|
}
|
|
|
|
return HTTP_OK;
|
|
}
|
|
|
|
/**
|
|
* JSON-RPC protocol. Bitcoin speaks version 1.0 for maximum compatibility,
|
|
* but uses JSON-RPC 1.1/2.0 standards for parts of the 1.0 standard that were
|
|
* unspecified (HTTP errors and contents of 'error').
|
|
*
|
|
* 1.0 spec: http://json-rpc.org/wiki/specification
|
|
* 1.2 spec: http://jsonrpc.org/historical/json-rpc-over-http.html
|
|
*/
|
|
|
|
string JSONRPCRequest(const string& strMethod, const UniValue& params, const UniValue& id)
|
|
{
|
|
UniValue request(UniValue::VOBJ);
|
|
request.push_back(Pair("method", strMethod));
|
|
request.push_back(Pair("params", params));
|
|
request.push_back(Pair("id", id));
|
|
return request.write() + "\n";
|
|
}
|
|
|
|
UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const UniValue& id)
|
|
{
|
|
UniValue reply(UniValue::VOBJ);
|
|
if (!error.isNull())
|
|
reply.push_back(Pair("result", NullUniValue));
|
|
else
|
|
reply.push_back(Pair("result", result));
|
|
reply.push_back(Pair("error", error));
|
|
reply.push_back(Pair("id", id));
|
|
return reply;
|
|
}
|
|
|
|
string JSONRPCReply(const UniValue& result, const UniValue& error, const UniValue& id)
|
|
{
|
|
UniValue reply = JSONRPCReplyObj(result, error, id);
|
|
return reply.write() + "\n";
|
|
}
|
|
|
|
UniValue JSONRPCError(int code, const string& message)
|
|
{
|
|
UniValue error(UniValue::VOBJ);
|
|
error.push_back(Pair("code", code));
|
|
error.push_back(Pair("message", message));
|
|
return error;
|
|
}
|
|
|
|
/** Username used when cookie authentication is in use (arbitrary, only for
|
|
* recognizability in debugging/logging purposes)
|
|
*/
|
|
static const std::string COOKIEAUTH_USER = "__cookie__";
|
|
/** Default name for auth cookie file */
|
|
static const std::string COOKIEAUTH_FILE = ".cookie";
|
|
|
|
boost::filesystem::path GetAuthCookieFile()
|
|
{
|
|
boost::filesystem::path path(GetArg("-rpccookiefile", COOKIEAUTH_FILE));
|
|
if (!path.is_complete()) path = GetDataDir() / path;
|
|
return path;
|
|
}
|
|
|
|
bool GenerateAuthCookie(std::string *cookie_out)
|
|
{
|
|
unsigned char rand_pwd[32];
|
|
GetRandBytes(rand_pwd, 32);
|
|
std::string cookie = COOKIEAUTH_USER + ":" + EncodeBase64(&rand_pwd[0],32);
|
|
|
|
/** the umask determines what permissions are used to create this file -
|
|
* these are set to 077 in init.cpp unless overridden with -sysperms.
|
|
*/
|
|
std::ofstream file;
|
|
boost::filesystem::path filepath = GetAuthCookieFile();
|
|
file.open(filepath.string().c_str());
|
|
if (!file.is_open()) {
|
|
LogPrintf("Unable to open cookie authentication file %s for writing\n", filepath.string());
|
|
return false;
|
|
}
|
|
file << cookie;
|
|
file.close();
|
|
LogPrintf("Generated RPC authentication cookie %s\n", filepath.string());
|
|
|
|
if (cookie_out)
|
|
*cookie_out = cookie;
|
|
return true;
|
|
}
|
|
|
|
bool GetAuthCookie(std::string *cookie_out)
|
|
{
|
|
std::ifstream file;
|
|
std::string cookie;
|
|
boost::filesystem::path filepath = GetAuthCookieFile();
|
|
file.open(filepath.string().c_str());
|
|
if (!file.is_open())
|
|
return false;
|
|
std::getline(file, cookie);
|
|
file.close();
|
|
|
|
if (cookie_out)
|
|
*cookie_out = cookie;
|
|
return true;
|
|
}
|
|
|
|
void DeleteAuthCookie()
|
|
{
|
|
try {
|
|
boost::filesystem::remove(GetAuthCookieFile());
|
|
} catch (const boost::filesystem::filesystem_error& e) {
|
|
LogPrintf("%s: Unable to remove random auth cookie file: %s\n", __func__, e.what());
|
|
}
|
|
}
|
|
|