diff --git a/docs/CONSOLE_COMMANDS.MD b/docs/CONSOLE_COMMANDS.MD index 25b2850..4afafa1 100644 --- a/docs/CONSOLE_COMMANDS.MD +++ b/docs/CONSOLE_COMMANDS.MD @@ -18,3 +18,30 @@ start_mining **T**|start mining (**T** is the number of threads to use, must be stop_mining|stop mining exit|terminate p2pool version|show p2pool version + +### Non-interactive console access + +It's possible to send console commands via a local TCP connection. For this, you need to enable the API: `--data-api api --local-api`. + +A sample Python script that sends console commands to P2Pool via TCP: + +``` +import sys +import socket +import json + +with open('api/local/console', 'r') as file: + data = json.load(file) + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect(('127.0.0.1', data['tcp_port'])) + +command = data['cookie']; +command += sys.argv[1]; +command += '\n'; + +s.sendall(command.encode('utf-8')) +s.close() +``` + +Run it in P2Pool's directory: `python3 ./p2pool_cmd.py status` diff --git a/src/console_commands.cpp b/src/console_commands.cpp index 84f087e..47ae64a 100644 --- a/src/console_commands.cpp +++ b/src/console_commands.cpp @@ -84,8 +84,10 @@ ConsoleCommands::ConsoleCommands(p2pool* pool) } } - std::random_device rd; - std::mt19937_64 rng(rd()); + std::mt19937_64 rng(RandomDeviceSeed::instance); + + // Diffuse the initial state in case it has low quality + rng.discard(10000); for (int i = 0; i < 10; ++i) { if (start_listening(false, "127.0.0.1", 49152 + (rng() % 16384))) { @@ -98,6 +100,13 @@ ConsoleCommands::ConsoleCommands(p2pool* pool) throw std::exception(); } + // 20 random characters in the range [32, 126] should be enough for 128 bits of entropy + m_cookie.resize(20); + + for (char& c : m_cookie) { + c = static_cast(rng() % (127 - 32) + 32); + } + if (m_pool->api() && m_pool->params().m_localStats) { m_pool->api()->set(p2pool_api::Category::LOCAL, "console", [stdin_type, this](log::Stream& s) @@ -114,7 +123,7 @@ ConsoleCommands::ConsoleCommands(p2pool* pool) s << static_cast(stdin_type); } - s << "\",\"tcp_port\":" << m_listenPort << '}'; + s << "\",\"tcp_port\":" << m_listenPort << ",\"cookie\":\"" << log::EscapedString(m_cookie) << "\"}"; }); } @@ -349,7 +358,7 @@ void ConsoleCommands::stdinReadCallback(uv_stream_t* stream, ssize_t nread, cons ConsoleCommands* pThis = static_cast(stream->data); if (nread > 0) { - pThis->process_input(pThis->m_command, buf->base, static_cast(nread)); + pThis->process_input(pThis->m_command, buf->base, static_cast(nread), false); } else if (nread < 0) { LOGWARN(4, "read error " << uv_err_name(static_cast(nread))); @@ -359,7 +368,7 @@ void ConsoleCommands::stdinReadCallback(uv_stream_t* stream, ssize_t nread, cons } -void ConsoleCommands::process_input(std::string& command, const char* data, uint32_t size) +void ConsoleCommands::process_input(std::string& command, const char* data, uint32_t size, bool check_cookie) { command.append(data, size); @@ -370,31 +379,40 @@ void ConsoleCommands::process_input(std::string& command, const char* data, uint } command[k] = '\0'; - cmd* c = cmds; - for (; c->name.len; ++c) { - if (!strncmp(command.c_str(), c->name.str, c->name.len)) { - const char* args = (c->name.len + 1 <= k) ? (command.c_str() + c->name.len + 1) : ""; + if (check_cookie && ((k <= m_cookie.length()) || (memcmp(command.data(), m_cookie.data(), m_cookie.length()) != 0))) { + LOGWARN(4, "cookie check failed, skipping command " << command); + } + else { + if (check_cookie) { + command.erase(0, m_cookie.length()); + } - // Skip spaces - while ((args[0] == ' ') || (args[0] == '\t')) { - ++args; - } + cmd* c = cmds; + for (; c->name.len; ++c) { + if (!strncmp(command.c_str(), c->name.str, c->name.len)) { + const char* args = (c->name.len + 1 <= k) ? (command.c_str() + c->name.len + 1) : ""; - // Check if an argument is required - if (strlen(c->arg) && !strlen(args)) { - LOGWARN(0, c->name.str << " requires arguments"); - do_help(nullptr, nullptr); + // Skip spaces + while ((args[0] == ' ') || (args[0] == '\t')) { + ++args; + } + + // Check if an argument is required + if (strlen(c->arg) && !strlen(args)) { + LOGWARN(0, c->name.str << " requires arguments"); + do_help(nullptr, nullptr); + break; + } + + c->func(m_pool, args); break; } - - c->func(m_pool, args); - break; } - } - if (!c->name.len) { - LOGWARN(0, "Unknown command " << command.c_str()); - do_help(nullptr, nullptr); + if (!c->name.len) { + LOGWARN(0, "Unknown command " << command.c_str()); + do_help(nullptr, nullptr); + } } k = command.find_first_not_of("\r\n", k + 1); diff --git a/src/console_commands.h b/src/console_commands.h index a1eae80..097405d 100644 --- a/src/console_commands.h +++ b/src/console_commands.h @@ -40,7 +40,7 @@ public: size_t size() const override { return sizeof(ConsoleClient); } bool on_connect() override { return true; }; - bool on_read(const char* data, uint32_t size) override { static_cast(m_owner)->process_input(m_command, data, size); return true; }; + bool on_read(const char* data, uint32_t size) override { static_cast(m_owner)->process_input(m_command, data, size, true); return true; }; alignas(8) char m_consoleReadBuf[1024] = {}; @@ -62,11 +62,12 @@ private: bool m_readBufInUse; std::string m_command; + std::string m_cookie; static void allocCallback(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf); static void stdinReadCallback(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf); - void process_input(std::string& command, const char* data, uint32_t size); + void process_input(std::string& command, const char* data, uint32_t size, bool check_cookie); }; } // namespace p2pool diff --git a/src/log.h b/src/log.h index 4b27472..b2122ae 100644 --- a/src/log.h +++ b/src/log.h @@ -435,6 +435,28 @@ template<> struct log::Stream::Entry } }; +struct EscapedString +{ + explicit FORCEINLINE EscapedString(const std::string& data, const char* characters_to_escape = "\"\\", char escape_character = '\\') + { + m_data.reserve(data.length() * 2); + + for (const char c : data) { + if (strchr(characters_to_escape, c)) { + m_data.append(1, escape_character); + } + m_data.append(1, c); + } + } + + std::string m_data; +}; + +template<> struct log::Stream::Entry +{ + static FORCEINLINE void put(const EscapedString& value, Stream* wrapper) { *wrapper << value.m_data; } +}; + template struct PadRight { diff --git a/src/p2pool_api.cpp b/src/p2pool_api.cpp index dc8b521..d35e000 100644 --- a/src/p2pool_api.cpp +++ b/src/p2pool_api.cpp @@ -64,11 +64,11 @@ p2pool_api::p2pool_api(const std::string& api_path, const bool local_stats) m_poolPath = m_apiPath + "pool/"; m_localPath = m_apiPath + "local/"; - create_dir(m_networkPath); - create_dir(m_poolPath); + create_dir(m_networkPath, false); + create_dir(m_poolPath, false); if (local_stats) { - create_dir(m_localPath); + create_dir(m_localPath, true); } } @@ -77,14 +77,16 @@ p2pool_api::~p2pool_api() uv_mutex_destroy(&m_dumpDataLock); } -void p2pool_api::create_dir(const std::string& path) +void p2pool_api::create_dir(const std::string& path, bool is_restricted) { + (void) is_restricted; + #ifdef _MSC_VER int result = _mkdir(path.c_str()); #else int result = mkdir(path.c_str() #ifndef _WIN32 - , 0775 + , is_restricted ? 0750 : 0775 #endif ); #endif @@ -169,7 +171,10 @@ void p2pool_api::dump_to_file() #endif ; - const int result = uv_fs_open(uv_default_loop_checked(), &work->req, work->tmp_name.c_str(), flags, 0644, on_fs_open); + // LOCAL category has restricted access + const int mode = (work->tmp_name.find(m_localPath) == 0) ? 0640 : 0644; + + const int result = uv_fs_open(uv_default_loop_checked(), &work->req, work->tmp_name.c_str(), flags, mode, on_fs_open); if (result < 0) { LOGWARN(4, "failed to open " << work->tmp_name << ", error " << uv_err_name(result)); delete work; diff --git a/src/p2pool_api.h b/src/p2pool_api.h index 7655e9d..235ddc5 100644 --- a/src/p2pool_api.h +++ b/src/p2pool_api.h @@ -41,7 +41,7 @@ public: void set(Category category, const char* filename, T&& callback) { dump_to_file_async_internal(category, filename, Callback::Derived(std::move(callback))); } private: - void create_dir(const std::string& path); + void create_dir(const std::string& path, bool is_restricted); static void on_dump_to_file(uv_async_t* async) { reinterpret_cast(async->data)->dump_to_file(); }