terminal/src/cascadia/TerminalConnection/TelnetConnection.cpp
Carlos Zamora 2608e94822
Introduce TerminalSettingsModel project (#7667)
Introduces a new TerminalSettingsModel (TSM) project. This project is
responsible for (de)serializing and exposing Windows Terminal's settings
as WinRT objects.

## References
#885: TSM epic
#1564: Settings UI is dependent on this for data binding and settings access
#6904: TSM Spec

In the process of ripping out TSM from TerminalApp, a few other changes
were made to make this possible:
1. AppLogic's `ApplicationDisplayName` and `ApplicationVersion` was
   moved to `CascadiaSettings`
   - These are defined as static functions. They also no longer check if
     `AppLogic::Current()` is nullptr.
2. `enum LaunchMode` was moved from TerminalApp to TSM
3. `AzureConnectionType` and `TelnetConnectionType` were moved from the
   profile generators to their respective TerminalConnections
4. CascadiaSettings' `SettingsPath` and `DefaultSettingsPath` are
   exposed as `hstring` instead of `std::filesystem::path`
5. `Command::ExpandCommands()` was exposed via the IDL
   - This required some of the warnings to be saved to an `IVector`
     instead of `std::vector`, among some other small changes.
6. The localization resources had to be split into two halves.
   - Resource file linked in init.cpp. Verified at runtime thanks to the
     StaticResourceLoader.
7. Added constructors to some `ActionArgs`
8. Utils.h/cpp were moved to `cascadia/inc`. `JsonKey()` was moved to
   `JsonUtils`. Both TermApp and TSM need access to Utils.h/cpp.

A large amount of work includes moving to the new namespace
(`TerminalApp` --> `Microsoft::Terminal::Settings::Model`).

Fixing the tests had its own complications. Testing required us to split
up TSM into a DLL and LIB, similar to TermApp. Discussion on creating a
non-local test variant can be found in #7743.

Closes #885
2020-10-06 09:56:59 -07:00

342 lines
12 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "TelnetConnection.h"
#include <LibraryResources.h>
#include "TelnetConnection.g.cpp"
#include "../../types/inc/Utils.hpp"
using namespace ::Microsoft::Console;
constexpr std::wstring_view telnetScheme = L"telnet";
constexpr std::wstring_view msTelnetLoopbackScheme = L"ms-telnet-loop";
// {311153fb-d3f0-4ac6-b920-038de7cf5289}
static constexpr winrt::guid TelnetConnectionType = { 0x311153fb, 0xd3f0, 0x4ac6, { 0xb9, 0x20, 0x03, 0x8d, 0xe7, 0xcf, 0x52, 0x89 } };
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
winrt::guid TelnetConnection::ConnectionType() noexcept
{
return TelnetConnectionType;
}
TelnetConnection::TelnetConnection(const hstring& uri) :
_reader{ nullptr },
_writer{ nullptr },
_uri{ uri },
_receiveBuffer{}
{
_session.install(_nawsServer);
_nawsServer.activate([](auto&&) {});
}
// Method description:
// - ascribes to the ITerminalConnection interface
// - creates the output thread
void TelnetConnection::Start()
try
{
// Create our own output handling thread
// Each connection needs to make sure to drain the output from its backing host.
_hOutputThread.reset(CreateThread(
nullptr,
0,
[](LPVOID lpParameter) {
auto pInstance = static_cast<TelnetConnection*>(lpParameter);
if (pInstance)
{
return pInstance->_outputThread();
}
return gsl::narrow_cast<DWORD>(ERROR_BAD_ARGUMENTS);
},
this,
0,
nullptr));
THROW_LAST_ERROR_IF_NULL(_hOutputThread);
_transitionToState(ConnectionState::Connecting);
// Set initial window title.
_TerminalOutputHandlers(L"\x1b]0;Telnet\x7");
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
_transitionToState(ConnectionState::Failed);
}
// Method description:
// - ascribes to the ITerminalConnection interface
// - handles the different possible inputs in the different states
// Arguments:
// the user's input
void TelnetConnection::WriteInput(hstring const& data)
{
if (!_isStateOneOf(ConnectionState::Connected, ConnectionState::Connecting))
{
return;
}
auto str = winrt::to_string(data);
if (str.size() == 1 && str.at(0) == L'\r')
{
str = "\r\n";
}
#pragma warning(suppress : 26490) // Using something that isn't reinterpret_cast to forward stream bytes is more clumsy than just using it.
telnetpp::bytes bytes(reinterpret_cast<const uint8_t*>(str.data()), str.size());
_session.send(bytes, [=](telnetpp::bytes data) {
_socketSend(data);
});
}
// Method description:
// - ascribes to the ITerminalConnection interface
// - resizes the terminal
// Arguments:
// - the new rows/cols values
void TelnetConnection::Resize(uint32_t rows, uint32_t columns)
{
if (_prevResize.has_value() && _prevResize.value().first == rows && _prevResize.value().second == columns)
{
return;
}
_prevResize.emplace(std::pair{ rows, columns });
_nawsServer.set_window_size(gsl::narrow<uint16_t>(columns),
gsl::narrow<uint16_t>(rows),
[=](telnetpp::subnegotiation sub) {
_session.send(sub,
[=](telnetpp::bytes data) {
_socketBufferedSend(data);
});
_socketFlushBuffer();
});
}
// Method description:
// - ascribes to the ITerminalConnection interface
// - closes the socket connection and the output thread
void TelnetConnection::Close()
try
{
if (_transitionToState(ConnectionState::Closing))
{
_socket.Close();
if (_hOutputThread)
{
// Tear down our output thread
WaitForSingleObject(_hOutputThread.get(), INFINITE);
_hOutputThread.reset();
}
_transitionToState(ConnectionState::Closed);
}
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
_transitionToState(ConnectionState::Failed);
}
// Method description:
// - this is the output thread, where we initiate the connection to the remote host
// and establish a socket connection
// Return value:
// - return status
DWORD TelnetConnection::_outputThread()
try
{
while (true)
{
if (_isStateOneOf(ConnectionState::Failed))
{
_TerminalOutputHandlers(RS_(L"TelnetInternetOrServerIssue") + L"\r\n");
return E_FAIL;
}
else if (_isStateAtOrBeyond(ConnectionState::Closing))
{
return S_FALSE;
}
else if (_isStateOneOf(ConnectionState::Connecting))
{
try
{
const auto uri = Windows::Foundation::Uri(_uri);
const auto host = Windows::Networking::HostName(uri.Host());
bool autoLogin = false;
// If we specified the special ms loopback scheme, then set autologin and proceed below.
if (msTelnetLoopbackScheme == uri.SchemeName())
{
autoLogin = true;
}
// Otherwise, make sure we said telnet://, anything else is not supported here.
else if (telnetScheme != uri.SchemeName())
{
THROW_HR(E_INVALIDARG);
}
_socket.ConnectAsync(host, winrt::to_hstring(uri.Port())).get();
_writer = Windows::Storage::Streams::DataWriter(_socket.OutputStream());
_reader = Windows::Storage::Streams::DataReader(_socket.InputStream());
_reader.InputStreamOptions(Windows::Storage::Streams::InputStreamOptions::Partial); // returns when 1 or more bytes ready.
_transitionToState(ConnectionState::Connected);
if (autoLogin)
{
// Send newline to bypass User Name prompt.
const auto newline = winrt::to_hstring("\r\n");
WriteInput(newline);
// Wait for login.
Sleep(1000);
// Send "cls" enter to clear the thing and just look like a prompt.
const auto clearScreen = winrt::to_hstring("cls\r\n");
WriteInput(clearScreen);
}
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
_transitionToState(ConnectionState::Failed);
}
}
else if (_isStateOneOf(ConnectionState::Connected))
{
// Read from socket
const auto amountReceived = _socketReceive(_receiveBuffer);
_session.receive(
telnetpp::bytes{ _receiveBuffer.data(), amountReceived },
[=](telnetpp::bytes data,
std::function<void(telnetpp::bytes)> const& send) {
_applicationReceive(data, send);
},
[=](telnetpp::bytes data) {
_socketSend(data);
});
}
}
}
catch (...)
{
// If the exception was thrown while we were already supposed to be closing, fine. We're closed.
// This is because the socket got mad things were being torn down.
if (_isStateAtOrBeyond(ConnectionState::Closing))
{
_transitionToState(ConnectionState::Closed);
return S_OK;
}
else
{
LOG_CAUGHT_EXCEPTION();
_transitionToState(ConnectionState::Failed);
return E_FAIL;
}
}
// Routine Description:
// - Call to buffer up bytes to send to the remote device.
// - You must flush before they'll go out.
// Arguments:
// - data - View of bytes to be sent
// Return Value:
// - <none>
void TelnetConnection::_socketBufferedSend(telnetpp::bytes data)
{
// winrt::array_view should take data/size but it doesn't.
// We contacted the WinRT owners and they said, more or less, that it's not worth fixing
// with std::span on the horizon instead of this. So we're suppressing the warning
// and hoping for a std::span future that will eliminate winrt::array_view<T>
#pragma warning(push)
#pragma warning(disable : 26481)
const uint8_t* first = data.data();
const uint8_t* last = data.data() + data.size();
#pragma warning(pop)
const winrt::array_view<const uint8_t> arrayView(first, last);
_writer.WriteBytes(arrayView);
}
// Routine Description:
// - Flushes any buffered bytes to the underlying socket
// Arguments:
// - <none>
// Return Value:
// - <none>
fire_and_forget TelnetConnection::_socketFlushBuffer()
{
co_await _writer.StoreAsync();
}
// Routine Description:
// - Used to send bytes into the socket to the remote device
// Arguments:
// - data - View of bytes to be sent
// Return Value:
// - <none>
void TelnetConnection::_socketSend(telnetpp::bytes data)
{
_socketBufferedSend(data);
_socketFlushBuffer();
}
// Routine Description:
// - Reads bytes from the socket into the given array.
// Arguments:
// - buffer - The array of bytes to use for storage
// Return Value:
// - The number of bytes actually read (less than or equal to input array size)
size_t TelnetConnection::_socketReceive(gsl::span<telnetpp::byte> buffer)
{
const auto bytesLoaded = _reader.LoadAsync(gsl::narrow<uint32_t>(buffer.size())).get();
// winrt::array_view, despite having a pointer and size constructor
// hides it as protected.
// So we have to get first/last (even though cppcorechecks will be
// mad at us for it) to use a public array_view constructor.
// The WinRT team isn't fixing this because std::span is coming
// soon and that will do it.
#pragma warning(push)
#pragma warning(disable : 26481)
const auto first = buffer.data();
const auto last = first + bytesLoaded;
#pragma warning(pop)
const winrt::array_view<uint8_t> arrayView(first, last);
_reader.ReadBytes(arrayView);
return bytesLoaded;
}
// Routine Description:
// - Called by telnetpp framework when application data is received on the channel
// In contrast, telnet metadata payload is consumed by telnetpp and not forwarded to us.
// Arguments:
// - data - The relevant application-level payload received
// - send - A function where we can send a reply to given data immediately
// in reaction to the received message.
// Return Value:
// - <none>
void TelnetConnection::_applicationReceive(telnetpp::bytes data,
std::function<void(telnetpp::bytes)> const& /*send*/)
{
// Convert telnetpp bytes to standard string_view
#pragma warning(suppress : 26490) // Using something that isn't reinterpret_cast to forward stream bytes is more clumsy than just using it.
const auto stringView = std::string_view{ reinterpret_cast<const char*>(data.data()), gsl::narrow<size_t>(data.size()) };
// Convert to hstring
const auto hstr = winrt::to_hstring(stringView);
// Pass the output to our registered event handlers
_TerminalOutputHandlers(hstr);
}
}