diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj
index d68e526e2f..afda7ec289 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj
+++ b/Builds/VisualStudio2015/RippleD.vcxproj
@@ -3431,6 +3431,14 @@
True
True
+
+ True
+ True
+
+
+ True
+ True
+
@@ -3607,6 +3615,8 @@
+
+
True
True
@@ -3765,6 +3775,10 @@
+
+
+
+
True
diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters
index f1ae037027..a6b3dde06f 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj.filters
+++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters
@@ -406,6 +406,9 @@
{44780F86-42D3-2F2B-0846-5AEE2CA6D7FE}
+
+ {1D54E820-ADC9-94FB-19E7-653EFDE4CBE9}
+
{15B4B65A-0F03-7BA9-38CD-42A5712392CB}
@@ -3924,6 +3927,12 @@
ripple\test\impl
+
+ ripple\test\impl
+
+
+ ripple\test\impl
+
ripple\test
@@ -4110,6 +4119,9 @@
ripple\test\mao
+
+ ripple\test
+
ripple\unity
@@ -4245,6 +4257,12 @@
ripple\websocket
+
+ ripple\wsproto
+
+
+ ripple\wsproto
+
rocksdb2\db
diff --git a/src/ripple/test/WSClient.h b/src/ripple/test/WSClient.h
new file mode 100644
index 0000000000..1a8c69535e
--- /dev/null
+++ b/src/ripple/test/WSClient.h
@@ -0,0 +1,55 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2016 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#ifndef RIPPLE_TEST_WSCLIENT_H_INCLUDED
+#define RIPPLE_TEST_WSCLIENT_H_INCLUDED
+
+#include
+#include
+#include
+#include
+#include
+
+namespace ripple {
+namespace test {
+
+class WSClient : public AbstractClient
+{
+public:
+ /** Retrieve a message. */
+ virtual
+ boost::optional
+ getMsg(std::chrono::milliseconds const& timeout =
+ std::chrono::milliseconds{0}) = 0;
+
+ /** Retrieve a message that meets the predicate criteria. */
+ virtual
+ boost::optional
+ findMsg(std::chrono::milliseconds const& timeout,
+ std::function pred) = 0;
+};
+
+/** Returns a client operating through WebSockets/S. */
+std::unique_ptr
+makeWSClient(Config const& cfg);
+
+} // test
+} // ripple
+
+#endif
diff --git a/src/ripple/test/impl/WSClient.cpp b/src/ripple/test/impl/WSClient.cpp
new file mode 100644
index 0000000000..856d0f2cdc
--- /dev/null
+++ b/src/ripple/test/impl/WSClient.cpp
@@ -0,0 +1,313 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2016 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace ripple {
+namespace test {
+
+class WSClientImpl : public WSClient
+{
+ using error_code = boost::system::error_code;
+
+ struct msg
+ {
+ Json::Value jv;
+
+ explicit
+ msg(Json::Value&& jv_)
+ : jv(jv_)
+ {
+ }
+ };
+
+ class read_frame_op
+ {
+ struct data
+ {
+ WSClientImpl& wsc;
+ wsproto::frame_header fh;
+ boost::asio::streambuf sb;
+
+ data(WSClientImpl& wsc_)
+ : wsc(wsc_)
+ {
+ }
+
+ ~data()
+ {
+ wsc.on_read_done();
+ }
+ };
+
+ std::shared_ptr d_;
+
+ public:
+ read_frame_op(read_frame_op const&) = default;
+ read_frame_op(read_frame_op&&) = default;
+
+ explicit
+ read_frame_op(WSClientImpl& wsc)
+ : d_(std::make_shared(wsc))
+ {
+ read_one();
+ }
+
+ void read_one()
+ {
+ // hack
+ d_->sb.consume(d_->sb.size());
+ d_->wsc.ws_.async_read_fh(
+ d_->fh, std::move(*this));
+ }
+
+ void operator()(error_code const& ec)
+ {
+ if(ec)
+ return d_->wsc.on_read_frame(
+ ec, d_->fh, 0, d_->sb.data(),
+ std::move(*this));
+ d_->wsc.ws_.async_read(d_->fh,
+ d_->sb.prepare(d_->fh.len), std::move(*this));
+ }
+
+ void operator()(error_code const& ec,
+ wsproto::frame_header const& fh,
+ std::size_t bytes_transferred)
+ {
+ if(ec)
+ return d_->wsc.on_read_frame(
+ ec, d_->fh, 0, d_->sb.data(),
+ std::move(*this));
+ if(d_->fh.mask)
+ {
+ // TODO: apply key mask to payload
+ }
+ d_->sb.commit(bytes_transferred);
+ return d_->wsc.on_read_frame(
+ ec, d_->fh, bytes_transferred, d_->sb.data(),
+ std::move(*this));
+ }
+ };
+
+ static
+ boost::asio::ip::tcp::endpoint
+ getEndpoint(BasicConfig const& cfg)
+ {
+ auto& log = std::cerr;
+ ParsedPort common;
+ parse_Port (common, cfg["server"], log);
+ for (auto const& name : cfg.section("server").values())
+ {
+ if (! cfg.exists(name))
+ continue;
+ ParsedPort pp;
+ parse_Port(pp, cfg[name], log);
+ if(pp.protocol.count("ws") == 0)
+ continue;
+ using boost::asio::ip::address_v4;
+ if(*pp.ip == address_v4{0x00000000})
+ *pp.ip = address_v4{0x7f000001};
+ return { *pp.ip, *pp.port };
+ }
+ throw std::runtime_error("Missing WebSocket port");
+ }
+
+ template
+ static
+ std::string
+ buffer_string (ConstBuffers const& b)
+ {
+ using namespace boost::asio;
+ std::string s;
+ s.resize(buffer_size(b));
+ buffer_copy(buffer(&s[0], s.size()), b);
+ return s;
+ }
+
+ boost::asio::io_service ios_;
+ boost::optional<
+ boost::asio::io_service::work> work_;
+ std::thread thread_;
+ boost::asio::ip::tcp::socket stream_;
+ wsproto::basic_socket ws_;
+
+ // synchronize destructor
+ bool b0_ = false;
+ std::mutex m0_;
+ std::condition_variable cv0_;
+
+ // sychronize message queue
+ std::mutex m_;
+ std::condition_variable cv_;
+ std::list> msgs_;
+
+public:
+ explicit
+ WSClientImpl(Config const& cfg)
+ : work_(ios_)
+ , thread_([&]{ ios_.run(); })
+ , stream_(ios_)
+ , ws_(stream_)
+ {
+ using namespace boost::asio;
+ stream_.connect(getEndpoint(cfg));
+ error_code ec;
+ ws_.connect(ec);
+ if(ec)
+ throw ec;
+ read_frame_op{*this};
+ }
+
+ ~WSClientImpl() override
+ {
+ stream_.close();
+ {
+ std::unique_lock lock(m0_);
+ cv0_.wait(lock, [&]{ return b0_; });
+ }
+ work_ = boost::none;
+ thread_.join();
+
+ //stream_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
+ //stream_.close();
+ }
+
+ Json::Value
+ invoke(std::string const& cmd,
+ Json::Value const& params) override
+ {
+ using namespace boost::asio;
+ using namespace std::chrono_literals;
+
+ {
+ Json::Value jp;
+ if(params)
+ jp = params;
+ jp["command"] = cmd;
+ auto const s = to_string(jp);
+ ws_.write(buffer(s));
+ }
+
+ auto jv = findMsg(5s,
+ [&](Json::Value const& jv)
+ {
+ return jv[jss::type] == jss::response;
+ });
+ if (jv)
+ return *jv;
+ return {};
+ }
+
+ boost::optional
+ getMsg(std::chrono::milliseconds const& timeout) override
+ {
+ std::shared_ptr m;
+ {
+ std::unique_lock lock(m_);
+ if(! cv_.wait_for(lock, timeout,
+ [&]{ return ! msgs_.empty(); }))
+ return boost::none;
+ m = std::move(msgs_.back());
+ msgs_.pop_back();
+ }
+ return std::move(m->jv);
+ }
+
+ boost::optional
+ findMsg(std::chrono::milliseconds const& timeout,
+ std::function pred) override
+ {
+ std::shared_ptr m;
+ {
+ std::unique_lock lock(m_);
+ if(! cv_.wait_for(lock, timeout,
+ [&]
+ {
+ for (auto it = msgs_.begin();
+ it != msgs_.end(); ++it)
+ {
+ if (pred((*it)->jv))
+ {
+ m = std::move(*it);
+ msgs_.erase(it);
+ return true;
+ }
+ }
+ return false;
+ }))
+ {
+ return boost::none;
+ }
+ }
+ return std::move(m->jv);
+ }
+
+private:
+ template
+ void
+ on_read_frame(error_code const& ec,
+ wsproto::frame_header const& fh,
+ std::size_t bytes_transferred,
+ ConstBuffers const& b,
+ read_frame_op&& op)
+ {
+ if(bytes_transferred == 0)
+ return;
+ Json::Value jv;
+ Json::Reader jr;
+ jr.parse(buffer_string(b), jv);
+ auto m = std::make_shared(
+ std::move(jv));
+ {
+ std::lock_guard lock(m_);
+ msgs_.push_front(m);
+ cv_.notify_all();
+ }
+ op.read_one();
+ }
+
+ // Called when the read op terminates
+ void
+ on_read_done()
+ {
+ std::lock_guard lock(m_);
+ b0_ = true;
+ cv0_.notify_all();
+ }
+};
+
+std::unique_ptr
+makeWSClient(Config const& cfg)
+{
+ return std::make_unique(cfg);
+}
+
+} // test
+} // ripple
diff --git a/src/ripple/test/impl/WSClient_test.cpp b/src/ripple/test/impl/WSClient_test.cpp
new file mode 100644
index 0000000000..999165ff5e
--- /dev/null
+++ b/src/ripple/test/impl/WSClient_test.cpp
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2016 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#include
+#include
+#include
+#include
+
+namespace ripple {
+namespace test {
+
+class WSClient_test : public beast::unit_test::suite
+{
+public:
+ void run() override
+ {
+ using namespace jtx;
+ Env env(*this);
+ auto wsc = makeWSClient(env.app().config());
+ {
+ Json::Value jv;
+ jv["streams"] = Json::arrayValue;
+ jv["streams"].append("ledger");
+ //jv["streams"].append("server");
+ //jv["streams"].append("transactions");
+ //jv["streams"].append("transactions_proposed");
+ log << pretty(wsc->invoke("subscribe", jv));
+ }
+ env.fund(XRP(10000), "alice");
+ env.close();
+ /*
+ env.fund(XRP(10000), "dan", "eric", "fred");
+ env.close();
+ env.fund(XRP(10000), "george", "harold", "iris");
+ env.close();
+ */
+ auto jv = wsc->getMsg(std::chrono::seconds(1));
+ if(jv)
+ log << pretty(*jv);
+ pass();
+ }
+};
+
+BEAST_DEFINE_TESTSUITE(WSClient,test,ripple);
+
+} // test
+} // ripple
diff --git a/src/ripple/unity/test.cpp b/src/ripple/unity/test.cpp
index 4ad8fc39aa..630a51f319 100644
--- a/src/ripple/unity/test.cpp
+++ b/src/ripple/unity/test.cpp
@@ -50,3 +50,5 @@
#include
#include
#include
+#include
+#include