Skip to content

Commit

Permalink
Improve interface (#259)
Browse files Browse the repository at this point in the history
* Reworked interface of sessions and options.

Removed most of the "continuation" concepts from options and
sessions, replacing with a type-erased "channel" concept.  This
reduces interface complexity greatly.
  • Loading branch information
KazDragon authored Jul 24, 2022
1 parent e3fe05e commit 87748f6
Show file tree
Hide file tree
Showing 69 changed files with 2,345 additions and 3,586 deletions.
9 changes: 7 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ target_compile_options(telnetpp
$<$<CXX_COMPILER_ID:MSVC>:/WX>

# Add warnings on g++ and Clang
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:-Wall -Wextra -Wshadow -Wnon-virtual-dtor -Wpedantic -Wsign-conversion -Werror>
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:-Wall -Wextra -Wshadow -Wnon-virtual-dtor -Wpedantic -Wsign-conversion -Werror -Wno-unused-parameter>
)

# The required C++ Standard for Telnet++ is C++17.
Expand Down Expand Up @@ -343,14 +343,19 @@ add_executable(telnetpp_tester)

target_sources(telnetpp_tester
PRIVATE
test/fakes/fake_channel.hpp
test/fakes/fake_client_option.hpp
test/fakes/fake_compressor.hpp
test/fakes/fake_decompressor.hpp
test/telnet_option_fixture.hpp

test/client_option_test.cpp
test/command_test.cpp
test/command_router_test.cpp
test/echo_client_test.cpp
test/echo_server_test.cpp
test/element_test.cpp
test/generator_test.cpp
test/in_memory_session_test.cpp
test/mccp_client_test.cpp
test/mccp_server_test.cpp
test/msdp_client_test.cpp
Expand Down
64 changes: 30 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ A [telnetpp::element](include/telnetpp/element.hpp) is a variant that may contai

# Stream-Unaware

The Telnet++ library does not impose any requirement on any kind of data stream API. In order to accomplish this, it makes heavy use of continuation functions. See the [telnetpp::session](include/telnetpp/session.hpp) class for an in-depth explanation of how this works.
The Telnet++ library does not impose any requirement on any kind of data stream API. In order to accomplish this, it makes heavy use of a channel concept. See the [telnetpp::session](include/telnetpp/session.hpp) class for an in-depth explanation of how this works.

# Options

Expand All @@ -108,30 +108,32 @@ As alluded to earlier, each distinct feature is represented by either a [telnetp
All of the above can be quite complicated to manage, so Telnet++ provides the [telnetpp::session](include/telnetpp/session.hpp) class. This is the key abstraction of the Telnet++ library, and is used to manage an entire Telnet feature set for a connection. This is accomplished by "install"ing handlers for commands and options:

```cpp
// A user-specified function for sending bytes to the remote.
void my_socket_send(telnetpp::bytes data);
// A user-supplied class that models the channel concept
class channel {
void write(telnetpp::bytes);
void async_read(std::function<void (telnetpp::bytes)>);
bool is_alive() const;
void close();
}

// Create a session object, which manages the inputs and outputs from my connection. It requires
// a function that is to be called whenever non-Telnet input is received.
telnetpp::session session;
channel my_channel;

// Create a session object, which manages the inputs and outputs from my channel.
telnetpp::session session{my_channel};

// An echo server (provided with Telnet++) is used to control whether a server responds to input from
// a client by transmitting the same text back to the client. By default, this does not happen, and
// clients print out locally whatever is typed in. By activating this option, the client no longer
// locally echos input, and the server is totally in control of what appears on the screen.
telnetpp::options::echo::server echo_server;
telnetpp::options::echo::server echo_server{session};

// The session now knows we want this feature to be handled and does all the heavy lifting for us.
session.install(echo_server);

// By default, options sit there in a deactivated state unless explicitly activated either locally
// in code or in protocol from the remote. Here, we activate it ourselves, forwarding protocol
// via the session, and eventually out through our user-specified function.
echo_server.activate(
[&](telnetpp::element const &elem)
{
session.send(elem, my_socket_send);
});
// in code or in protocol from the remote. Here, we activate it ourselves. This uses the session
// to ensure that the protocol bytes are forwarded to the channel.
echo_server.activate();

// Sessions just pass on commands to functions installed on a per-command basis. Here we pass a
// lambda to handle the Are You There command.
Expand All @@ -143,29 +145,23 @@ session.install(
using telnetpp::literals;
auto const message = "Yes, I'm here"_tb;

session.send(message, my_socket_send);
session.write(message, my_socket_send);
});
```
Receiving data is slightly more complex in that any reception of data may require data to be sent, and so functions that receive data also have a continuation for what to do with the response.
Receiving data is slightly more complex since it is asynchronous and so requires a callback that is called when data is received.
```cpp
// A user-specified function used for receiving bytes sent from the
// remote.
int my_socket_receive(telnetpp::byte *buffer, int size);
// A user-specified function that transmits data up to the application
// Note that the second argument is used to tell the application how
// it may send a respond to the data it receives.
void my_application_receive(
telnetpp::bytes data,
std::function<void (telnetpp::bytes)> const &send);
telnetpp::byte my_buffer[1024];
int amount_received = my_socket_receive(my_buffer, 1024);
session.receive(
telnetpp::bytes{my_buffer, amount_received},
my_application_receive,
my_socket_send);
// A user-specified function that transmits data up to the application.
// Note: the session indicates that an async_read is complete by sending
// an empty packet of data. This can be used to prompt a new async_read,
// for example.
void my_application_receive(telnetpp::bytes data);
session.async_read(
[&](telnetpp::bytes data)
{
my_application_receive(data);
});
```
2 changes: 1 addition & 1 deletion examples/rot13server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ if (EXISTS ${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
)
else()
find_package(serverpp REQUIRED)
find_package(telnetpp REQUIRED)
find_package(telnetpp 3.0.0 REQUIRED)
find_package(Boost COMPONENTS container REQUIRED)
find_package(gsl-lite REQUIRED)
find_package(ZLIB REQUIRED)
Expand Down
92 changes: 28 additions & 64 deletions examples/rot13server/src/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,26 @@ struct connection::impl
// When the window size of the client changes, announce it to a
// listener
telnet_naws_client_.on_window_size_changed.connect(
[this](auto &&width, auto &&height, auto &&continuation)
[this](auto &&width, auto &&height)
{
this->on_window_size_changed(width, height);
});

// When the Terminal Type option becomes active, request the client's
// terminal type.
telnet_terminal_type_client_.on_state_changed.connect(
[this](auto &&continuation)
[this]()
{
if (telnet_terminal_type_client_.active())
{
telnet_terminal_type_client_.request_terminal_type(continuation);
telnet_terminal_type_client_.request_terminal_type();
}
});

// When we receive the client's terminal type, announce it to a
// listener
telnet_terminal_type_client_.on_terminal_type.connect(
[this](auto &&type, auto &&continuation)
[this](auto &&type)
{
std::string user_type(type.begin(), type.end());
this->on_terminal_type_detected(user_type);
Expand All @@ -55,24 +55,17 @@ struct connection::impl
telnet_session_.install(telnet_naws_client_);
telnet_session_.install(telnet_terminal_type_client_);

// Send activations for the options we want to be activated immediately.
auto const &write_continuation =
[this](telnetpp::element const &elem)
{
this->write(elem);
};

// Activating ECHO stops the terminal from locally echoing what is
// typed, and SUPPRESS-GA on top of that enables "character at a time"
// mode, as opposed to line mode.
telnet_echo_server_.activate(write_continuation);
telnet_suppress_ga_server_.activate(write_continuation);
telnet_echo_server_.activate();
telnet_suppress_ga_server_.activate();

// For fun, activate NAWS so that we get information about when the
// terminal size changes, and also TERMINAL-TYPE so that we can see
// what the client has used to connect.
telnet_naws_client_.activate(write_continuation);
telnet_terminal_type_client_.activate(write_continuation);
telnet_naws_client_.activate();
telnet_terminal_type_client_.activate();
}

// ======================================================================
Expand Down Expand Up @@ -107,12 +100,7 @@ struct connection::impl
{
// Send general data via the Telnet Session so that Telnet control
// characters can be escaped properly.
telnet_session_.send(
data,
[this](telnetpp::bytes data)
{
this->raw_write(data);
});
telnet_session_.write(data);
}

// ======================================================================
Expand All @@ -122,45 +110,21 @@ struct connection::impl
std::function<void (serverpp::bytes)> const &data_continuation,
std::function<void ()> const &read_complete_continuation)
{
// Requests that we read some data from the socket, passing a
// continuation that is called when data is received.

// Upon reception, this is filtered through the Telnet Session.
// This takes two continuations:
// o The first is for application layer non-Telnet data.
// This continuation passes both the data and also another
// continuation that can be used for the application to
// respond to the data, although this is unused in this
// example.
//
// o The second is the Telnet "response" continuation.
// Since the Telnet session object doesn't know or care
// where the data is coming from or going to, this is
// the function that it uses to respond to incoming
// messages (e.g. if it receives an option negotiation,
// this is the function it calls to send the response).
//
// Finally, the completion continuation is called every time.
// This is because it could be the case that all the data is
// consumed as Telnet messages, and more data needs to be
// requested. Or the socket could have disconnected with no
// data received. Either way, the caller of async_read must
// know to take action at this point in time.
socket_.async_read(
[=](serverpp::bytes data)
// Requests that we read some data from the telnet session,
// Because Telnet contains both in- and out-of band data, one
// read may invoke the data continuation several times. The end
// of the current read is indicated by sending us empty data.
telnet_session_.async_read(
[=](telnetpp::bytes data)
{
telnet_session_.receive(
data,
[=](telnetpp::bytes data, auto &&send)
{
data_continuation(data);
},
[=](telnetpp::bytes data)
{
this->raw_write(data);
});

read_complete_continuation();
if (data.empty())
{
read_complete_continuation();
}
else
{
data_continuation(data);
}
});
}

Expand Down Expand Up @@ -199,11 +163,11 @@ struct connection::impl

serverpp::tcp_socket socket_;

telnetpp::session telnet_session_;
telnetpp::options::echo::server telnet_echo_server_;
telnetpp::options::suppress_ga::server telnet_suppress_ga_server_;
telnetpp::options::naws::client telnet_naws_client_;
telnetpp::options::terminal_type::client telnet_terminal_type_client_;
telnetpp::session telnet_session_{socket_};
telnetpp::options::echo::server telnet_echo_server_{telnet_session_};
telnetpp::options::suppress_ga::server telnet_suppress_ga_server_{telnet_session_};
telnetpp::options::naws::client telnet_naws_client_{telnet_session_};
telnetpp::options::terminal_type::client telnet_terminal_type_client_{telnet_session_};

std::function<void (std::uint16_t, std::uint16_t)> on_window_size_changed_;

Expand Down
21 changes: 15 additions & 6 deletions include/telnetpp/client_option.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,20 @@ namespace telnetpp {
/// state.
/// \see https://tools.ietf.org/html/std8
//* =========================================================================
using client_option = telnetpp::option<
telnetpp::do_,
telnetpp::dont,
telnetpp::will,
telnetpp::wont
>;
class TELNETPP_EXPORT client_option
: public telnetpp::option<
telnetpp::do_,
telnetpp::dont,
telnetpp::will,
telnetpp::wont
>
{
public:
explicit client_option(
telnetpp::session &sess, telnetpp::option_type code) noexcept
: option{sess, code}
{
}
};

}
4 changes: 1 addition & 3 deletions include/telnetpp/detail/command_router.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ class command_router
: public router<
command_type,
command,
void (
command const &,
std::function<void (telnetpp::element const &)>),
void (command),
detail::command_router_key_from_message_policy
>
{
Expand Down
4 changes: 1 addition & 3 deletions include/telnetpp/detail/negotiation_router.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ class negotiation_router
: public router<
negotiation,
negotiation,
void (
telnetpp::negotiation,
std::function<void (telnetpp::element const &)> const &),
void (telnetpp::negotiation),
detail::negotiation_router_key_from_message_policy
>
{
Expand Down
8 changes: 4 additions & 4 deletions include/telnetpp/detail/registration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ void register_route_from_negotiation_to_option(
{
route.register_route(
negotiation{request, option.option_code()},
[&option, request](auto &&/*neg*/, auto &&cont)
[&option, request](telnetpp::negotiation const &)
{
return option.negotiate(request, cont);
return option.negotiate(request);
});
}

Expand All @@ -51,9 +51,9 @@ void register_route_from_subnegotiation_to_option(
SubnegotiableOption &option)
{
route.register_route(option.option_code(),
[&option](auto &&sub, auto &&cont)
[&option](telnetpp::subnegotiation const &sub)
{
return option.subnegotiate(sub.content(), cont);
return option.subnegotiate(sub.content());
});
}

Expand Down
4 changes: 1 addition & 3 deletions include/telnetpp/detail/subnegotiation_router.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ class subnegotiation_router
: public router<
option_type,
subnegotiation,
void (
telnetpp::subnegotiation,
std::function<void (telnetpp::element const &)>),
void (telnetpp::subnegotiation),
detail::subnegotiation_router_key_from_message_policy
>
{
Expand Down
Loading

0 comments on commit 87748f6

Please sign in to comment.