From 8d83aa5c071b115f4a372cb18dfd851acfa3d511 Mon Sep 17 00:00:00 2001 From: Mike Ellery Date: Wed, 9 Nov 2016 14:04:20 -0800 Subject: [PATCH] Add server/connection tests (RIPD-1336): Migrate tests in uniport-test.js to cpp/jtx. Handle exceptions in WSClient and JSONRPClient constructors. Use shorter timeout for HTTP and WS Peers when client is localhost. Add missing call to start_timer in HTTP Peer. Add incomplete WS Upgrade request test to prove that server timeout is working. --- Builds/VisualStudio2015/RippleD.vcxproj | 2 + .../VisualStudio2015/RippleD.vcxproj.filters | 6 + src/ripple/server/impl/BaseHTTPPeer.h | 11 +- src/ripple/server/impl/BaseWSPeer.h | 5 +- src/ripple/test/impl/WSClient.cpp | 36 +- src/test/server/ServerStatus_test.cpp | 561 +++++++++++++++--- 6 files changed, 509 insertions(+), 112 deletions(-) diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index d9edda3aa6..ecb13c5ffe 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -236,6 +236,8 @@ + + diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 30189bf056..13c29164cd 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -25,6 +25,9 @@ {932F732F-F09E-5C50-C8A1-D62342CCAA1F} + + {0ED4CDBE-296D-2599-04B3-095BFD1668A4} + {2762284D-66E5-8B48-1F8E-67116DB1FC6B} @@ -543,6 +546,9 @@ . + + beast\test + beast\unit_test diff --git a/src/ripple/server/impl/BaseHTTPPeer.h b/src/ripple/server/impl/BaseHTTPPeer.h index e2963325e2..e84787554a 100644 --- a/src/ripple/server/impl/BaseHTTPPeer.h +++ b/src/ripple/server/impl/BaseHTTPPeer.h @@ -64,7 +64,8 @@ protected: bufferSize = 4 * 1024, // Max seconds without completing a message - timeoutSeconds = 30 + timeoutSeconds = 30, + timeoutSecondsLocal = 3 //used for localhost clients }; struct buffer @@ -277,7 +278,12 @@ BaseHTTPPeer:: start_timer() { error_code ec; - timer_.expires_from_now(std::chrono::seconds(timeoutSeconds), ec); + timer_.expires_from_now( + std::chrono::seconds( + remote_address_.address().is_loopback() ? + timeoutSecondsLocal : + timeoutSeconds), + ec); if(ec) return fail(ec, "start_timer"); timer_.async_wait(strand_.wrap(std::bind( @@ -318,6 +324,7 @@ do_read(yield_context do_yield) { complete_ = false; error_code ec; + start_timer(); beast::http::async_read(impl().stream_, read_buf_, message_, do_yield[ec]); // VFALCO What if the connection was closed? diff --git a/src/ripple/server/impl/BaseWSPeer.h b/src/ripple/server/impl/BaseWSPeer.h index fa64689236..4391add5bd 100644 --- a/src/ripple/server/impl/BaseWSPeer.h +++ b/src/ripple/server/impl/BaseWSPeer.h @@ -364,8 +364,11 @@ start_timer() { // Max seconds without completing a message static constexpr std::chrono::seconds timeout{30}; + static constexpr std::chrono::seconds timeoutLocal{3}; error_code ec; - timer_.expires_from_now(timeout, ec); + timer_.expires_from_now( + remote_endpoint().address().is_loopback() ? timeoutLocal : timeout, + ec); if(ec) return fail(ec, "start_timer"); timer_.async_wait(strand_.wrap(std::bind( diff --git a/src/ripple/test/impl/WSClient.cpp b/src/ripple/test/impl/WSClient.cpp index d4de122ba3..e0375096f1 100644 --- a/src/ripple/test/impl/WSClient.cpp +++ b/src/ripple/test/impl/WSClient.cpp @@ -109,6 +109,15 @@ class WSClientImpl : public WSClient unsigned rpc_version_; + void cleanup() + { + error_code ec; + ws_.close({}, ec); + stream_.close(ec); + work_ = boost::none; + thread_.join(); + } + public: WSClientImpl(Config const& cfg, bool v2, unsigned rpc_version) : work_(ios_) @@ -118,21 +127,26 @@ public: , ws_(stream_) , rpc_version_(rpc_version) { - auto const ep = getEndpoint(cfg, v2); - stream_.connect(ep); - ws_.handshake(ep.address().to_string() + - ":" + std::to_string(ep.port()), "/"); - ws_.async_read(op_, rb_, - strand_.wrap(std::bind(&WSClientImpl::on_read_msg, - this, beast::asio::placeholders::error))); + try + { + auto const ep = getEndpoint(cfg, v2); + stream_.connect(ep); + ws_.handshake(ep.address().to_string() + + ":" + std::to_string(ep.port()), "/"); + ws_.async_read(op_, rb_, + strand_.wrap(std::bind(&WSClientImpl::on_read_msg, + this, beast::asio::placeholders::error))); + } + catch(std::exception&) + { + cleanup(); + Rethrow(); + } } ~WSClientImpl() override { - ws_.close({}); - stream_.close(); - work_ = boost::none; - thread_.join(); + cleanup(); } Json::Value diff --git a/src/test/server/ServerStatus_test.cpp b/src/test/server/ServerStatus_test.cpp index 3ccbc39ae9..c6e1d3b1c1 100644 --- a/src/test/server/ServerStatus_test.cpp +++ b/src/test/server/ServerStatus_test.cpp @@ -19,22 +19,260 @@ #include #include +#include #include +#include +#include +#include #include #include +#include #include #include #include +#include namespace ripple { namespace test { -class ServerStatus_test : public beast::unit_test::suite +class ServerStatus_test : + public beast::unit_test::suite, public beast::test::enable_yield_to { -public: - void - testUnauthorizedRequest() + auto makeConfig( + std::string const& proto, + bool admin = true, + bool credentials = false) { + auto const section_name = + boost::starts_with(proto, "h") ? "port_rpc" : "port_ws"; + auto p = std::make_unique(); + setupConfigForUnitTests(*p); + + p->overwrite(section_name, "protocol", proto); + if(! admin) + p->overwrite(section_name, "admin", ""); + + if(credentials) + { + (*p)[section_name].set("admin_password", "p"); + (*p)[section_name].set("admin_user", "u"); + } + + p->overwrite( + boost::starts_with(proto, "h") ? "port_ws" : "port_rpc", + "protocol", + boost::starts_with(proto, "h") ? "ws" : "http"); + + if(proto == "https") + { + // this port is here to allow the env to create its internal client, + // which requires an http endpoint to talk to. In the connection + // failure test, this endpoint should never be used + (*p)["server"].append("port_alt"); + (*p)["port_alt"].set("ip", "127.0.0.1"); + (*p)["port_alt"].set("port", "8099"); + (*p)["port_alt"].set("protocol", "http"); + (*p)["port_alt"].set("admin", "127.0.0.1"); + } + + return p; + } + + auto makeWSUpgrade( + std::string const& host, + uint16_t port) + { + using namespace boost::asio; + using namespace beast::http; + request_v1 req; + + req.url = "/"; + req.version = 11; + req.headers.insert("Host", host + ":" + to_string(port)); + req.headers.insert("User-Agent", "test"); + req.method = "GET"; + req.headers.insert("Upgrade", "websocket"); + beast::websocket::detail::maskgen maskgen; + std::string key = beast::websocket::detail::make_sec_ws_key(maskgen); + req.headers.insert("Sec-WebSocket-Key", key); + req.headers.insert("Sec-WebSocket-Version", "13"); + prepare(req, connection::upgrade); + return req; + } + + auto makeHTTPRequest( + std::string const& host, + uint16_t port, + std::string const& body) + { + using namespace boost::asio; + using namespace beast::http; + request_v1 req; + + req.url = "/"; + req.version = 11; + req.headers.insert("Host", host + ":" + to_string(port)); + req.headers.insert("User-Agent", "test"); + if(body.empty()) + { + req.method = "GET"; + } + else + { + req.method = "POST"; + req.headers.insert("Content-Type", "application/json; charset=UTF-8"); + req.body = body; + } + prepare(req); + + return req; + } + + void + doRequest( + boost::asio::yield_context& yield, + beast::http::request_v1 const& req, + std::string const& host, + uint16_t port, + bool secure, + beast::http::response_v1& resp, + boost::system::error_code& ec) + { + using namespace boost::asio; + using namespace beast::http; + io_service& ios = get_io_service(); + ip::tcp::resolver r{ios}; + beast::streambuf sb; + + auto it = + r.async_resolve( + ip::tcp::resolver::query{host, to_string(port)}, yield[ec]); + if(ec) + return; + + if(secure) + { + ssl::context ctx{ssl::context::sslv23}; + ctx.set_verify_mode(ssl::verify_none); + ssl::stream ss{ios, ctx}; + async_connect(ss.next_layer(), it, yield[ec]); + if(ec) + return; + ss.async_handshake(ssl::stream_base::client, yield[ec]); + if(ec) + return; + async_write(ss, req, yield[ec]); + if(ec) + return; + async_read(ss, sb, resp, yield[ec]); + if(ec) + return; + } + else + { + ip::tcp::socket sock{ios}; + async_connect(sock, it, yield[ec]); + if(ec) + return; + async_write(sock, req, yield[ec]); + if(ec) + return; + async_read(sock, sb, resp, yield[ec]); + if(ec) + return; + } + + return; + } + + void + doWSRequest( + test::jtx::Env& env, + boost::asio::yield_context& yield, + bool secure, + beast::http::response_v1& resp, + boost::system::error_code& ec) + { + auto const port = env.app().config()["port_ws"]. + get("port"); + auto const ip = env.app().config()["port_ws"]. + get("ip"); + doRequest( + yield, makeWSUpgrade(*ip, *port), *ip, *port, secure, resp, ec); + return; + } + + void + doHTTPRequest( + test::jtx::Env& env, + boost::asio::yield_context& yield, + bool secure, + beast::http::response_v1& resp, + boost::system::error_code& ec, + std::string const& body = "") + { + auto const port = env.app().config()["port_rpc"]. + get("port"); + auto const ip = env.app().config()["port_rpc"]. + get("ip"); + doRequest( + yield, + makeHTTPRequest(*ip, *port, body), + *ip, + *port, + secure, + resp, + ec); + return; + } + + auto makeAdminRequest( + jtx::Env & env, + std::string const& proto, + std::string const& user, + std::string const& password, + bool subobject = false) + { + Json::Value jrr; + + Json::Value jp = Json::objectValue; + if(! user.empty()) + { + jp["admin_user"] = user; + if(subobject) + { + //special case of bad password..passed as object + Json::Value jpi = Json::objectValue; + jpi["admin_password"] = password; + jp["admin_password"] = jpi; + } + else + { + jp["admin_password"] = password; + } + } + + if(boost::starts_with(proto, "h")) + { + auto jrc = makeJSONRPCClient(env.app().config()); + jrr = jrc->invoke("ledger_accept", jp); + } + else + { + auto wsc = makeWSClient(env.app().config(), proto == "ws2"); + jrr = wsc->invoke("ledger_accept", jp); + } + + return jrr; + } + +// ------------ +// Test Cases +// ------------ + void + testWSClientToHttpServer(boost::asio::yield_context& yield) + { + testcase("WS client to http server fails"); using namespace jtx; Env env(*this, []() { @@ -43,61 +281,22 @@ public: p->section("port_ws").set("protocol", "http,https"); return p; }()); - auto const port = env.app().config()["port_ws"]. - get("port"); - if(! BEAST_EXPECT(port)) - return; - using namespace boost::asio; - using namespace beast::http; - io_service ios; - ip::tcp::resolver r{ios}; - beast::streambuf sb; - response_v1 resp; - boost::system::error_code ec; - - beast::websocket::detail::maskgen maskgen; - request_v1 req; - req.url = "/"; - req.version = 11; - req.method = "GET"; - req.headers.insert("Host", "127.0.0.1:" + to_string(*port)); - req.headers.insert("Upgrade", "websocket"); - std::string key = beast::websocket::detail::make_sec_ws_key(maskgen); - req.headers.insert("Sec-WebSocket-Key", key); - req.headers.insert("Sec-WebSocket-Version", "13"); - prepare(req, connection::upgrade); - - // non secure socket + //non-secure request { - ip::tcp::socket sock{ios}; - connect(sock, r.resolve( - ip::tcp::resolver::query{"127.0.0.1", to_string(*port)}), ec); - if(! BEAST_EXPECTS(! ec, ec.message())) - return; - write(sock, req, ec); - if(! BEAST_EXPECTS(! ec, ec.message())) - return; - read(sock, sb, resp, ec); + boost::system::error_code ec; + beast::http::response_v1 resp; + doWSRequest(env, yield, false, resp, ec); if(! BEAST_EXPECTS(! ec, ec.message())) return; BEAST_EXPECT(resp.status == 401); } - // secure socket + //secure request { - ssl::context ctx{ssl::context::sslv23}; - ctx.set_verify_mode(ssl::verify_none); - ssl::stream ss{ios, ctx}; - connect(ss.next_layer(), r.resolve( - ip::tcp::resolver::query{"127.0.0.1", to_string(*port)})); - ss.handshake(ssl::stream_base::client, ec); - if(! BEAST_EXPECTS(! ec, ec.message())) - return; - write(ss, req, ec); - if(! BEAST_EXPECTS(! ec, ec.message())) - return; - read(ss, sb, resp, ec); + boost::system::error_code ec; + beast::http::response_v1 resp; + doWSRequest(env, yield, true, resp, ec); if(! BEAST_EXPECTS(! ec, ec.message())) return; BEAST_EXPECT(resp.status == 401); @@ -105,78 +304,244 @@ public: } void - testStatusRequest() + testStatusRequest(boost::asio::yield_context& yield) { + testcase("Status request"); using namespace jtx; Env env(*this, []() { auto p = std::make_unique(); setupConfigForUnitTests(*p); - p->section("port_ws").set("protocol", "ws2,wss2"); + p->section("port_rpc").set("protocol", "ws2,wss2"); + p->section("port_ws").set("protocol", "http"); return p; }()); - auto const port = env.app().config()["port_ws"]. - get("port"); - if(! BEAST_EXPECT(port)) - return; - using namespace boost::asio; - using namespace beast::http; - io_service ios; - ip::tcp::resolver r{ios}; - beast::streambuf sb; - response_v1 resp; - boost::system::error_code ec; - - request_v1 req; - req.url = "/"; - req.version = 11; - req.method = "GET"; - req.headers.insert("Host", "127.0.0.1:" + to_string(*port)); - req.headers.insert("User-Agent", "test"); - prepare(req); - - // Request the status page on a non secure socket + //non-secure request { - ip::tcp::socket sock{ios}; - connect(sock, r.resolve( - ip::tcp::resolver::query{"127.0.0.1", to_string(*port)}), ec); - if(! BEAST_EXPECTS(! ec, ec.message())) - return; - write(sock, req, ec); - if(! BEAST_EXPECTS(! ec, ec.message())) - return; - read(sock, sb, resp, ec); + boost::system::error_code ec; + beast::http::response_v1 resp; + doHTTPRequest(env, yield, false, resp, ec); if(! BEAST_EXPECTS(! ec, ec.message())) return; BEAST_EXPECT(resp.status == 200); } - // Request the status page on a secure socket + //secure request { - ssl::context ctx{ssl::context::sslv23}; - ctx.set_verify_mode(ssl::verify_none); - ssl::stream ss{ios, ctx}; - connect(ss.next_layer(), r.resolve( - ip::tcp::resolver::query{"127.0.0.1", to_string(*port)})); - ss.handshake(ssl::stream_base::client, ec); - if(! BEAST_EXPECTS(! ec, ec.message())) - return; - write(ss, req, ec); - if(! BEAST_EXPECTS(! ec, ec.message())) - return; - read(ss, sb, resp, ec); + boost::system::error_code ec; + beast::http::response_v1 resp; + doHTTPRequest(env, yield, true, resp, ec); if(! BEAST_EXPECTS(! ec, ec.message())) return; BEAST_EXPECT(resp.status == 200); } }; + void + testTruncatedWSUpgrade(boost::asio::yield_context& yield) + { + testcase("Partial WS upgrade request"); + using namespace jtx; + using namespace boost::asio; + using namespace beast::http; + Env env(*this, []() + { + auto p = std::make_unique(); + setupConfigForUnitTests(*p); + p->section("port_ws").set("protocol", "ws2"); + return p; + }()); + + auto const port = env.app().config()["port_ws"]. + get("port"); + auto const ip = env.app().config()["port_ws"]. + get("ip"); + + boost::system::error_code ec; + response_v1 resp; + auto req = makeWSUpgrade(*ip, *port); + + //truncate the request message to near the value of the version header + auto req_string = boost::lexical_cast(req); + req_string.erase(req_string.find_last_of("13"), std::string::npos); + + io_service& ios = get_io_service(); + ip::tcp::resolver r{ios}; + beast::streambuf sb; + + auto it = + r.async_resolve( + ip::tcp::resolver::query{*ip, to_string(*port)}, yield[ec]); + if(! BEAST_EXPECTS(! ec, ec.message())) + return; + + ip::tcp::socket sock{ios}; + async_connect(sock, it, yield[ec]); + if(! BEAST_EXPECTS(! ec, ec.message())) + return; + async_write(sock, boost::asio::buffer(req_string), yield[ec]); + if(! BEAST_EXPECTS(! ec, ec.message())) + return; + // since we've sent an incomplete request, the server will + // keep trying to read until it gives up (by timeout) + async_read(sock, sb, resp, yield[ec]); + BEAST_EXPECT(ec); + }; + + void + testCantConnect( + std::string const& client_protocol, + std::string const& server_protocol, + boost::asio::yield_context& yield) + { + testcase << "Connect fails: " << client_protocol << " client to " << + server_protocol << " server"; + using namespace jtx; + Env env {*this, makeConfig(server_protocol)}; + + beast::http::response_v1 resp; + boost::system::error_code ec; + // The essence of this test is to have a client and server configured + // out-of-phase with respect to ssl (secure client and insecure server + // or vice-versa) - as such, here is a config to pass to + // WSClient/JSONRPCClient that configures it for a protocol that + // doesn't match the actual server + auto cfg = makeConfig(client_protocol); + if(boost::starts_with(client_protocol, "h")) + { + doHTTPRequest( + env, + yield, + client_protocol == "https", + resp, + ec); + BEAST_EXPECT(ec); + } + else + { + doWSRequest( + env, + yield, + client_protocol == "wss" || client_protocol == "wss2", + resp, + ec); + BEAST_EXPECT(ec); + } + } + + void + testAdminRequest(std::string const& proto, bool admin, bool credentials) + { + testcase << "Admin request over " << proto << + ", config " << (admin ? "enabled" : "disabled") << + ", credentials " << (credentials ? "" : "not ") << "set"; + using namespace jtx; + Env env {*this, makeConfig(proto, admin, credentials)}; + + auto const user = env.app().config() + [boost::starts_with(proto, "h") ? "port_rpc" : "port_ws"]. + get("admin_user"); + + auto const password = env.app().config() + [boost::starts_with(proto, "h") ? "port_rpc" : "port_ws"]. + get("admin_password"); + + Json::Value jrr; + + // the set of checks we do are different depending + // on how the admin config options are set + + if(admin && credentials) + { + //1 - FAILS with wrong pass + jrr = makeAdminRequest(env, proto, *user, *password + "_")[jss::result]; + BEAST_EXPECT(jrr["error"] == + boost::starts_with(proto, "h") ? "noPermission" : "forbidden"); + BEAST_EXPECT(jrr["error_message"] == + boost::starts_with(proto, "h") ? + "You don't have permission for this command." : + "Bad credentials."); + + //2 - FAILS with password in an object + jrr = makeAdminRequest(env, proto, *user, *password, true)[jss::result]; + BEAST_EXPECT(jrr["error"] == + boost::starts_with(proto, "h") ? "noPermission" : "forbidden"); + BEAST_EXPECT(jrr["error_message"] == + boost::starts_with(proto, "h") ? + "You don't have permission for this command." : + "Bad credentials."); + + //3 - FAILS with wrong user + jrr = makeAdminRequest(env, proto, *user + "_", *password)[jss::result]; + BEAST_EXPECT(jrr["error"] == + boost::starts_with(proto, "h") ? "noPermission" : "forbidden"); + BEAST_EXPECT(jrr["error_message"] == + boost::starts_with(proto, "h") ? + "You don't have permission for this command." : + "Bad credentials."); + + //4 - FAILS no credentials + jrr = makeAdminRequest(env, proto, "", "")[jss::result]; + BEAST_EXPECT(jrr["error"] == + boost::starts_with(proto, "h") ? "noPermission" : "forbidden"); + BEAST_EXPECT(jrr["error_message"] == + boost::starts_with(proto, "h") ? + "You don't have permission for this command." : + "Bad credentials."); + + //5 - SUCCEEDS with proper credentials + jrr = makeAdminRequest(env, proto, *user, *password)[jss::result]; + BEAST_EXPECT(jrr["status"] == "success"); + } + else if(admin) + { + //1 - SUCCEEDS with proper credentials + jrr = makeAdminRequest(env, proto, "u", "p")[jss::result]; + BEAST_EXPECT(jrr["status"] == "success"); + + //2 - SUCCEEDS without proper credentials + jrr = makeAdminRequest(env, proto, "", "")[jss::result]; + BEAST_EXPECT(jrr["status"] == "success"); + } + else + { + //1 - FAILS - admin disabled + jrr = makeAdminRequest(env, proto, "", "")[jss::result]; + BEAST_EXPECT(jrr["error"] == + boost::starts_with(proto, "h") ? "noPermission" : "forbidden"); + BEAST_EXPECT(jrr["error_message"] == + boost::starts_with(proto, "h") ? + "You don't have permission for this command." : + "Bad credentials."); + } + } + +public: void run() { - testUnauthorizedRequest(); - testStatusRequest(); + yield_to([&](boost::asio::yield_context& yield) + { + testWSClientToHttpServer(yield); + testStatusRequest(yield); + testTruncatedWSUpgrade(yield); + // these are secure/insecure protocol pairs, i.e. for + // each item, the second value is the secure or insecure equivalent + testCantConnect("ws", "wss", yield); + testCantConnect("ws2", "wss2", yield); + testCantConnect("http", "https", yield); + //THIS HANGS - testCantConnect("wss", "ws", yield); + testCantConnect("wss2", "ws2", yield); + testCantConnect("https", "http", yield); + }); + + for (auto it : {"http", "ws", "ws2"}) + { + testAdminRequest(it, true, true); + testAdminRequest(it, true, false); + testAdminRequest(it, false, false); + } }; };