diff --git a/CHANGELOG.md b/CHANGELOG.md index d5bbcaeafd..47bc205c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added + +- Basic API authentication to protect exposure of API port to the internet [#1228](https://github.com/ethereum-mining/ethminer/pull/1228). ## 0.15.0rc1 diff --git a/ethminer/main.cpp b/ethminer/main.cpp index 4460110c77..a8c7a746ae 100644 --- a/ethminer/main.cpp +++ b/ethminer/main.cpp @@ -211,6 +211,11 @@ class MinerCLI ->group(APIGroup) ->check(CLI::Range(-65535, 65535)); + app.add_option("--api-password", m_api_password, + "Set the password to protect interaction with Api server. If not set any connection is granted access." + "Be advised passwords are sent unencrypted over plain tcp !!") + ->group(APIGroup); + app.add_option("--http-port", m_http_port, "Set the web api port, the miner should listen to. Use 0 to disable. Data shown depends on hwmon setting", true) ->group(APIGroup) @@ -710,7 +715,7 @@ class MinerCLI #if API_CORE - ApiServer api(m_io_service, abs(m_api_port), (m_api_port < 0) ? true : false, f); + ApiServer api(m_io_service, abs(m_api_port), (m_api_port < 0) ? true : false, m_api_password, f); api.start(); http_server.run(m_http_port, &f, m_show_hwmonitors, m_show_power); @@ -817,6 +822,7 @@ class MinerCLI #if API_CORE int m_api_port = 0; + string m_api_password; unsigned m_http_port = 0; #endif diff --git a/libapicore/ApiServer.cpp b/libapicore/ApiServer.cpp index de4f16cdf1..a35708d5d0 100644 --- a/libapicore/ApiServer.cpp +++ b/libapicore/ApiServer.cpp @@ -2,114 +2,125 @@ #include -ApiServer::ApiServer(boost::asio::io_service& io_service, int portnum, bool readonly, Farm& f) : - m_readonly(readonly), - m_portnumber(portnum), - m_acceptor(io_service), - m_io_strand(io_service), - m_farm(f) -{ -} +ApiServer::ApiServer( + boost::asio::io_service& io_service, int portnum, bool readonly, string password, Farm& f) + : m_readonly(readonly), + m_password(std::move(password)), + m_portnumber(portnum), + m_acceptor(io_service), + m_io_strand(io_service), + m_farm(f) +{} void ApiServer::start() { - // cnote << "ApiServer::start"; - if (m_portnumber == 0) return; - - m_running.store(true, std::memory_order_relaxed); - - tcp::endpoint endpoint(tcp::v4(), m_portnumber); - - // Try to bind to port number - // if exception occurs it may be due to the fact that - // requested port is already in use by another service - try - { - m_acceptor.open(endpoint.protocol()); - m_acceptor.bind(endpoint); - m_acceptor.listen(64); - } - catch (const std::exception& _e) - { - cwarn << "Could not start API server on port : " + to_string(m_acceptor.local_endpoint().port()); - cwarn << "Ensure port is not in use by another service"; - return; - } - - cnote << "Api server listening for connections on port " + to_string(m_acceptor.local_endpoint().port()); - m_workThread = std::thread{ boost::bind(&ApiServer::begin_accept, this) }; - + // cnote << "ApiServer::start"; + if (m_portnumber == 0) + return; + + m_running.store(true, std::memory_order_relaxed); + + tcp::endpoint endpoint(tcp::v4(), m_portnumber); + + // Try to bind to port number + // if exception occurs it may be due to the fact that + // requested port is already in use by another service + try + { + m_acceptor.open(endpoint.protocol()); + m_acceptor.bind(endpoint); + m_acceptor.listen(64); + } + catch (const std::exception& _e) + { + cwarn << "Could not start API server on port : " + + to_string(m_acceptor.local_endpoint().port()); + cwarn << "Ensure port is not in use by another service"; + return; + } + + cnote << "Api server listening on port " + to_string(m_acceptor.local_endpoint().port()) + << (m_password.empty() ? "." : ". Authentication needed."); + m_workThread = std::thread{boost::bind(&ApiServer::begin_accept, this)}; } void ApiServer::stop() { - // Exit if not started - if (!m_running.load(std::memory_order_relaxed)) return; - - m_acceptor.cancel(); - m_acceptor.close(); - m_running.store(false, std::memory_order_relaxed); + // Exit if not started + if (!m_running.load(std::memory_order_relaxed)) + return; - // Dispose all sessions (if any) - m_sessions.clear(); + m_acceptor.cancel(); + m_acceptor.close(); + m_running.store(false, std::memory_order_relaxed); + // Dispose all sessions (if any) + m_sessions.clear(); } void ApiServer::begin_accept() { - if (!isRunning()) return; - - dev::setThreadName("Api"); - std::shared_ptr session = std::make_shared(m_acceptor.get_io_service(), ++lastSessionId, m_readonly, m_farm); - m_acceptor.async_accept(session->socket(), m_io_strand.wrap(boost::bind(&ApiServer::handle_accept, this, session, boost::asio::placeholders::error))); + if (!isRunning()) + return; + + dev::setThreadName("Api"); + std::shared_ptr session = std::make_shared( + m_acceptor.get_io_service(), ++lastSessionId, m_readonly, m_password, m_farm); + m_acceptor.async_accept( + session->socket(), m_io_strand.wrap(boost::bind(&ApiServer::handle_accept, this, session, + boost::asio::placeholders::error))); } void ApiServer::handle_accept(std::shared_ptr session, boost::system::error_code ec) { - // Start new connection - // cnote << "ApiServer::handle_accept"; - if (!ec) { - session->onDisconnected([&](int id) - { - // Destroy pointer to session - auto it = find_if(m_sessions.begin(), m_sessions.end(), [&id](const std::shared_ptr session) {return session->getId() == id; }); - if (it != m_sessions.end()) { - auto index = std::distance(m_sessions.begin(), it); - m_sessions.erase(m_sessions.begin() + index); - } - - }); - dev::setThreadName("Api"); - session->start(); - m_sessions.push_back(session); - cnote << "New api session from " << session->socket().remote_endpoint(); - - } - else { - session.reset(); - } - - // Resubmit new accept - begin_accept(); - + // Start new connection + // cnote << "ApiServer::handle_accept"; + if (!ec) + { + session->onDisconnected([&](int id) { + // Destroy pointer to session + auto it = find_if(m_sessions.begin(), m_sessions.end(), + [&id](const std::shared_ptr session) { + return session->getId() == id; + }); + if (it != m_sessions.end()) + { + auto index = std::distance(m_sessions.begin(), it); + m_sessions.erase(m_sessions.begin() + index); + } + }); + dev::setThreadName("Api"); + session->start(); + m_sessions.push_back(session); + cnote << "New api session from " << session->socket().remote_endpoint(); + } + else + { + session.reset(); + } + + // Resubmit new accept + begin_accept(); } void ApiConnection::disconnect() { - // cnote << "ApiConnection::disconnect"; - - // Cancel pending operations - m_socket.cancel(); - - if (m_socket.is_open()) { - - boost::system::error_code ec; - m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); - m_socket.close(ec); - } - - if (m_onDisconnected) { m_onDisconnected(this->getId()); } - + // cnote << "ApiConnection::disconnect"; + + // Cancel pending operations + m_socket.cancel(); + + if (m_socket.is_open()) + { + boost::system::error_code ec; + m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); + m_socket.close(ec); + } + + if (m_onDisconnected) + { + m_onDisconnected(this->getId()); + } } void ApiConnection::start() @@ -142,141 +153,175 @@ void ApiConnection::processRequest(Json::Value& requestObject) std::string _method = requestObject.get("method", "").asString(); jRes["id"] = requestObject.get("id", 0).asInt(); - - if (_method == "miner_getstat1") - { - jRes["result"] = getMinerStat1(); - } - else if (_method == "miner_getstathr") - { - jRes["result"] = getMinerStatHR(); - } - else if (_method == "miner_shuffle") - { - - // Gives nonce scrambler a new range - cnote << "Miner Shuffle requested"; - jRes["result"] = true; - m_farm.shuffle(); - - } - else if (_method == "miner_ping") - { - - // Replies back to (check for liveness) - jRes["result"] = "pong"; - - } - else if (_method == "miner_restart") - { - // Send response to client of success - // and invoke an async restart - // to prevent locking - if (m_readonly) - { - jRes["error"]["code"] = -32601; - jRes["error"]["message"] = "Method not available"; - } - else - { - cnote << "Miner Restart requested"; - jRes["result"] = true; - m_farm.restart_async(); - } - - } - else if (_method == "miner_reboot") - { - - // Not implemented yet - jRes["error"]["code"] = -32601; - jRes["error"]["message"] = "Method not implemented"; - - } - else - { - - // Any other method not found - jRes["error"]["code"] = -32601; - jRes["error"]["message"] = "Method not found"; - } - - // Send response - sendSocketData(jRes); - + // Check authentication + if (!m_is_authenticated) + { + if (_method == "api_authorize") + { + if (!requestObject.isMember("params") || requestObject["params"].empty() || + !requestObject["params"].isObject()) + { + jRes["error"]["code"] = -32600; + jRes["error"]["message"] = "Invalid request"; + } + else + { + Json::Value jPrm = requestObject["params"]; + if (!jPrm.isMember("psw") || jPrm["psw"].empty() || + !jPrm["psw"].isString()) + { + jRes["error"]["code"] = -32602; + jRes["error"]["message"] = "Missing password"; + } + else + { + if (jPrm.get("psw", "").asString() == m_password) + { + m_is_authenticated = true; + } + else + { + // Use error code like http 401 Unauthorized + jRes["error"]["code"] = -401; + jRes["error"]["message"] = "Invalid password"; + } + } + } + } + else + { + // Use error code like http 403 Forbidden + jRes["error"]["code"] = -403; + jRes["error"]["message"] = "Authorization needed"; + } + } + + if (m_is_authenticated) + { + if (_method == "miner_getstat1") + { + jRes["result"] = getMinerStat1(); + } + else if (_method == "miner_getstathr") + { + jRes["result"] = getMinerStatHR(); + } + else if (_method == "miner_shuffle") + { + // Gives nonce scrambler a new range + cnote << "Miner Shuffle requested"; + jRes["result"] = true; + m_farm.shuffle(); + } + else if (_method == "miner_ping") + { + // Replies back to (check for liveness) + jRes["result"] = "pong"; + } + else if (_method == "miner_restart") + { + // Send response to client of success + // and invoke an async restart + // to prevent locking + if (m_readonly) + { + jRes["error"]["code"] = -32601; + jRes["error"]["message"] = "Method not available"; + } + else + { + cnote << "Miner Restart requested"; + jRes["result"] = true; + m_farm.restart_async(); + } + } + else if (_method == "miner_reboot") + { + // Not implemented yet + jRes["error"]["code"] = -32601; + jRes["error"]["message"] = "Method not implemented"; + } + else + { + // Any other method not found + jRes["error"]["code"] = -32601; + jRes["error"]["message"] = "Method not found"; + } + } + + // Send response + sendSocketData(jRes); } void ApiConnection::recvSocketData() { - // cnote << "ApiConnection::recvSocketData"; - boost::asio::async_read_until(m_socket, m_recvBuffer, "\n", - m_io_strand.wrap(boost::bind(&ApiConnection::onRecvSocketDataCompleted, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred))); - + // cnote << "ApiConnection::recvSocketData"; + boost::asio::async_read_until(m_socket, m_recvBuffer, "\n", + m_io_strand.wrap(boost::bind(&ApiConnection::onRecvSocketDataCompleted, this, + boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred))); } void ApiConnection::onRecvSocketDataCompleted(const boost::system::error_code& ec, std::size_t bytes_transferred) { - // cnote << "ApiConnection::onRecvSocketDataCompleted"; - // Due to the nature of io_service's queue and - // the implementation of the loop this event may trigger - // late after clean disconnection. Check status of connection - // before triggering all stack of calls - - if (!ec && bytes_transferred > 0) { - - // Extract received message - std::istream is(&m_recvBuffer); - std::string message; - getline(is, message); - - if (m_socket.is_open()) { - - if (!message.empty()) { - - // Test validity of chunk and process - Json::Value jMsg; - Json::Reader jRdr; - if (jRdr.parse(message, jMsg)) { - processRequest(jMsg); - } - else { - Json::Value jRes; - jRes["jsonrpc"] = "2.0"; - jRes["id"] = Json::nullValue; - jRes["error"]["code"] = -32700; - jRes["error"]["message"] = "Parse Error"; - sendSocketData(jRes); - } - - } - - // Eventually keep reading from socket - recvSocketData(); - - } - - - } - else - { - if (m_socket.is_open()) { - disconnect(); - } - } - + // cnote << "ApiConnection::onRecvSocketDataCompleted"; + // Due to the nature of io_service's queue and + // the implementation of the loop this event may trigger + // late after clean disconnection. Check status of connection + // before triggering all stack of calls + + if (!ec && bytes_transferred > 0) + { + // Extract received message + std::istream is(&m_recvBuffer); + std::string message; + getline(is, message); + + if (m_socket.is_open()) + { + if (!message.empty()) + { + // Test validity of chunk and process + Json::Value jMsg; + Json::Reader jRdr; + if (jRdr.parse(message, jMsg)) + { + processRequest(jMsg); + } + else + { + Json::Value jRes; + jRes["jsonrpc"] = "2.0"; + jRes["id"] = Json::nullValue; + jRes["error"]["code"] = -32700; + jRes["error"]["message"] = "Parse Error"; + sendSocketData(jRes); + } + } + + // Eventually keep reading from socket + recvSocketData(); + } + } + else + { + if (m_socket.is_open()) + { + disconnect(); + } + } } void ApiConnection::sendSocketData(Json::Value const & jReq) { - if (!m_socket.is_open()) - return; - - std::ostream os(&m_sendBuffer); - os << m_jWriter.write(jReq); // Do not add lf. It's added by writer. + if (!m_socket.is_open()) + return; - async_write(m_socket, m_sendBuffer, - m_io_strand.wrap(boost::bind(&ApiConnection::onSendSocketDataCompleted, this, boost::asio::placeholders::error))); + std::ostream os(&m_sendBuffer); + os << m_jWriter.write(jReq); // Do not add lf. It's added by writer. + async_write(m_socket, m_sendBuffer, + m_io_strand.wrap(boost::bind( + &ApiConnection::onSendSocketDataCompleted, this, boost::asio::placeholders::error))); } void ApiConnection::onSendSocketDataCompleted(const boost::system::error_code& ec) { diff --git a/libapicore/ApiServer.h b/libapicore/ApiServer.h index c8b68f4532..35860e6a47 100644 --- a/libapicore/ApiServer.h +++ b/libapicore/ApiServer.h @@ -17,12 +17,18 @@ class ApiConnection { public: - ApiConnection(boost::asio::io_service& io_service, int id, bool readonly, Farm& f) : + ApiConnection(boost::asio::io_service& io_service, int id, bool readonly, string password, Farm& f) : m_sessionId(id), m_socket(io_service), m_io_strand(io_service), m_readonly(readonly), - m_farm(f) {} + m_password(std::move(password)), + m_farm(f) + { + + if (!m_password.empty()) m_is_authenticated = false; + + } ~ApiConnection() {} @@ -58,8 +64,11 @@ class ApiConnection Json::FastWriter m_jWriter; bool m_readonly = false ; + std::string m_password = ""; Farm& m_farm; + bool m_is_authenticated = true; + }; @@ -67,7 +76,7 @@ class ApiServer { public: - ApiServer(boost::asio::io_service& io_service, int portnum, bool readonly, Farm& f); + ApiServer(boost::asio::io_service& io_service, int portnum, bool readonly, string password, Farm& f); bool isRunning() { return m_running.load(std::memory_order_relaxed); }; void start(); void stop(); @@ -81,6 +90,7 @@ class ApiServer std::thread m_workThread; std::atomic m_readonly = { false }; + std::string m_password = ""; std::atomic m_running = { false }; int m_portnumber; tcp::acceptor m_acceptor;