diff --git a/rippled-example.cfg b/rippled-example.cfg index ef45147b7c..8bd553b9cb 100644 --- a/rippled-example.cfg +++ b/rippled-example.cfg @@ -157,9 +157,10 @@ # connections. # # [websocket_public_secure] -# 0 or 1. +# 0, 1 or 2. # 0: Provide ws service for websocket_public_ip/websocket_public_port. -# 1: Provide wss service for websocket_public_ip/websocket_public_port. [default] +# 1: Provide both ws and wss service for websocket_public_ip/websocket_public_port. [default] +# 2: Provide wss service only for websocket_public_ip/websocket_public_port. # # Browser pages like the Ripple client will not be able to connect to a secure # websocket connection if a self-signed certificate is used. As the Ripple @@ -177,9 +178,10 @@ # Port to bind to allow trusted ADMIN connections from backend applications. # # [websocket_secure] -# 0 or 1. -# 0: Provide ws service for websocket_ip/websocket_port. [default] -# 1: Provide wss service for websocket_ip/websocket_port. +# 0, 1, or 2. +# 0: Provide ws service only for websocket_ip/websocket_port. [default] +# 1: Provide ws and wss service for websocket_ip/websocket_port +# 2: Provide wss service for websocket_ip/websocket_port. # # [websocket_ssl_key]: # Specify the filename holding the SSL key in PEM format. diff --git a/src/cpp/ripple/Application.cpp b/src/cpp/ripple/Application.cpp index ec034eedfc..58b75e8b48 100644 --- a/src/cpp/ripple/Application.cpp +++ b/src/cpp/ripple/Application.cpp @@ -20,6 +20,7 @@ SETUP_LOG(); LogPartition TaggedCachePartition("TaggedCache"); +LogPartition AutoSocketPartition("AutoSocket"); Application* theApp = NULL; DatabaseCon::DatabaseCon(const std::string& strName, const char *initStrings[], int initCount) diff --git a/src/cpp/ripple/AutoSocket.cpp b/src/cpp/ripple/AutoSocket.cpp deleted file mode 100644 index dd553a4c57..0000000000 --- a/src/cpp/ripple/AutoSocket.cpp +++ /dev/null @@ -1,32 +0,0 @@ - -#include "AutoSocket.h" - -#include - -void AutoSocket::handle_autodetect(const error_code& ec) -{ - if (ec) - { - if (mCallback) - mCallback(ec); - return; - } - - if ((mBuffer[0] < 127) && (mBuffer[0] > 31) && - (mBuffer[1] < 127) && (mBuffer[1] > 31) && - (mBuffer[2] < 127) && (mBuffer[2] > 31) && - (mBuffer[3] < 127) && (mBuffer[3] > 31)) - { // non-SSL - mSecure = false; - if (mCallback) - mCallback(ec); - } - else - { // ssl - mSecure = true; - SSLSocket().async_handshake(ssl_socket::server, mCallback); - mCallback = callback(); - } -} - -// vim:ts=4 diff --git a/src/cpp/ripple/AutoSocket.h b/src/cpp/ripple/AutoSocket.h index d8529ea282..d965825259 100644 --- a/src/cpp/ripple/AutoSocket.h +++ b/src/cpp/ripple/AutoSocket.h @@ -5,9 +5,14 @@ #include #include +#include +#include #include #include +#include "Log.h" +extern LogPartition AutoSocketPartition; + // Socket wrapper that supports both SSL and non-SSL connections. // Generally, handle it as you would an SSL connection. // To force a non-SSL connection, just don't call async_handshake. @@ -20,78 +25,140 @@ class AutoSocket { public: typedef bassl::stream ssl_socket; + typedef boost::shared_ptr socket_ptr; typedef ssl_socket::next_layer_type plain_socket; + typedef ssl_socket::lowest_layer_type lowest_layer_type; + typedef ssl_socket::handshake_type handshake_type; typedef boost::system::error_code error_code; - typedef boost::function callback; + typedef boost::function callback; protected: - ssl_socket mSocket; + socket_ptr mSocket; bool mSecure; - callback mCallback; std::vector mBuffer; public: - AutoSocket(basio::io_service& s, bassl::context& c) : mSocket(s, c), mSecure(false), mBuffer(4) { ; } + AutoSocket(basio::io_service& s, bassl::context& c) : mSecure(false), mBuffer(4) + { + mSocket = boost::make_shared(boost::ref(s), boost::ref(c)); + } + + AutoSocket(basio::io_service& s, bassl::context& c, bool secureOnly, bool plainOnly) + : mSecure(secureOnly), mBuffer((plainOnly || secureOnly) ? 0 : 4) + { + mSocket = boost::make_shared(boost::ref(s), boost::ref(c)); + } bool isSecure() { return mSecure; } - ssl_socket& SSLSocket() { return mSocket; } - plain_socket& PlainSocket() { return mSocket.next_layer(); } + ssl_socket& SSLSocket() { return *mSocket; } + plain_socket& PlainSocket() { return mSocket->next_layer(); } + void setSSLOnly() { mSecure = true;} + void setPlainOnly() { mBuffer.clear(); } - void setSSLOnly() { mBuffer.clear(); } + lowest_layer_type& lowest_layer() { return mSocket->lowest_layer(); } - void async_handshake(ssl_socket::handshake_type type, callback cbFunc) + void swap(AutoSocket& s) { - mSecure = true; - if ((type == ssl_socket::client) || (mBuffer.empty())) - SSLSocket().async_handshake(type, cbFunc); + mBuffer.swap(s.mBuffer); + mSocket.swap(s.mSocket); + std::swap(mSecure, s.mSecure); + } + + void async_handshake(handshake_type type, callback cbFunc) + { + if ((type == ssl_socket::client) || (mSecure)) + { // must be ssl + mSecure = true; + mSocket->async_handshake(type, cbFunc); + } + else if (mBuffer.empty()) + { // must be plain + mSecure = false; + mSocket->get_io_service().post(boost::bind(cbFunc, error_code())); + } else - { - mCallback = cbFunc; - PlainSocket().async_receive(basio::buffer(mBuffer), basio::socket_base::message_peek, - boost::bind(&AutoSocket::handle_autodetect, this, basio::placeholders::error)); - + { // autodetect + mSocket->next_layer().async_receive(basio::buffer(mBuffer), basio::socket_base::message_peek, + boost::bind(&AutoSocket::handle_autodetect, this, cbFunc, basio::placeholders::error)); } } - template StreamType& getSocket() - { - if (isSecure()) - return SSLSocket(); - if (!isSecure()) - return PlainSocket(); - } - template void async_shutdown(ShutdownHandler handler) { if (isSecure()) - SSLSocket().async_shutdown(handler); + mSocket->async_shutdown(handler); else { - PlainSocket().shutdown(plain_socket::shutdown_both); - if (handler) - mSocket.get_io_service().post(handler); + lowest_layer().shutdown(plain_socket::shutdown_both); + mSocket->get_io_service().post(boost::bind(handler, error_code())); } } template void async_read_some(const Seq& buffers, Handler handler) { if (isSecure()) - SSLSocket().async_read_some(buffers, handler); + mSocket->async_read_some(buffers, handler); else PlainSocket().async_read_some(buffers, handler); } + template void async_write(const Buf& buffers, Handler handler) + { + if (isSecure()) + boost::asio::async_write(*mSocket, buffers, handler); + else + boost::asio::async_write(PlainSocket(), buffers, handler); + } + + + template + void async_read(const Buf& buffers, Condition cond, Handler handler) + { + if (isSecure()) + boost::asio::async_read(*mSocket, buffers, cond, handler); + else + boost::asio::async_read(PlainSocket(), buffers, cond, handler); + } + + template void async_read(const Buf& buffers, Handler handler) + { + if (isSecure()) + boost::asio::async_read(*mSocket, buffers, handler); + else + boost::asio::async_read(PlainSocket(), buffers, handler); + } + template void async_write_some(const Seq& buffers, Handler handler) { if (isSecure()) - SSLSocket().async_write_some(buffers, handler); + mSocket->async_write_some(buffers, handler); else PlainSocket().async_write_some(buffers, handler); } protected: - void handle_autodetect(const error_code&); + void handle_autodetect(callback cbFunc, const error_code& ec) + { + if (ec) + { + Log(lsWARNING, AutoSocketPartition) << "Handle autodetect error: " << ec; + cbFunc(ec); + } + else if ((mBuffer[0] < 127) && (mBuffer[0] > 31) && + (mBuffer[1] < 127) && (mBuffer[1] > 31) && + (mBuffer[2] < 127) && (mBuffer[2] > 31) && + (mBuffer[3] < 127) && (mBuffer[3] > 31)) + { // not ssl + mSecure = false; + cbFunc(ec); + } + else + { // ssl + mSecure = true; + mSocket->async_handshake(ssl_socket::server, cbFunc); + } + } }; #endif diff --git a/src/cpp/ripple/Config.cpp b/src/cpp/ripple/Config.cpp index 1eee97fba8..d45d35d65a 100644 --- a/src/cpp/ripple/Config.cpp +++ b/src/cpp/ripple/Config.cpp @@ -178,8 +178,8 @@ Config::Config() RPC_PORT = 5001; WEBSOCKET_PORT = SYSTEM_WEBSOCKET_PORT; WEBSOCKET_PUBLIC_PORT = SYSTEM_WEBSOCKET_PUBLIC_PORT; - WEBSOCKET_PUBLIC_SECURE = true; - WEBSOCKET_SECURE = false; + WEBSOCKET_PUBLIC_SECURE = 1; + WEBSOCKET_SECURE = 0; NUMBER_CONNECTIONS = 30; // a new ledger every minute @@ -340,10 +340,10 @@ void Config::load() WEBSOCKET_PUBLIC_PORT = boost::lexical_cast(strTemp); if (sectionSingleB(secConfig, SECTION_WEBSOCKET_SECURE, strTemp)) - WEBSOCKET_SECURE = boost::lexical_cast(strTemp); + WEBSOCKET_SECURE = boost::lexical_cast(strTemp); if (sectionSingleB(secConfig, SECTION_WEBSOCKET_PUBLIC_SECURE, strTemp)) - WEBSOCKET_PUBLIC_SECURE = boost::lexical_cast(strTemp); + WEBSOCKET_PUBLIC_SECURE = boost::lexical_cast(strTemp); sectionSingleB(secConfig, SECTION_WEBSOCKET_SSL_CERT, WEBSOCKET_SSL_CERT); sectionSingleB(secConfig, SECTION_WEBSOCKET_SSL_CHAIN, WEBSOCKET_SSL_CHAIN); diff --git a/src/cpp/ripple/Config.h b/src/cpp/ripple/Config.h index abb1c56628..6fe20fcc46 100644 --- a/src/cpp/ripple/Config.h +++ b/src/cpp/ripple/Config.h @@ -101,11 +101,12 @@ public: // Websocket networking parameters std::string WEBSOCKET_PUBLIC_IP; // XXX Going away. Merge with the inbound peer connction. int WEBSOCKET_PUBLIC_PORT; - bool WEBSOCKET_PUBLIC_SECURE; + int WEBSOCKET_PUBLIC_SECURE; std::string WEBSOCKET_IP; int WEBSOCKET_PORT; - bool WEBSOCKET_SECURE; + int WEBSOCKET_SECURE; + std::string WEBSOCKET_SSL_CERT; std::string WEBSOCKET_SSL_CHAIN; std::string WEBSOCKET_SSL_KEY; diff --git a/src/cpp/ripple/SerializedTransaction.cpp b/src/cpp/ripple/SerializedTransaction.cpp index e7b1ef7e59..5961d25f7d 100644 --- a/src/cpp/ripple/SerializedTransaction.cpp +++ b/src/cpp/ripple/SerializedTransaction.cpp @@ -100,9 +100,10 @@ std::vector SerializedTransaction::getMentionedAccounts() const if (!found) accounts.push_back(na); } - if (it.getFName() == sfLimitAmount) + const STAmount* sam = dynamic_cast(&it); + if (sam) { - uint160 issuer = dynamic_cast(&it)->getIssuer(); + uint160 issuer = sam->getIssuer(); if (issuer.isNonZero()) { RippleAddress na; diff --git a/src/cpp/ripple/TransactionEngine.cpp b/src/cpp/ripple/TransactionEngine.cpp index 4ab5cad747..4a31227ccb 100644 --- a/src/cpp/ripple/TransactionEngine.cpp +++ b/src/cpp/ripple/TransactionEngine.cpp @@ -127,28 +127,23 @@ TER TransactionEngine::applyTransaction(const SerializedTransaction& txn, Transa uint32 t_seq = txn.getSequence(); uint32 a_seq = txnAcct->getFieldU32(sfSequence); - if (t_seq != a_seq) - { - if (a_seq < t_seq) - terResult = terPRE_SEQ; - else - terResult = tefPAST_SEQ; - } + if (a_seq < t_seq) + terResult = terPRE_SEQ; + else if (a_seq > t_seq) + terResult = tefPAST_SEQ; else { STAmount fee = txn.getTransactionFee(); STAmount balance = txnAcct->getFieldAmount(sfBalance); if (balance < fee) - { terResult = terINSUF_FEE_B; - } else { txnAcct->setFieldAmount(sfBalance, balance - fee); txnAcct->setFieldU32(sfSequence, t_seq + 1); - didApply = true; entryModify(txnAcct); + didApply = true; } } } diff --git a/src/cpp/ripple/TransactionMeta.cpp b/src/cpp/ripple/TransactionMeta.cpp index 1e69ec6aea..bd87829a72 100644 --- a/src/cpp/ripple/TransactionMeta.cpp +++ b/src/cpp/ripple/TransactionMeta.cpp @@ -81,7 +81,8 @@ std::vector TransactionMetaSet::getAffectedAccounts() const STAccount* sa = dynamic_cast(&field); if (sa) addIfUnique(accounts, sa->getValueNCA()); - else if ((field.getFName() == sfLowLimit) || (field.getFName() == sfHighLimit)) + else if ((field.getFName() == sfLowLimit) || (field.getFName() == sfHighLimit) || + (field.getFName() == sfTakerPays) || (field.getFName() == sfTakerGets)) { const STAmount* lim = dynamic_cast(&field); if (lim != NULL) diff --git a/src/cpp/ripple/WSConnection.h b/src/cpp/ripple/WSConnection.h index ca088225ec..d6fe475f49 100644 --- a/src/cpp/ripple/WSConnection.h +++ b/src/cpp/ripple/WSConnection.h @@ -1,5 +1,5 @@ -#include "../websocketpp/src/sockets/tls.hpp" +#include "../websocketpp/src/sockets/autotls.hpp" #include "../websocketpp/src/websocketpp.hpp" #include "../json/value.h" diff --git a/src/cpp/ripple/WSDoor.cpp b/src/cpp/ripple/WSDoor.cpp index 961ec3f3ce..ec4a84e89d 100644 --- a/src/cpp/ripple/WSDoor.cpp +++ b/src/cpp/ripple/WSDoor.cpp @@ -2,7 +2,7 @@ #include "Log.h" #define WSDOOR_CPP -#include "../websocketpp/src/sockets/tls.hpp" +#include "../websocketpp/src/sockets/autotls.hpp" #include "../websocketpp/src/websocketpp.hpp" SETUP_LOG(); @@ -59,80 +59,40 @@ void WSDoor::startListening() SSL_CTX_set_tmp_dh_callback(mCtx->native_handle(), handleTmpDh); - if (mPublic ? theConfig.WEBSOCKET_PUBLIC_SECURE : theConfig.WEBSOCKET_SECURE) + // Construct a single handler for all requests. + websocketpp::server_autotls::handler::ptr handler(new WSServerHandler(mCtx, mPublic)); + + // Construct a websocket server. + mSEndpoint = new websocketpp::server_autotls(handler); + + // mEndpoint->alog().unset_level(websocketpp::log::alevel::ALL); + // mEndpoint->elog().unset_level(websocketpp::log::elevel::ALL); + + // Call the main-event-loop of the websocket server. + try { - // Construct a single handler for all requests. - websocketpp::server_tls::handler::ptr handler(new WSServerHandler(mCtx, mPublic)); - - // Construct a websocket server. - mSEndpoint = new websocketpp::server_tls(handler); - - // mEndpoint->alog().unset_level(websocketpp::log::alevel::ALL); - // mEndpoint->elog().unset_level(websocketpp::log::elevel::ALL); - - // Call the main-event-loop of the websocket server. - try - { - mSEndpoint->listen( - boost::asio::ip::tcp::endpoint( - boost::asio::ip::address().from_string(mIp), mPort)); - } - catch (websocketpp::exception& e) - { - cLog(lsWARNING) << "websocketpp exception: " << e.what(); - while (1) // temporary workaround for websocketpp throwing exceptions on access/close races - { // https://github.com/zaphoyd/websocketpp/issues/98 - try - { - mSEndpoint->get_io_service().run(); - break; - } - catch (websocketpp::exception& e) - { - cLog(lsWARNING) << "websocketpp exception: " << e.what(); - } + mSEndpoint->listen( + boost::asio::ip::tcp::endpoint( + boost::asio::ip::address().from_string(mIp), mPort)); + } + catch (websocketpp::exception& e) + { + cLog(lsWARNING) << "websocketpp exception: " << e.what(); + while (1) // temporary workaround for websocketpp throwing exceptions on access/close races + { // https://github.com/zaphoyd/websocketpp/issues/98 + try + { + mSEndpoint->get_io_service().run(); + break; + } + catch (websocketpp::exception& e) + { + cLog(lsWARNING) << "websocketpp exception: " << e.what(); } } - - delete mSEndpoint; } - else - { - // Construct a single handler for all requests. - websocketpp::server::handler::ptr handler(new WSServerHandler(mCtx, mPublic)); - // Construct a websocket server. - mEndpoint = new websocketpp::server(handler); - - // mEndpoint->alog().unset_level(websocketpp::log::alevel::ALL); - // mEndpoint->elog().unset_level(websocketpp::log::elevel::ALL); - - // Call the main-event-loop of the websocket server. - try - { - mEndpoint->listen( - boost::asio::ip::tcp::endpoint( - boost::asio::ip::address().from_string(mIp), mPort)); - } - catch (websocketpp::exception& e) - { - cLog(lsWARNING) << "websocketpp exception: " << e.what(); - while (1) // temporary workaround for websocketpp throwing exceptions on access/close races - { // https://github.com/zaphoyd/websocketpp/issues/98 - try - { - mEndpoint->get_io_service().run(); - break; - } - catch (websocketpp::exception& e) - { - cLog(lsWARNING) << "websocketpp exception: " << e.what(); - } - } - } - - delete mEndpoint; - } + delete mSEndpoint; } WSDoor* WSDoor::createWSDoor(const std::string& strIp, const int iPort, bool bPublic) @@ -154,8 +114,6 @@ void WSDoor::stop() { if (mThread) { - if (mEndpoint) - mEndpoint->stop(); if (mSEndpoint) mSEndpoint->stop(); diff --git a/src/cpp/ripple/WSDoor.h b/src/cpp/ripple/WSDoor.h index 24ba138a89..bdac87ea70 100644 --- a/src/cpp/ripple/WSDoor.h +++ b/src/cpp/ripple/WSDoor.h @@ -12,7 +12,7 @@ namespace websocketpp { class server; - class server_tls; + class server_autotls; } #endif @@ -20,19 +20,18 @@ namespace websocketpp class WSDoor { private: - websocketpp::server* mEndpoint; - websocketpp::server_tls* mSEndpoint; + websocketpp::server_autotls* mSEndpoint; - boost::thread* mThread; - bool mPublic; - std::string mIp; - int mPort; + boost::thread* mThread; + bool mPublic; + std::string mIp; + int mPort; void startListening(); public: - WSDoor(const std::string& strIp, int iPort, bool bPublic) : mEndpoint(0), mSEndpoint(0), mThread(0), mPublic(bPublic), mIp(strIp), mPort(iPort) { ; } + WSDoor(const std::string& strIp, int iPort, bool bPublic) : mSEndpoint(0), mThread(0), mPublic(bPublic), mIp(strIp), mPort(iPort) { ; } void stop(); diff --git a/src/cpp/ripple/WSHandler.h b/src/cpp/ripple/WSHandler.h index 793ca98b8c..4324e7bfc8 100644 --- a/src/cpp/ripple/WSHandler.h +++ b/src/cpp/ripple/WSHandler.h @@ -37,7 +37,7 @@ protected: public: WSServerHandler(boost::shared_ptr spCtx, bool bPublic) : mCtx(spCtx), mPublic(bPublic) { - if (theConfig.WEBSOCKET_SECURE) + if (theConfig.WEBSOCKET_SECURE != 0) { initSSLContext(*mCtx, theConfig.WEBSOCKET_SSL_KEY, theConfig.WEBSOCKET_SSL_CERT, theConfig.WEBSOCKET_SSL_CHAIN); diff --git a/src/cpp/websocketpp/src/connection.hpp b/src/cpp/websocketpp/src/connection.hpp index 53f37a559e..9883148ac6 100644 --- a/src/cpp/websocketpp/src/connection.hpp +++ b/src/cpp/websocketpp/src/connection.hpp @@ -898,20 +898,39 @@ public: !m_protocol_error) { // TODO: read timeout timer? - - boost::asio::async_read( - socket_type::get_socket(), - m_buf, - boost::asio::transfer_at_least(std::min( - m_read_threshold, - static_cast(m_processor->get_bytes_needed()) - )), - m_strand.wrap(boost::bind( - &type::handle_read_frame, - type::shared_from_this(), - boost::asio::placeholders::error - )) - ); + + if (socket_type::get_socket().isSecure()) + { + boost::asio::async_read( + socket_type::get_socket().SSLSocket(), + m_buf, + boost::asio::transfer_at_least(std::min( + m_read_threshold, + static_cast(m_processor->get_bytes_needed()) + )), + m_strand.wrap(boost::bind( + &type::handle_read_frame, + type::shared_from_this(), + boost::asio::placeholders::error + )) + ); + } + else + { + boost::asio::async_read( + socket_type::get_socket().PlainSocket(), + m_buf, + boost::asio::transfer_at_least(std::min( + m_read_threshold, + static_cast(m_processor->get_bytes_needed()) + )), + m_strand.wrap(boost::bind( + &type::handle_read_frame, + type::shared_from_this(), + boost::asio::placeholders::error + )) + ); + } } } public: @@ -1209,15 +1228,31 @@ public: //m_endpoint.alog().at(log::alevel::DEVEL) << "write header: " << zsutil::to_hex(m_write_queue.front()->get_header()) << log::endl; - boost::asio::async_write( - socket_type::get_socket(), - m_write_buf, - m_strand.wrap(boost::bind( - &type::handle_write, - type::shared_from_this(), - boost::asio::placeholders::error - )) - ); + if (socket_type::get_socket().isSecure()) + { + boost::asio::async_write( + socket_type::get_socket().SSLSocket(), + m_write_buf, + m_strand.wrap(boost::bind( + &type::handle_write, + type::shared_from_this(), + boost::asio::placeholders::error + )) + ); + } + else + { + boost::asio::async_write( + socket_type::get_socket().PlainSocket(), + m_write_buf, + m_strand.wrap(boost::bind( + &type::handle_write, + type::shared_from_this(), + boost::asio::placeholders::error + )) + ); + } + } else { // if we are in an inturrupted state and had nothing else to write // it is safe to terminate the connection. diff --git a/src/cpp/websocketpp/src/endpoint.hpp b/src/cpp/websocketpp/src/endpoint.hpp index f9ac5a582b..862243c1c2 100644 --- a/src/cpp/websocketpp/src/endpoint.hpp +++ b/src/cpp/websocketpp/src/endpoint.hpp @@ -29,7 +29,7 @@ #define WEBSOCKETPP_ENDPOINT_HPP #include "connection.hpp" -#include "sockets/plain.hpp" // should this be here? +#include "sockets/autotls.hpp" // should this be here? #include "logger/logger.hpp" #include @@ -74,7 +74,7 @@ protected: */ template < template class role, - template class socket = socket::plain, + template class socket = socket::autotls, template class logger = log::logger> class endpoint : public endpoint_base, diff --git a/src/cpp/websocketpp/src/roles/server.hpp b/src/cpp/websocketpp/src/roles/server.hpp index 754f4f6b67..0e3de9db85 100644 --- a/src/cpp/websocketpp/src/roles/server.hpp +++ b/src/cpp/websocketpp/src/roles/server.hpp @@ -57,26 +57,30 @@ typedef boost::asio::buffers_iterator match_header(bufIterator begin, bufIterator end) { + static const std::string eol_match = "\n"; + static const std::string header_match = "\n\r\n"; + static const std::string alt_header_match = "\n\n"; + static const std::string flash_match = ""; + // Do we have a complete HTTP request - const std::string header_match = "\r\n\r\n"; bufIterator it = std::search(begin, end, header_match.begin(), header_match.end()); if (it != end) - return std::make_pair(it, true); + return std::make_pair(it + header_match.size(), true); + it = std::search(begin, end, alt_header_match.begin(), alt_header_match.end()); + return std::make_pair(it + alt_header_match.size(), true); // If we don't have a flash policy request, we're done - const std::string flash_match = ""; it = std::search(begin, end, flash_match.begin(), flash_match.end()); - if (it == end) + if (it == end) // No match return std::make_pair(end, false); // If we have a line ending before the flash policy request, treat as http - const std::string eol_match = "\r\n"; bufIterator it2 = std::search(begin, end, eol_match.begin(), eol_match.end()); if ((it2 != end) || (it < it2)) return std::make_pair(end, false); // Treat as flash policy request - return std::make_pair(it, true); + return std::make_pair(it + flash_match.size(), true); } // Forward declarations @@ -539,18 +543,37 @@ void server::connection::async_init() { // TODO: make this value configurable m_connection.register_timeout(5000,fail::status::TIMEOUT_WS, "Timeout on WebSocket handshake"); - - boost::asio::async_read_until( - m_connection.get_socket(), - m_connection.buffer(), - match_header, - m_connection.get_strand().wrap(boost::bind( - &type::handle_read_request, - m_connection.shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred - )) - ); + + if (m_connection.get_socket().isSecure()) + { + boost::asio::async_read_until( + m_connection.get_socket().SSLSocket(), + m_connection.buffer(), +// match_header, + "\r\n\r\n", + m_connection.get_strand().wrap(boost::bind( + &type::handle_read_request, + m_connection.shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred + )) + ); + } + else + { + boost::asio::async_read_until( + m_connection.get_socket().PlainSocket(), + m_connection.buffer(), +// match_header, + "\r\n\r\n", + m_connection.get_strand().wrap(boost::bind( + &type::handle_read_request, + m_connection.shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred + )) + ); + } } /// processes the response from an async read for an HTTP header @@ -699,9 +722,9 @@ void server::connection::handle_read_request( { // TODO: this makes the assumption that WS and HTTP // default ports are the same. - m_uri.reset(new uri(m_endpoint.is_secure(),h,m_request.uri())); + m_uri.reset(new uri(m_connection.is_secure(),h,m_request.uri())); } else { - m_uri.reset(new uri(m_endpoint.is_secure(), + m_uri.reset(new uri(m_connection.is_secure(), h.substr(0,last_colon), h.substr(last_colon+1), m_request.uri())); @@ -821,17 +844,16 @@ void server::connection::write_response() { shared_const_buffer buffer(raw); m_endpoint.m_alog->at(log::alevel::DEBUG_HANDSHAKE) << raw << log::endl; - - boost::asio::async_write( - m_connection.get_socket(), - //boost::asio::buffer(raw), - buffer, - boost::bind( - &type::handle_write_response, - m_connection.shared_from_this(), - boost::asio::placeholders::error - ) - ); + + m_connection.get_socket().async_write( + //boost::asio::buffer(raw), + buffer, + boost::bind( + &type::handle_write_response, + m_connection.shared_from_this(), + boost::asio::placeholders::error + ) + ); } template diff --git a/src/cpp/websocketpp/src/sockets/plain.hpp b/src/cpp/websocketpp/src/sockets/plain.hpp index 48aa910799..c7ecaa96b1 100644 --- a/src/cpp/websocketpp/src/sockets/plain.hpp +++ b/src/cpp/websocketpp/src/sockets/plain.hpp @@ -25,6 +25,8 @@ * */ +#error Use Auto TLS only + #ifndef WEBSOCKETPP_SOCKET_PLAIN_HPP #define WEBSOCKETPP_SOCKET_PLAIN_HPP diff --git a/src/cpp/websocketpp/src/sockets/tls.hpp b/src/cpp/websocketpp/src/sockets/tls.hpp index 156901a4ec..34e7af8caa 100644 --- a/src/cpp/websocketpp/src/sockets/tls.hpp +++ b/src/cpp/websocketpp/src/sockets/tls.hpp @@ -25,6 +25,8 @@ * */ +#error Use auto TLS only + #ifndef WEBSOCKETPP_SOCKET_TLS_HPP #define WEBSOCKETPP_SOCKET_TLS_HPP diff --git a/src/cpp/websocketpp/src/websocketpp.hpp b/src/cpp/websocketpp/src/websocketpp.hpp index 221d66e6f2..c6692a9ba3 100644 --- a/src/cpp/websocketpp/src/websocketpp.hpp +++ b/src/cpp/websocketpp/src/websocketpp.hpp @@ -41,6 +41,10 @@ namespace websocketpp { typedef websocketpp::endpoint server_tls; #endif + #ifdef WEBSOCKETPP_SOCKET_AUTOTLS_HPP + typedef websocketpp::endpoint server_autotls; + #endif #endif diff --git a/src/js/remote.js b/src/js/remote.js index f9de6ed51e..c1e45e03cc 100644 --- a/src/js/remote.js +++ b/src/js/remote.js @@ -16,9 +16,10 @@ // npm var EventEmitter = require('events').EventEmitter; -var Amount = require('./amount.js').Amount; -var Currency = require('./amount.js').Currency; -var UInt160 = require('./amount.js').UInt160; +var Amount = require('./amount').Amount; +var Currency = require('./amount').Currency; +var UInt160 = require('./amount').UInt160; +var Transaction = require('./transaction').Transaction; var utils = require('./utils'); @@ -281,25 +282,6 @@ Remote.online_states = [ 'full' ]; -Remote.flags = { - 'OfferCreate' : { - 'Passive' : 0x00010000, - }, - - 'Payment' : { - 'NoRippleDirect' : 0x00010000, - 'PartialPayment' : 0x00020000, - 'LimitQuality' : 0x00040000, - }, -}; - -// XXX This needs to be determined from the network. -Remote.fees = { - 'default' : Amount.from_json("10"), - 'nickname_create' : Amount.from_json("1000"), - 'offer' : Amount.from_json("10"), -}; - // Inform remote that the remote server is not comming back. Remote.prototype.server_fatal = function () { this._server_fatal = true; @@ -1173,501 +1155,6 @@ Remote.prototype.transaction = function () { return new Transaction(this); }; -// -// Transactions -// -// Construction: -// remote.transaction() // Build a transaction object. -// .offer_create(...) // Set major parameters. -// .set_flags() // Set optional parameters. -// .on() // Register for events. -// .submit(); // Send to network. -// -// Events: -// 'success' : Transaction submitted without error. -// 'error' : Error submitting transaction. -// 'proposed' : Advisory proposed status transaction. -// - A client should expect 0 to multiple results. -// - Might not get back. The remote might just forward the transaction. -// - A success could be reverted in final. -// - local error: other remotes might like it. -// - malformed error: local server thought it was malformed. -// - The client should only trust this when talking to a trusted server. -// 'final' : Final status of transaction. -// - Only expect a final from dishonest servers after a tesSUCCESS or ter*. -// 'lost' : Gave up looking for on ledger_closed. -// 'pending' : Transaction was not found on ledger_closed. -// 'state' : Follow the state of a transaction. -// 'client_submitted' - Sent to remote -// |- 'remoteError' - Remote rejected transaction. -// \- 'client_proposed' - Remote provisionally accepted transaction. -// |- 'client_missing' - Transaction has not appeared in ledger as expected. -// | |\- 'client_lost' - No longer monitoring missing transaction. -// |/ -// |- 'tesSUCCESS' - Transaction in ledger as expected. -// |- 'ter...' - Transaction failed. -// \- 'tec...' - Transaction claimed fee only. -// -// Notes: -// - All transactions including those with local and malformed errors may be -// forwarded anyway. -// - A malicous server can: -// - give any proposed result. -// - it may declare something correct as incorrect or something correct as incorrect. -// - it may not communicate with the rest of the network. -// - may or may not forward. -// - -var SUBMIT_MISSING = 4; // Report missing. -var SUBMIT_LOST = 8; // Give up tracking. - -// A class to implement transactions. -// - Collects parameters -// - Allow event listeners to be attached to determine the outcome. -var Transaction = function (remote) { - // YYY Make private as many variables as possible. - var self = this; - - this.callback = undefined; - this.remote = remote; - this._secret = undefined; - this._build_path = false; - this.tx_json = { // Transaction data. - 'Flags' : 0, // XXX Would be nice if server did not require this. - }; - this.hash = undefined; - this.submit_index = undefined; // ledger_current_index was this when transaction was submited. - this.state = undefined; // Under construction. - - this.on('success', function (message) { - if (message.engine_result) { - self.hash = message.tx_json.hash; - - self.set_state('client_proposed'); - - self.emit('proposed', { - 'tx_json' : message.tx_json, - 'result' : message.engine_result, - 'result_code' : message.engine_result_code, - 'result_message' : message.engine_result_message, - 'rejected' : self.isRejected(message.engine_result_code), // If server is honest, don't expect a final if rejected. - }); - } - }); - - this.on('error', function (message) { - // Might want to give more detailed information. - self.set_state('remoteError'); - }); -}; - -Transaction.prototype = new EventEmitter; - -Transaction.prototype.consts = { - 'telLOCAL_ERROR' : -399, - 'temMALFORMED' : -299, - 'tefFAILURE' : -199, - 'terRETRY' : -99, - 'tesSUCCESS' : 0, - 'tecCLAIMED' : 100, -}; - -Transaction.prototype.isTelLocal = function (ter) { - return ter >= this.consts.telLOCAL_ERROR && ter < this.consts.temMALFORMED; -}; - -Transaction.prototype.isTemMalformed = function (ter) { - return ter >= this.consts.temMALFORMED && ter < this.consts.tefFAILURE; -}; - -Transaction.prototype.isTefFailure = function (ter) { - return ter >= this.consts.tefFAILURE && ter < this.consts.terRETRY; -}; - -Transaction.prototype.isTerRetry = function (ter) { - return ter >= this.consts.terRETRY && ter < this.consts.tesSUCCESS; -}; - -Transaction.prototype.isTepSuccess = function (ter) { - return ter >= this.consts.tesSUCCESS; -}; - -Transaction.prototype.isTecClaimed = function (ter) { - return ter >= this.consts.tecCLAIMED; -}; - -Transaction.prototype.isRejected = function (ter) { - return this.isTelLocal(ter) || this.isTemMalformed(ter) || this.isTefFailure(ter); -}; - -Transaction.prototype.set_state = function (state) { - if (this.state !== state) { - this.state = state; - this.emit('state', state); - } -}; - -// Submit a transaction to the network. -// XXX Don't allow a submit without knowing ledger_index. -// XXX Have a network canSubmit(), post events for following. -// XXX Also give broader status for tracking through network disconnects. -// callback = function (status, info) { -// // status is final status. Only works under a ledger_accepting conditions. -// switch status: -// case 'tesSUCCESS': all is well. -// case 'tejServerUntrusted': sending secret to untrusted server. -// case 'tejInvalidAccount': locally detected error. -// case 'tejLost': locally gave up looking -// default: some other TER -// } -Transaction.prototype.submit = function (callback) { - var self = this; - var tx_json = this.tx_json; - - this.callback = callback; - - if ('string' !== typeof tx_json.Account) - { - (this.callback || this.emit)('error', { - 'error' : 'tejInvalidAccount', - 'error_message' : 'Bad account.' - }); - return; - } - - // YYY Might check paths for invalid accounts. - - if (this.remote.local_fee && undefined === tx_json.Fee) { - tx_json.Fee = Remote.fees['default'].to_json(); - } - - if (this.callback || this.listeners('final').length || this.listeners('lost').length || this.listeners('pending').length) { - // There are listeners for callback, 'final', 'lost', or 'pending' arrange to emit them. - - this.submit_index = this.remote._ledger_current_index; - - // When a ledger closes, look for the result. - var on_ledger_closed = function (message) { - var ledger_hash = message.ledger_hash; - var ledger_index = message.ledger_index; - var stop = false; - -// XXX make sure self.hash is available. - self.remote.request_transaction_entry(self.hash) - .ledger_hash(ledger_hash) - .on('success', function (message) { - self.set_state(message.metadata.TransactionResult); - self.emit('final', message); - - if (self.callback) - self.callback(message.metadata.TransactionResult, message); - - stop = true; - }) - .on('error', function (message) { - if ('remoteError' === message.error - && 'transactionNotFound' === message.remote.error) { - if (self.submit_index + SUBMIT_LOST < ledger_index) { - self.set_state('client_lost'); // Gave up. - self.emit('lost'); - - if (self.callback) - self.callback('tejLost', message); - - stop = true; - } - else if (self.submit_index + SUBMIT_MISSING < ledger_index) { - self.set_state('client_missing'); // We don't know what happened to transaction, still might find. - self.emit('pending'); - } - else { - self.emit('pending'); - } - } - // XXX Could log other unexpectedness. - }) - .request(); - - if (stop) { - self.remote.removeListener('ledger_closed', on_ledger_closed); - self.emit('final', message); - } - }; - - this.remote.on('ledger_closed', on_ledger_closed); - - if (this.callback) { - this.on('error', function (message) { - self.callback(message.error, message); - }); - } - } - - this.set_state('client_submitted'); - - this.remote.submit(this); - - return this; -} - -// -// Set options for Transactions -// - -// --> build: true, to have server blindly construct a path. -// -// "blindly" because the sender has no idea of the actual cost except that is must be less than send max. -Transaction.prototype.build_path = function (build) { - this._build_path = build; - - return this; -} - -// tag should be undefined or a 32 bit integer. -// YYY Add range checking for tag. -Transaction.prototype.destination_tag = function (tag) { - if (undefined !== tag) - this.tx_json.DestinationTag = tag; - - return this; -} - -Transaction._path_rewrite = function (path) { - var path_new = []; - - for (var index in path) { - var node = path[index]; - var node_new = {}; - - if ('account' in node) - node_new.account = UInt160.json_rewrite(node.account); - - if ('issuer' in node) - node_new.issuer = UInt160.json_rewrite(node.issuer); - - if ('currency' in node) - node_new.currency = Currency.json_rewrite(node.currency); - - path_new.push(node_new); - } - - return path_new; -} - -Transaction.prototype.path_add = function (path) { - this.tx_json.Paths = this.tx_json.Paths || [] - this.tx_json.Paths.push(Transaction._path_rewrite(path)); - - return this; -} - -// --> paths: undefined or array of path -// A path is an array of objects containing some combination of: account, currency, issuer -Transaction.prototype.paths = function (paths) { - for (var index in paths) { - this.path_add(paths[index]); - } - - return this; -} - -// If the secret is in the config object, it does not need to be provided. -Transaction.prototype.secret = function (secret) { - this._secret = secret; -} - -Transaction.prototype.send_max = function (send_max) { - if (send_max) - this.tx_json.SendMax = Amount.json_rewrite(send_max); - - return this; -} - -// tag should be undefined or a 32 bit integer. -// YYY Add range checking for tag. -Transaction.prototype.source_tag = function (tag) { - if (undefined !== tag) - this.tx_json.SourceTag = tag; - - return this; -} - -// --> rate: In billionths. -Transaction.prototype.transfer_rate = function (rate) { - this.tx_json.TransferRate = Number(rate); - - if (this.tx_json.TransferRate < 1e9) - throw 'invalidTransferRate'; - - return this; -} - -// Add flags to a transaction. -// --> flags: undefined, _flag_, or [ _flags_ ] -Transaction.prototype.set_flags = function (flags) { - if (flags) { - var transaction_flags = Remote.flags[this.tx_json.TransactionType]; - - if (undefined == this.tx_json.Flags) // We plan to not define this field on new Transaction. - this.tx_json.Flags = 0; - - var flag_set = 'object' === typeof flags ? flags : [ flags ]; - - for (index in flag_set) { - var flag = flag_set[index]; - - if (flag in transaction_flags) - { - this.tx_json.Flags += transaction_flags[flag]; - } - else { - // XXX Immediately report an error or mark it. - } - } - } - - return this; -} - -// -// Transactions -// - -Transaction.prototype._account_secret = function (account) { - // Fill in secret from remote, if available. - return this.remote.secrets[account]; -}; - -// Options: -// .domain() NYI -// .message_key() NYI -// .transfer_rate() -// .wallet_locator() NYI -// .wallet_size() NYI -Transaction.prototype.account_set = function (src) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'AccountSet'; - this.tx_json.Account = UInt160.json_rewrite(src); - - return this; -}; - -Transaction.prototype.claim = function (src, generator, public_key, signature) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'Claim'; - this.tx_json.Generator = generator; - this.tx_json.PublicKey = public_key; - this.tx_json.Signature = signature; - - return this; -}; - -Transaction.prototype.offer_cancel = function (src, sequence) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'OfferCancel'; - this.tx_json.Account = UInt160.json_rewrite(src); - this.tx_json.OfferSequence = Number(sequence); - - return this; -}; - -// --> expiration : Date or Number -Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expiration) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'OfferCreate'; - this.tx_json.Account = UInt160.json_rewrite(src); - this.tx_json.TakerPays = Amount.json_rewrite(taker_pays); - this.tx_json.TakerGets = Amount.json_rewrite(taker_gets); - - if (this.remote.local_fee) { - this.tx_json.Fee = Remote.fees.offer.to_json(); - } - - if (expiration) - this.tx_json.Expiration = Date === expiration.constructor - ? expiration.getTime() - : Number(expiration); - - return this; -}; - -Transaction.prototype.password_fund = function (src, dst) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'PasswordFund'; - this.tx_json.Destination = UInt160.json_rewrite(dst); - - return this; -} - -Transaction.prototype.password_set = function (src, authorized_key, generator, public_key, signature) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'PasswordSet'; - this.tx_json.RegularKey = authorized_key; - this.tx_json.Generator = generator; - this.tx_json.PublicKey = public_key; - this.tx_json.Signature = signature; - - return this; -} - -// Construct a 'payment' transaction. -// -// When a transaction is submitted: -// - If the connection is reliable and the server is not merely forwarding and is not malicious, -// --> src : UInt160 or String -// --> dst : UInt160 or String -// --> deliver_amount : Amount or String. -// -// Options: -// .paths() -// .build_path() -// .destination_tag() -// .path_add() -// .secret() -// .send_max() -// .set_flags() -// .source_tag() -Transaction.prototype.payment = function (src, dst, deliver_amount) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'Payment'; - this.tx_json.Account = UInt160.json_rewrite(src); - this.tx_json.Amount = Amount.json_rewrite(deliver_amount); - this.tx_json.Destination = UInt160.json_rewrite(dst); - - return this; -} - -Transaction.prototype.ripple_line_set = function (src, limit, quality_in, quality_out) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'TrustSet'; - this.tx_json.Account = UInt160.json_rewrite(src); - - // Allow limit of 0 through. - if (undefined !== limit) - this.tx_json.LimitAmount = Amount.json_rewrite(limit); - - if (quality_in) - this.tx_json.QualityIn = quality_in; - - if (quality_out) - this.tx_json.QualityOut = quality_out; - - // XXX Throw an error if nothing is set. - - return this; -}; - -Transaction.prototype.wallet_add = function (src, amount, authorized_key, public_key, signature) { - this._secret = this._account_secret(src); - this.tx_json.TransactionType = 'WalletAdd'; - this.tx_json.Amount = Amount.json_rewrite(amount); - this.tx_json.RegularKey = authorized_key; - this.tx_json.PublicKey = public_key; - this.tx_json.Signature = signature; - - return this; -}; - exports.config = {}; exports.Remote = Remote; diff --git a/src/js/transaction.js b/src/js/transaction.js new file mode 100644 index 0000000000..3fedb3dca6 --- /dev/null +++ b/src/js/transaction.js @@ -0,0 +1,522 @@ +// +// Transactions +// +// Construction: +// remote.transaction() // Build a transaction object. +// .offer_create(...) // Set major parameters. +// .set_flags() // Set optional parameters. +// .on() // Register for events. +// .submit(); // Send to network. +// +// Events: +// 'success' : Transaction submitted without error. +// 'error' : Error submitting transaction. +// 'proposed' : Advisory proposed status transaction. +// - A client should expect 0 to multiple results. +// - Might not get back. The remote might just forward the transaction. +// - A success could be reverted in final. +// - local error: other remotes might like it. +// - malformed error: local server thought it was malformed. +// - The client should only trust this when talking to a trusted server. +// 'final' : Final status of transaction. +// - Only expect a final from dishonest servers after a tesSUCCESS or ter*. +// 'lost' : Gave up looking for on ledger_closed. +// 'pending' : Transaction was not found on ledger_closed. +// 'state' : Follow the state of a transaction. +// 'client_submitted' - Sent to remote +// |- 'remoteError' - Remote rejected transaction. +// \- 'client_proposed' - Remote provisionally accepted transaction. +// |- 'client_missing' - Transaction has not appeared in ledger as expected. +// | |\- 'client_lost' - No longer monitoring missing transaction. +// |/ +// |- 'tesSUCCESS' - Transaction in ledger as expected. +// |- 'ter...' - Transaction failed. +// \- 'tec...' - Transaction claimed fee only. +// +// Notes: +// - All transactions including those with local and malformed errors may be +// forwarded anyway. +// - A malicous server can: +// - give any proposed result. +// - it may declare something correct as incorrect or something correct as incorrect. +// - it may not communicate with the rest of the network. +// - may or may not forward. +// + +var Amount = require('./amount').Amount; +var Currency = require('./amount').Currency; +var UInt160 = require('./amount').UInt160; +var EventEmitter = require('events').EventEmitter; + +var SUBMIT_MISSING = 4; // Report missing. +var SUBMIT_LOST = 8; // Give up tracking. + +// A class to implement transactions. +// - Collects parameters +// - Allow event listeners to be attached to determine the outcome. +var Transaction = function (remote) { + // YYY Make private as many variables as possible. + var self = this; + + this.callback = undefined; + this.remote = remote; + this._secret = undefined; + this._build_path = false; + this.tx_json = { // Transaction data. + 'Flags' : 0, // XXX Would be nice if server did not require this. + }; + this.hash = undefined; + this.submit_index = undefined; // ledger_current_index was this when transaction was submited. + this.state = undefined; // Under construction. + + this.on('success', function (message) { + if (message.engine_result) { + self.hash = message.tx_json.hash; + + self.set_state('client_proposed'); + + self.emit('proposed', { + 'tx_json' : message.tx_json, + 'result' : message.engine_result, + 'result_code' : message.engine_result_code, + 'result_message' : message.engine_result_message, + 'rejected' : self.isRejected(message.engine_result_code), // If server is honest, don't expect a final if rejected. + }); + } + }); + + this.on('error', function (message) { + // Might want to give more detailed information. + self.set_state('remoteError'); + }); +}; + +Transaction.prototype = new EventEmitter; + +// XXX This needs to be determined from the network. +Transaction.fees = { + 'default' : Amount.from_json("10"), + 'nickname_create' : Amount.from_json("1000"), + 'offer' : Amount.from_json("10"), +}; + +Transaction.flags = { + 'OfferCreate' : { + 'Passive' : 0x00010000, + }, + + 'Payment' : { + 'NoRippleDirect' : 0x00010000, + 'PartialPayment' : 0x00020000, + 'LimitQuality' : 0x00040000, + }, +}; + +Transaction.prototype.consts = { + 'telLOCAL_ERROR' : -399, + 'temMALFORMED' : -299, + 'tefFAILURE' : -199, + 'terRETRY' : -99, + 'tesSUCCESS' : 0, + 'tecCLAIMED' : 100, +}; + +Transaction.prototype.isTelLocal = function (ter) { + return ter >= this.consts.telLOCAL_ERROR && ter < this.consts.temMALFORMED; +}; + +Transaction.prototype.isTemMalformed = function (ter) { + return ter >= this.consts.temMALFORMED && ter < this.consts.tefFAILURE; +}; + +Transaction.prototype.isTefFailure = function (ter) { + return ter >= this.consts.tefFAILURE && ter < this.consts.terRETRY; +}; + +Transaction.prototype.isTerRetry = function (ter) { + return ter >= this.consts.terRETRY && ter < this.consts.tesSUCCESS; +}; + +Transaction.prototype.isTepSuccess = function (ter) { + return ter >= this.consts.tesSUCCESS; +}; + +Transaction.prototype.isTecClaimed = function (ter) { + return ter >= this.consts.tecCLAIMED; +}; + +Transaction.prototype.isRejected = function (ter) { + return this.isTelLocal(ter) || this.isTemMalformed(ter) || this.isTefFailure(ter); +}; + +Transaction.prototype.set_state = function (state) { + if (this.state !== state) { + this.state = state; + this.emit('state', state); + } +}; + +// Submit a transaction to the network. +// XXX Don't allow a submit without knowing ledger_index. +// XXX Have a network canSubmit(), post events for following. +// XXX Also give broader status for tracking through network disconnects. +// callback = function (status, info) { +// // status is final status. Only works under a ledger_accepting conditions. +// switch status: +// case 'tesSUCCESS': all is well. +// case 'tejServerUntrusted': sending secret to untrusted server. +// case 'tejInvalidAccount': locally detected error. +// case 'tejLost': locally gave up looking +// default: some other TER +// } +Transaction.prototype.submit = function (callback) { + var self = this; + var tx_json = this.tx_json; + + this.callback = callback; + + if ('string' !== typeof tx_json.Account) + { + (this.callback || this.emit)('error', { + 'error' : 'tejInvalidAccount', + 'error_message' : 'Bad account.' + }); + return; + } + + // YYY Might check paths for invalid accounts. + + if (this.remote.local_fee && undefined === tx_json.Fee) { + tx_json.Fee = Transaction.fees['default'].to_json(); + } + + if (this.callback || this.listeners('final').length || this.listeners('lost').length || this.listeners('pending').length) { + // There are listeners for callback, 'final', 'lost', or 'pending' arrange to emit them. + + this.submit_index = this.remote._ledger_current_index; + + // When a ledger closes, look for the result. + var on_ledger_closed = function (message) { + var ledger_hash = message.ledger_hash; + var ledger_index = message.ledger_index; + var stop = false; + +// XXX make sure self.hash is available. + self.remote.request_transaction_entry(self.hash) + .ledger_hash(ledger_hash) + .on('success', function (message) { + self.set_state(message.metadata.TransactionResult); + self.emit('final', message); + + if (self.callback) + self.callback(message.metadata.TransactionResult, message); + + stop = true; + }) + .on('error', function (message) { + if ('remoteError' === message.error + && 'transactionNotFound' === message.remote.error) { + if (self.submit_index + SUBMIT_LOST < ledger_index) { + self.set_state('client_lost'); // Gave up. + self.emit('lost'); + + if (self.callback) + self.callback('tejLost', message); + + stop = true; + } + else if (self.submit_index + SUBMIT_MISSING < ledger_index) { + self.set_state('client_missing'); // We don't know what happened to transaction, still might find. + self.emit('pending'); + } + else { + self.emit('pending'); + } + } + // XXX Could log other unexpectedness. + }) + .request(); + + if (stop) { + self.remote.removeListener('ledger_closed', on_ledger_closed); + self.emit('final', message); + } + }; + + this.remote.on('ledger_closed', on_ledger_closed); + + if (this.callback) { + this.on('error', function (message) { + self.callback(message.error, message); + }); + } + } + + this.set_state('client_submitted'); + + this.remote.submit(this); + + return this; +} + +// +// Set options for Transactions +// + +// --> build: true, to have server blindly construct a path. +// +// "blindly" because the sender has no idea of the actual cost except that is must be less than send max. +Transaction.prototype.build_path = function (build) { + this._build_path = build; + + return this; +} + +// tag should be undefined or a 32 bit integer. +// YYY Add range checking for tag. +Transaction.prototype.destination_tag = function (tag) { + if (undefined !== tag) + this.tx_json.DestinationTag = tag; + + return this; +} + +Transaction._path_rewrite = function (path) { + var path_new = []; + + for (var index in path) { + var node = path[index]; + var node_new = {}; + + if ('account' in node) + node_new.account = UInt160.json_rewrite(node.account); + + if ('issuer' in node) + node_new.issuer = UInt160.json_rewrite(node.issuer); + + if ('currency' in node) + node_new.currency = Currency.json_rewrite(node.currency); + + path_new.push(node_new); + } + + return path_new; +} + +Transaction.prototype.path_add = function (path) { + this.tx_json.Paths = this.tx_json.Paths || [] + this.tx_json.Paths.push(Transaction._path_rewrite(path)); + + return this; +} + +// --> paths: undefined or array of path +// A path is an array of objects containing some combination of: account, currency, issuer +Transaction.prototype.paths = function (paths) { + for (var index in paths) { + this.path_add(paths[index]); + } + + return this; +} + +// If the secret is in the config object, it does not need to be provided. +Transaction.prototype.secret = function (secret) { + this._secret = secret; +} + +Transaction.prototype.send_max = function (send_max) { + if (send_max) + this.tx_json.SendMax = Amount.json_rewrite(send_max); + + return this; +} + +// tag should be undefined or a 32 bit integer. +// YYY Add range checking for tag. +Transaction.prototype.source_tag = function (tag) { + if (undefined !== tag) + this.tx_json.SourceTag = tag; + + return this; +} + +// --> rate: In billionths. +Transaction.prototype.transfer_rate = function (rate) { + this.tx_json.TransferRate = Number(rate); + + if (this.tx_json.TransferRate < 1e9) + throw 'invalidTransferRate'; + + return this; +} + +// Add flags to a transaction. +// --> flags: undefined, _flag_, or [ _flags_ ] +Transaction.prototype.set_flags = function (flags) { + if (flags) { + var transaction_flags = Transaction.flags[this.tx_json.TransactionType]; + + if (undefined == this.tx_json.Flags) // We plan to not define this field on new Transaction. + this.tx_json.Flags = 0; + + var flag_set = 'object' === typeof flags ? flags : [ flags ]; + + for (index in flag_set) { + var flag = flag_set[index]; + + if (flag in transaction_flags) + { + this.tx_json.Flags += transaction_flags[flag]; + } + else { + // XXX Immediately report an error or mark it. + } + } + } + + return this; +} + +// +// Transactions +// + +Transaction.prototype._account_secret = function (account) { + // Fill in secret from remote, if available. + return this.remote.secrets[account]; +}; + +// Options: +// .domain() NYI +// .message_key() NYI +// .transfer_rate() +// .wallet_locator() NYI +// .wallet_size() NYI +Transaction.prototype.account_set = function (src) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'AccountSet'; + this.tx_json.Account = UInt160.json_rewrite(src); + + return this; +}; + +Transaction.prototype.claim = function (src, generator, public_key, signature) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'Claim'; + this.tx_json.Generator = generator; + this.tx_json.PublicKey = public_key; + this.tx_json.Signature = signature; + + return this; +}; + +Transaction.prototype.offer_cancel = function (src, sequence) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'OfferCancel'; + this.tx_json.Account = UInt160.json_rewrite(src); + this.tx_json.OfferSequence = Number(sequence); + + return this; +}; + +// --> expiration : Date or Number +Transaction.prototype.offer_create = function (src, taker_pays, taker_gets, expiration) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'OfferCreate'; + this.tx_json.Account = UInt160.json_rewrite(src); + this.tx_json.TakerPays = Amount.json_rewrite(taker_pays); + this.tx_json.TakerGets = Amount.json_rewrite(taker_gets); + + if (this.remote.local_fee) { + this.tx_json.Fee = Transaction.fees.offer.to_json(); + } + + if (expiration) + this.tx_json.Expiration = Date === expiration.constructor + ? expiration.getTime() + : Number(expiration); + + return this; +}; + +Transaction.prototype.password_fund = function (src, dst) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'PasswordFund'; + this.tx_json.Destination = UInt160.json_rewrite(dst); + + return this; +} + +Transaction.prototype.password_set = function (src, authorized_key, generator, public_key, signature) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'PasswordSet'; + this.tx_json.RegularKey = authorized_key; + this.tx_json.Generator = generator; + this.tx_json.PublicKey = public_key; + this.tx_json.Signature = signature; + + return this; +} + +// Construct a 'payment' transaction. +// +// When a transaction is submitted: +// - If the connection is reliable and the server is not merely forwarding and is not malicious, +// --> src : UInt160 or String +// --> dst : UInt160 or String +// --> deliver_amount : Amount or String. +// +// Options: +// .paths() +// .build_path() +// .destination_tag() +// .path_add() +// .secret() +// .send_max() +// .set_flags() +// .source_tag() +Transaction.prototype.payment = function (src, dst, deliver_amount) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'Payment'; + this.tx_json.Account = UInt160.json_rewrite(src); + this.tx_json.Amount = Amount.json_rewrite(deliver_amount); + this.tx_json.Destination = UInt160.json_rewrite(dst); + + return this; +} + +Transaction.prototype.ripple_line_set = function (src, limit, quality_in, quality_out) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'TrustSet'; + this.tx_json.Account = UInt160.json_rewrite(src); + + // Allow limit of 0 through. + if (undefined !== limit) + this.tx_json.LimitAmount = Amount.json_rewrite(limit); + + if (quality_in) + this.tx_json.QualityIn = quality_in; + + if (quality_out) + this.tx_json.QualityOut = quality_out; + + // XXX Throw an error if nothing is set. + + return this; +}; + +Transaction.prototype.wallet_add = function (src, amount, authorized_key, public_key, signature) { + this._secret = this._account_secret(src); + this.tx_json.TransactionType = 'WalletAdd'; + this.tx_json.Amount = Amount.json_rewrite(amount); + this.tx_json.RegularKey = authorized_key; + this.tx_json.PublicKey = public_key; + this.tx_json.Signature = signature; + + return this; +}; + +exports.Transaction = Transaction; + +// vim:sw=2:sts=2:ts=8:et diff --git a/test/offer-test.js b/test/offer-test.js index 787e06c438..9e4fd417bb 100644 --- a/test/offer-test.js +++ b/test/offer-test.js @@ -1,15 +1,16 @@ -var async = require("async"); -var buster = require("buster"); +var async = require("async"); +var buster = require("buster"); -var Amount = require("../src/js/amount.js").Amount; -var Remote = require("../src/js/remote.js").Remote; -var Server = require("./server.js").Server; +var Amount = require("../src/js/amount").Amount; +var Remote = require("../src/js/remote").Remote; +var Transaction = require("../src/js/transaction").Transaction; +var Server = require("./server").Server; -var testutils = require("./testutils.js"); +var testutils = require("./testutils"); -require("../src/js/amount.js").config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +require("../src/js/amount").config = require("./config"); +require("../src/js/remote").config = require("./config"); buster.testRunner.timeout = 5000; @@ -716,7 +717,7 @@ buster.testCase("Offer tests", { testutils.verify_balances(self.remote, { - "alice" : [ "0/USD/mtgox", String(10000000000+500-2*(Remote.fees['default'].to_number())) ], + "alice" : [ "0/USD/mtgox", String(10000000000+500-2*(Transaction.fees['default'].to_number())) ], "bob" : "100/USD/mtgox", }, callback); @@ -879,7 +880,7 @@ buster.testCase("Offer tests", { testutils.verify_balances(self.remote, { - "alice" : [ "160/USD/mtgox", String(10000000000+200-2*(Remote.fees['default'].to_number())) ], + "alice" : [ "160/USD/mtgox", String(10000000000+200-2*(Transaction.fees['default'].to_number())) ], "bob" : "40/USD/mtgox", }, callback); @@ -921,7 +922,7 @@ buster.testCase("Offer tests", { testutils.verify_balances(self.remote, { - "alice" : [ "100/USD/mtgox", String(10000000000+200+300-4*(Remote.fees['default'].to_number())) ], + "alice" : [ "100/USD/mtgox", String(10000000000+200+300-4*(Transaction.fees['default'].to_number())) ], "bob" : "100/USD/mtgox", }, callback); diff --git a/test/reserve-test.js b/test/reserve-test.js index 995345ccca..1abd4fe7d5 100644 --- a/test/reserve-test.js +++ b/test/reserve-test.js @@ -1,15 +1,16 @@ -var async = require("async"); -var buster = require("buster"); +var async = require("async"); +var buster = require("buster"); -var Amount = require("../src/js/amount.js").Amount; -var Remote = require("../src/js/remote.js").Remote; -var Server = require("./server.js").Server; +var Amount = require("../src/js/amount").Amount; +var Remote = require("../src/js/remote").Remote; +var Transaction = require("../src/js/transaction").Transaction; +var Server = require("./server").Server; -var testutils = require("./testutils.js"); +var testutils = require("./testutils"); -require("../src/js/amount.js").config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +require("../src/js/amount").config = require("./config"); +require("../src/js/remote").config = require("./config"); buster.testRunner.timeout = 5000; @@ -425,7 +426,7 @@ buster.testCase("Reserve", { testutils.verify_balances(self.remote, { - "alice" : [ "0/USD/mtgox", String(10000000000+500-2*(Remote.fees['default'].to_number())) ], + "alice" : [ "0/USD/mtgox", String(10000000000+500-2*(Transaction.fees['default'].to_number())) ], "bob" : "100/USD/mtgox", }, callback); @@ -588,7 +589,7 @@ buster.testCase("Reserve", { testutils.verify_balances(self.remote, { - "alice" : [ "160/USD/mtgox", String(10000000000+200-2*(Remote.fees['default'].to_number())) ], + "alice" : [ "160/USD/mtgox", String(10000000000+200-2*(Transaction.fees['default'].to_number())) ], "bob" : "40/USD/mtgox", }, callback); @@ -630,7 +631,7 @@ buster.testCase("Reserve", { testutils.verify_balances(self.remote, { - "alice" : [ "100/USD/mtgox", String(10000000000+200+300-4*(Remote.fees['default'].to_number())) ], + "alice" : [ "100/USD/mtgox", String(10000000000+200+300-4*(Transaction.fees['default'].to_number())) ], "bob" : "100/USD/mtgox", }, callback);