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);
+ }
};
};