Major overhaul to handshake internal state machine error checking references #389 #361

This overhaul removes the use of exceptions in handling state errors
during handshake processing. All such errors are demoted to
rerror/recoverable error and will terminate the connection only rather
than the whole application. In addition, there are some cases where we
expect to see a state error due to non-deterministic interleaving of
timeout handlers and read/write handlers. In these expected cases state
errors are ignored entirely.
This commit is contained in:
Peter Thorson committed 2014-12-11 09:52:03 -05:00
1 parent 922f1c35bc
commit e1f1c8873b
2 files changed
+209 -208

No files matched your search

-39
View File
@@ -1251,45 +1251,6 @@ protected:
/// Perform WebSocket handshake validation of m_request using m_processor.
/// set m_response and return false on error.
bool process_handshake_request();
/// Atomically change the internal connection state.
/**
* @param req The required starting state. If the internal state does not
* match req an exception is thrown.
*
* @param dest The state to change to.
*
* @param msg The message to include in the exception thrown
*/
void atomic_state_change(istate_type req, istate_type dest,
std::string msg);
/// Atomically change the internal and external connection state.
/**
* @param ireq The required starting internal state. If the internal state
* does not match ireq an exception is thrown.
*
* @param idest The internal state to change to.
*
* @param ereq The required starting external state. If the external state
* does not match ereq an exception is thrown.
*
* @param edest The external state to change to.
*
* @param msg The message to include in the exception thrown
*/
void atomic_state_change(istate_type ireq, istate_type idest,
session::state::value ereq, session::state::value edest,
std::string msg);
/// Atomically read and compared the internal state.
/**
* @param req The state to test against. If the internal state does not
* match req an exception is thrown.
*
* @param msg The message to include in the exception thrown
*/
void atomic_state_check(istate_type req, std::string msg);
private:
/// Completes m_response, serializes it, and sends it out on the wire.
void send_http_response();
+209 -169
View File
@@ -105,10 +105,12 @@ lib::error_code connection<config>::send(typename config::message_type::ptr msg)
if (m_alog.static_test(log::alevel::devel)) {
m_alog.write(log::alevel::devel,"connection send");
}
// TODO:
if (m_state != session::state::open) {
return error::make_error_code(error::invalid_state);
{
scoped_lock_type lock(m_connection_state_lock);
if (m_state != session::state::open) {
return error::make_error_code(error::invalid_state);
}
}
message_ptr outgoing_msg;
@@ -154,9 +156,15 @@ void connection<config>::ping(std::string const& payload, lib::error_code& ec) {
m_alog.write(log::alevel::devel,"connection ping");
}
if (m_state != session::state::open) {
ec = error::make_error_code(error::invalid_state);
return;
{
scoped_lock_type lock(m_connection_state_lock);
if (m_state != session::state::open) {
std::stringstream ss;
ss << "connection::ping called from invalid state " << m_state;
m_alog.write(log::alevel::devel,ss.str());
ec = error::make_error_code(error::invalid_state);
return;
}
}
message_ptr msg = m_msg_manager->get_message();
@@ -245,9 +253,15 @@ void connection<config>::pong(std::string const& payload, lib::error_code& ec) {
m_alog.write(log::alevel::devel,"connection pong");
}
if (m_state != session::state::open) {
ec = error::make_error_code(error::invalid_state);
return;
{
scoped_lock_type lock(m_connection_state_lock);
if (m_state != session::state::open) {
std::stringstream ss;
ss << "connection::pong called from invalid state " << m_state;
m_alog.write(log::alevel::devel,ss.str());
ec = error::make_error_code(error::invalid_state);
return;
}
}
message_ptr msg = m_msg_manager->get_message();
@@ -293,15 +307,17 @@ void connection<config>::close(close::status::value const code,
m_alog.write(log::alevel::devel,"connection close");
}
// Truncate reason to maximum size allowable in a close frame.
std::string tr(reason,0,std::min<size_t>(reason.size(),
frame::limits::close_reason_size));
scoped_lock_type lock(m_connection_state_lock);
if (m_state != session::state::open) {
ec = error::make_error_code(error::invalid_state);
return;
}
// Truncate reason to maximum size allowable in a close frame.
std::string tr(reason,0,std::min<size_t>(reason.size(),
frame::limits::close_reason_size));
ec = this->send_close_frame(code,tr,false,close::status::terminal(code));
}
@@ -526,21 +542,16 @@ connection<config>::get_response_header(std::string const & key) const {
template <typename config>
void connection<config>::set_status(http::status_code::value code)
{
//scoped_lock_type lock(m_connection_state_lock);
if (m_internal_state != istate::PROCESS_HTTP_REQUEST) {
throw exception("Call to set_status from invalid state",
error::make_error_code(error::invalid_state));
}
m_response.set_status(code);
}
template <typename config>
void connection<config>::set_status(http::status_code::value code,
std::string const & msg)
{
//scoped_lock_type lock(m_connection_state_lock);
if (m_internal_state != istate::PROCESS_HTTP_REQUEST) {
throw exception("Call to set_status from invalid state",
error::make_error_code(error::invalid_state));
@@ -550,8 +561,6 @@ void connection<config>::set_status(http::status_code::value code,
}
template <typename config>
void connection<config>::set_body(std::string const & value) {
//scoped_lock_type lock(m_connection_state_lock);
if (m_internal_state != istate::PROCESS_HTTP_REQUEST) {
throw exception("Call to set_status from invalid state",
error::make_error_code(error::invalid_state));
@@ -564,8 +573,6 @@ template <typename config>
void connection<config>::append_header(std::string const & key,
std::string const & val)
{
//scoped_lock_type lock(m_connection_state_lock);
if (m_is_server) {
if (m_internal_state == istate::PROCESS_HTTP_REQUEST) {
// we are setting response headers for an incoming server connection
@@ -588,8 +595,6 @@ template <typename config>
void connection<config>::replace_header(std::string const & key,
std::string const & val)
{
// scoped_lock_type lock(m_connection_state_lock);
if (m_is_server) {
if (m_internal_state == istate::PROCESS_HTTP_REQUEST) {
// we are setting response headers for an incoming server connection
@@ -611,8 +616,6 @@ void connection<config>::replace_header(std::string const & key,
template <typename config>
void connection<config>::remove_header(std::string const & key)
{
//scoped_lock_type lock(m_connection_state_lock);
if (m_is_server) {
if (m_internal_state == istate::PROCESS_HTTP_REQUEST) {
// we are setting response headers for an incoming server connection
@@ -643,11 +646,13 @@ template <typename config>
void connection<config>::start() {
m_alog.write(log::alevel::devel,"connection start");
this->atomic_state_change(
istate::USER_INIT,
istate::TRANSPORT_INIT,
"Start must be called from user init state"
);
if (m_internal_state != istate::USER_INIT) {
m_alog.write(log::alevel::devel,"Start called in invalid state");
this->terminate(error::make_error_code(error::invalid_state));
return;
}
m_internal_state = istate::TRANSPORT_INIT;
// Depending on how the transport implements init this function may return
// immediately and call handle_transport_init later or call
@@ -665,39 +670,31 @@ template <typename config>
void connection<config>::handle_transport_init(lib::error_code const & ec) {
m_alog.write(log::alevel::devel,"connection handle_transport_init");
{
scoped_lock_type lock(m_connection_state_lock);
lib::error_code ecm = ec;
if (m_internal_state != istate::TRANSPORT_INIT) {
throw exception("handle_transport_init must be called from transport init state",
error::make_error_code(error::invalid_state));
}
if (!ec) {
// unless there was a transport error, advance internal state.
if (m_is_server) {
m_internal_state = istate::READ_HTTP_REQUEST;
} else {
m_internal_state = istate::WRITE_HTTP_REQUEST;
}
}
if (m_internal_state != istate::TRANSPORT_INIT) {
m_alog.write(log::alevel::devel,
"handle_transport_init must be called from transport init state");
ecm = error::make_error_code(error::invalid_state);
}
if (ec) {
if (ecm) {
std::stringstream s;
s << "handle_transport_init received error: "<< ec.message();
m_elog.write(log::elevel::fatal,s.str());
s << "handle_transport_init received error: "<< ecm.message();
m_elog.write(log::elevel::rerror,s.str());
this->terminate(ec);
this->terminate(ecm);
return;
}
// At this point the transport is ready to read and write bytes.
if (m_is_server) {
m_internal_state = istate::READ_HTTP_REQUEST;
this->read_handshake(1);
} else {
// We are a client. Set the processor to the version specified in the
// config file and send a handshake request.
m_internal_state = istate::WRITE_HTTP_REQUEST;
m_processor = get_processor(config::client_version);
this->send_http_request();
}
@@ -739,24 +736,37 @@ void connection<config>::handle_read_handshake(lib::error_code const & ec,
{
m_alog.write(log::alevel::devel,"connection handle_read_handshake");
this->atomic_state_check(
istate::READ_HTTP_REQUEST,
"handle_read_handshake must be called from READ_HTTP_REQUEST state"
);
lib::error_code ecm = ec;
if (ec) {
if (ec == transport::error::eof) {
// we expect to get eof if the connection is closed already
if (m_state == session::state::closed) {
m_alog.write(log::alevel::devel,"got eof from closed con");
return;
}
if (!ecm) {
scoped_lock_type lock(m_connection_state_lock);
if (m_state == session::state::connecting) {
if (m_internal_state != istate::READ_HTTP_REQUEST) {
ecm = error::make_error_code(error::invalid_state);
}
} else if (m_state == session::state::closed) {
// The connection was canceled while the response was being sent,
// usually by the handshake timer. This is basically expected
// (though hopefully rare) and there is nothing we can do so ignore.
m_alog.write(log::alevel::devel,
"handle_send_http_response invoked after connection was closed");
return;
} else {
ecm = error::make_error_code(error::invalid_state);
}
}
std::stringstream s;
s << "error in handle_read_handshake: "<< ec.message();
m_elog.write(log::elevel::fatal,s.str());
this->terminate(ec);
if (ecm) {
if (ecm == transport::error::eof && m_state == session::state::closed) {
// we expect to get eof if the connection is closed already
m_alog.write(log::alevel::devel,
"got (expected) eof/state error from closed con");
return;
}
log_err(log::elevel::rerror,"handle_read_handshake",ecm);
this->terminate(ecm);
return;
}
@@ -831,12 +841,9 @@ void connection<config>::handle_read_handshake(lib::error_code const & ec,
std::copy(m_buf+bytes_processed,m_buf+bytes_transferred,m_buf);
m_buf_cursor = bytes_transferred-bytes_processed;
this->atomic_state_change(
istate::READ_HTTP_REQUEST,
istate::PROCESS_HTTP_REQUEST,
"send_http_response must be called from READ_HTTP_REQUEST state"
);
m_internal_state = istate::PROCESS_HTTP_REQUEST;
// We have the complete request. Process it.
this->process_handshake_request();
this->send_http_response();
@@ -863,11 +870,15 @@ void connection<config>::handle_read_handshake(lib::error_code const & ec,
// state and calls send_http_response
template <typename config>
void connection<config>::send_http_response_error() {
this->atomic_state_change(
istate::READ_HTTP_REQUEST,
istate::PROCESS_HTTP_REQUEST,
"send_http_response must be called from READ_HTTP_REQUEST state"
);
if (m_internal_state != istate::READ_HTTP_REQUEST) {
m_alog.write(log::alevel::devel,
"send_http_response_error called in invalid state");
this->terminate(error::make_error_code(error::invalid_state));
return;
}
m_internal_state = istate::PROCESS_HTTP_REQUEST;
this->send_http_response();
}
@@ -879,15 +890,16 @@ void connection<config>::handle_read_frame(lib::error_code const & ec,
{
//m_alog.write(log::alevel::devel,"connection handle_read_frame");
this->atomic_state_check(
istate::PROCESS_CONNECTION,
"handle_read_frame must be called from PROCESS_CONNECTION state"
);
lib::error_code ecm = ec;
if (ec) {
log::level echannel = log::elevel::fatal;
if (!ecm && m_internal_state != istate::PROCESS_CONNECTION) {
ecm = error::make_error_code(error::invalid_state);
}
if (ecm) {
log::level echannel = log::elevel::rerror;
if (ec == transport::error::eof) {
if (ecm == transport::error::eof) {
if (m_state == session::state::closed) {
// we expect to get eof if the connection is closed already
// just ignore it
@@ -900,8 +912,17 @@ void connection<config>::handle_read_frame(lib::error_code const & ec,
terminate(lib::error_code());
return;
}
}
if (ec == transport::error::tls_short_read) {
} else if (ecm == error::invalid_state) {
// In general, invalid state errors in the closed state are the
// result of handlers that were in the system already when the state
// changed and should be ignored as they pose no problems and there
// is nothing useful that we can do about them.
if (m_state == session::state::closed) {
m_alog.write(log::alevel::devel,
"handle_read_frame: got invalid istate in closed state");
return;
}
} else if (ecm == transport::error::tls_short_read) {
if (m_state == session::state::closed) {
// We expect to get a TLS short read if we try to read after the
// connection is closed. If this happens ignore and exit the
@@ -910,12 +931,12 @@ void connection<config>::handle_read_frame(lib::error_code const & ec,
return;
}
echannel = log::elevel::rerror;
} else if (ec == transport::error::action_after_shutdown) {
} else if (ecm == transport::error::action_after_shutdown) {
echannel = log::elevel::info;
}
log_err(echannel, "handle_read_frame", ec);
this->terminate(ec);
log_err(echannel, "handle_read_frame", ecm);
this->terminate(ecm);
return;
}
@@ -1230,24 +1251,47 @@ template <typename config>
void connection<config>::handle_send_http_response(lib::error_code const & ec) {
m_alog.write(log::alevel::devel,"handle_send_http_response");
this->atomic_state_check(
istate::PROCESS_HTTP_REQUEST,
"handle_send_http_response must be called from PROCESS_HTTP_REQUEST state"
);
lib::error_code ecm = ec;
if (ec) {
log_err(log::elevel::rerror,"handle_send_http_response",ec);
this->terminate(ec);
return;
if (!ecm) {
scoped_lock_type lock(m_connection_state_lock);
if (m_state == session::state::connecting) {
if (m_internal_state != istate::PROCESS_HTTP_REQUEST) {
ecm = error::make_error_code(error::invalid_state);
}
} else if (m_state == session::state::closed) {
// The connection was canceled while the response was being sent,
// usually by the handshake timer. This is basically expected
// (though hopefully rare) and there is nothing we can do so ignore.
m_alog.write(log::alevel::devel,
"handle_send_http_response invoked after connection was closed");
return;
} else {
ecm = error::make_error_code(error::invalid_state);
}
}
this->log_open_result();
if (ecm) {
if (ecm == transport::error::eof && m_state == session::state::closed) {
// we expect to get eof if the connection is closed already
m_alog.write(log::alevel::devel,
"got (expected) eof/state error from closed con");
return;
}
log_err(log::elevel::rerror,"handle_send_http_response",ecm);
this->terminate(ecm);
return;
}
if (m_handshake_timer) {
m_handshake_timer->cancel();
m_handshake_timer.reset();
}
this->log_open_result();
if (m_response.get_status_code() != http::status_code::switching_protocols)
{
if (m_processor) {
@@ -1264,13 +1308,8 @@ void connection<config>::handle_send_http_response(lib::error_code const & ec) {
return;
}
this->atomic_state_change(
istate::PROCESS_HTTP_REQUEST,
istate::PROCESS_CONNECTION,
session::state::connecting,
session::state::open,
"handle_send_http_response must be called from PROCESS_HTTP_REQUEST state"
);
m_internal_state = istate::PROCESS_CONNECTION;
m_state = session::state::open;
if (m_open_handler) {
m_open_handler(m_connection_hdl);
@@ -1342,22 +1381,41 @@ template <typename config>
void connection<config>::handle_send_http_request(lib::error_code const & ec) {
m_alog.write(log::alevel::devel,"handle_send_http_request");
this->atomic_state_check(
istate::WRITE_HTTP_REQUEST,
"handle_send_http_request must be called from WRITE_HTTP_REQUEST state"
);
lib::error_code ecm = ec;
if (ec) {
log_err(log::elevel::rerror,"handle_send_http_request",ec);
this->terminate(ec);
return;
if (!ecm) {
scoped_lock_type lock(m_connection_state_lock);
if (m_state == session::state::connecting) {
if (m_internal_state != istate::WRITE_HTTP_REQUEST) {
ecm = error::make_error_code(error::invalid_state);
} else {
m_internal_state = istate::READ_HTTP_RESPONSE;
}
} else if (m_state == session::state::closed) {
// The connection was canceled while the response was being sent,
// usually by the handshake timer. This is basically expected
// (though hopefully rare) and there is nothing we can do so ignore.
m_alog.write(log::alevel::devel,
"handle_send_http_request invoked after connection was closed");
return;
} else {
ecm = error::make_error_code(error::invalid_state);
}
}
this->atomic_state_change(
istate::WRITE_HTTP_REQUEST,
istate::READ_HTTP_RESPONSE,
"handle_send_http_request must be called from WRITE_HTTP_REQUEST state"
);
if (ecm) {
if (ecm == transport::error::eof && m_state == session::state::closed) {
// we expect to get eof if the connection is closed already
m_alog.write(log::alevel::devel,
"got (expected) eof/state error from closed con");
return;
}
log_err(log::elevel::rerror,"handle_send_http_request",ecm);
this->terminate(ecm);
return;
}
transport_con_type::async_read_at_least(
1,
@@ -1378,16 +1436,40 @@ void connection<config>::handle_read_http_response(lib::error_code const & ec,
{
m_alog.write(log::alevel::devel,"handle_read_http_response");
this->atomic_state_check(
istate::READ_HTTP_RESPONSE,
"handle_read_http_response must be called from READ_HTTP_RESPONSE state"
);
lib::error_code ecm = ec;
if (ec) {
log_err(log::elevel::rerror,"handle_send_http_response",ec);
this->terminate(ec);
if (!ecm) {
scoped_lock_type lock(m_connection_state_lock);
if (m_state == session::state::connecting) {
if (m_internal_state != istate::READ_HTTP_RESPONSE) {
ecm = error::make_error_code(error::invalid_state);
}
} else if (m_state == session::state::closed) {
// The connection was canceled while the response was being sent,
// usually by the handshake timer. This is basically expected
// (though hopefully rare) and there is nothing we can do so ignore.
m_alog.write(log::alevel::devel,
"handle_read_http_response invoked after connection was closed");
return;
} else {
ecm = error::make_error_code(error::invalid_state);
}
}
if (ecm) {
if (ecm == transport::error::eof && m_state == session::state::closed) {
// we expect to get eof if the connection is closed already
m_alog.write(log::alevel::devel,
"got (expected) eof/state error from closed con");
return;
}
log_err(log::elevel::rerror,"handle_read_http_response",ecm);
this->terminate(ecm);
return;
}
size_t bytes_processed = 0;
// TODO: refactor this to use error codes rather than exceptions
try {
@@ -1417,14 +1499,9 @@ void connection<config>::handle_read_http_response(lib::error_code const & ec,
return;
}
// response is valid, connection can now be assumed to be open
this->atomic_state_change(
istate::READ_HTTP_RESPONSE,
istate::PROCESS_CONNECTION,
session::state::connecting,
session::state::open,
"handle_read_http_response must be called from READ_HTTP_RESPONSE state"
);
// response is valid, connection can now be assumed to be open
m_internal_state = istate::PROCESS_CONNECTION;
m_state = session::state::open;
this->log_open_result();
@@ -1505,6 +1582,7 @@ void connection<config>::terminate(lib::error_code const & ec) {
m_local_close_reason = ec.message();
}
// TODO: does this need a mutex?
if (m_state == session::state::connecting) {
m_state = session::state::closed;
tstat = failed;
@@ -1708,44 +1786,6 @@ void connection<config>::handle_write_frame(lib::error_code const & ec)
}
}
template <typename config>
void connection<config>::atomic_state_change(istate_type req, istate_type dest,
std::string msg)
{
scoped_lock_type lock(m_connection_state_lock);
if (m_internal_state != req) {
throw exception(msg,error::make_error_code(error::invalid_state));
}
m_internal_state = dest;
}
template <typename config>
void connection<config>::atomic_state_change(istate_type internal_req,
istate_type internal_dest, session::state::value external_req,
session::state::value external_dest, std::string msg)
{
scoped_lock_type lock(m_connection_state_lock);
if (m_internal_state != internal_req || m_state != external_req) {
throw exception(msg,error::make_error_code(error::invalid_state));
}
m_internal_state = internal_dest;
m_state = external_dest;
}
template <typename config>
void connection<config>::atomic_state_check(istate_type req, std::string msg)
{
scoped_lock_type lock(m_connection_state_lock);
if (m_internal_state != req) {
throw exception(msg,error::make_error_code(error::invalid_state));
}
}
template <typename config>
std::vector<int> const & connection<config>::get_supported_versions() const
{