From 040947ee47270fa45a046f90bd4d4498b1c0ee91 Mon Sep 17 00:00:00 2001 From: Conrad Pankoff Date: Sun, 8 Sep 2019 17:51:28 +1000 Subject: TelnetServer: Implement basic telnet server Fixes #407 Depends on #530 to run reliably. --- Servers/TelnetServer/Client.cpp | 162 ++++++++++++++++++++++++++++++++++++++++ Servers/TelnetServer/Client.h | 43 +++++++++++ Servers/TelnetServer/Command.h | 56 ++++++++++++++ Servers/TelnetServer/Makefile | 25 +++++++ Servers/TelnetServer/Parser.cpp | 63 ++++++++++++++++ Servers/TelnetServer/Parser.h | 33 ++++++++ Servers/TelnetServer/main.cpp | 137 +++++++++++++++++++++++++++++++++ 7 files changed, 519 insertions(+) create mode 100644 Servers/TelnetServer/Client.cpp create mode 100644 Servers/TelnetServer/Client.h create mode 100644 Servers/TelnetServer/Command.h create mode 100644 Servers/TelnetServer/Makefile create mode 100644 Servers/TelnetServer/Parser.cpp create mode 100644 Servers/TelnetServer/Parser.h create mode 100644 Servers/TelnetServer/main.cpp (limited to 'Servers/TelnetServer') diff --git a/Servers/TelnetServer/Client.cpp b/Servers/TelnetServer/Client.cpp new file mode 100644 index 0000000000..485bb23134 --- /dev/null +++ b/Servers/TelnetServer/Client.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Client.h" + +Client::Client(int id, CTCPSocket* socket, int ptm_fd) + : m_id(id) + , m_socket(socket) + , m_ptm_fd(ptm_fd) + , m_ptm_notifier(ptm_fd, CNotifier::Read) +{ + m_socket->on_ready_to_read = [this] { drain_socket(); }; + m_ptm_notifier.on_ready_to_read = [this] { drain_pty(); }; + m_parser.on_command = [this](const Command& command) { handle_command(command); }; + m_parser.on_data = [this](const StringView& data) { handle_data(data); }; + m_parser.on_error = [this]() { handle_error(); }; + send_commands({ + { CMD_WILL, SUB_SUPPRESS_GO_AHEAD }, + { CMD_WILL, SUB_ECHO }, + { CMD_DO, SUB_SUPPRESS_GO_AHEAD }, + { CMD_DONT, SUB_ECHO }, + }); +} + +void Client::drain_socket() +{ + while (m_socket->can_read()) { + auto buf = m_socket->read(1024); + + m_parser.write(buf); + + if (m_socket->eof()) { + quit(); + break; + } + } +} + +void Client::drain_pty() +{ + u8 buffer[BUFSIZ]; + ssize_t nread = read(m_ptm_fd, buffer, sizeof(buffer)); + if (nread < 0) { + perror("read(ptm)"); + quit(); + return; + } + if (nread == 0) { + quit(); + return; + } + send_data(StringView(buffer, nread)); +} + +void Client::handle_data(const StringView& data) +{ + write(m_ptm_fd, data.characters_without_null_termination(), data.length()); +} + +void Client::handle_command(const Command& command) +{ + switch (command.command) { + case CMD_DO: + // no response - we've already advertised our options, and none of + // them can be disabled (or re-enabled) after connecting. + break; + case CMD_DONT: + // no response - we only "support" two options (echo and suppres + // go-ahead), and both of them are always enabled. + break; + case CMD_WILL: + switch (command.subcommand) { + case SUB_ECHO: + // we always want to be the ones in control of the output. tell + // the client to disable local echo. + send_command({ CMD_DONT, SUB_ECHO }); + break; + case SUB_SUPPRESS_GO_AHEAD: + send_command({ CMD_DO, SUB_SUPPRESS_GO_AHEAD }); + break; + default: + // don't respond to unknown commands + break; + } + break; + case CMD_WONT: + // no response - we don't care about anything the client says they + // won't do. + break; + } +} + +void Client::handle_error() +{ + quit(); +} + +void Client::send_data(StringView data) +{ + bool fast = true; + for (int i = 0; i < data.length(); i++) { + u8 c = data[i]; + if (c == '\n' || c == 0xff) + fast = false; + } + + if (fast) { + m_socket->write(data); + return; + } + + StringBuilder builder; + for (int i = 0; i < data.length(); i++) { + u8 c = data[i]; + + switch (c) { + case '\n': + builder.append("\r\n"); + break; + case IAC: + builder.append("\xff\xff"); + break; + default: + builder.append(c); + break; + } + } + + m_socket->write(builder.to_string()); +} + + +void Client::send_command(Command command) +{ + send_commands({ command }); +} + +void Client::send_commands(Vector commands) +{ + auto buffer = ByteBuffer::create_uninitialized(commands.size() * 3); + BufferStream stream(buffer); + for (auto& command : commands) + stream << (u8)IAC << command.command << command.subcommand; + stream.snip(); + m_socket->write(buffer.pointer(), buffer.size()); +} + +void Client::quit() +{ + m_ptm_notifier.set_enabled(false); + close(m_ptm_fd); + m_socket->close(); + if (on_exit) + on_exit(); +} diff --git a/Servers/TelnetServer/Client.h b/Servers/TelnetServer/Client.h new file mode 100644 index 0000000000..83956f8879 --- /dev/null +++ b/Servers/TelnetServer/Client.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Command.h" +#include "Parser.h" + +class Client : public RefCounted { +public: + static NonnullRefPtr create(int id, CTCPSocket* socket, int ptm_fd) + { + return adopt(*new Client(id, socket, ptm_fd)); + } + + Function on_exit; + +protected: + Client(int id, CTCPSocket* socket, int ptm_fd); + + void drain_socket(); + void drain_pty(); + void handle_data(const StringView&); + void handle_command(const Command& command); + void handle_error(); + void send_data(StringView str); + void send_command(Command command); + void send_commands(Vector commands); + void quit(); + +private: + // client id + int m_id { 0 }; + // client resources + CTCPSocket* m_socket { nullptr }; + Parser m_parser; + // pty resources + int m_ptm_fd { -1 }; + CNotifier m_ptm_notifier; +}; diff --git a/Servers/TelnetServer/Command.h b/Servers/TelnetServer/Command.h new file mode 100644 index 0000000000..bfe210e724 --- /dev/null +++ b/Servers/TelnetServer/Command.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +#define CMD_WILL 0xfb +#define CMD_WONT 0xfc +#define CMD_DO 0xfd +#define CMD_DONT 0xfe +#define SUB_ECHO 0x01 +#define SUB_SUPPRESS_GO_AHEAD 0x03 + +struct Command { + u8 command; + u8 subcommand; + + String to_string() const + { + StringBuilder builder; + + switch (command) { + case CMD_WILL: + builder.append("WILL"); + break; + case CMD_WONT: + builder.append("WONT"); + break; + case CMD_DO: + builder.append("DO"); + break; + case CMD_DONT: + builder.append("DONT"); + break; + default: + builder.append(String::format("UNKNOWN<%02x>", command)); + break; + } + + builder.append(" "); + + switch (subcommand) { + case SUB_ECHO: + builder.append("ECHO"); + break; + case SUB_SUPPRESS_GO_AHEAD: + builder.append("SUPPRESS_GO_AHEAD"); + break; + default: + builder.append(String::format("UNKNOWN<%02x>")); + break; + } + + return builder.to_string(); + }; +}; diff --git a/Servers/TelnetServer/Makefile b/Servers/TelnetServer/Makefile new file mode 100644 index 0000000000..ab0373e705 --- /dev/null +++ b/Servers/TelnetServer/Makefile @@ -0,0 +1,25 @@ +include ../../Makefile.common + +ECHOSERVER_OBJS = \ + Client.o \ + Parser.o \ + main.o + +APP = TelnetServer +OBJS = $(ECHOSERVER_OBJS) + +DEFINES += -DUSERLAND + +all: $(APP) + +$(APP): $(OBJS) + $(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lc -lcore + +.cpp.o: + @echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $< + +-include $(OBJS:%.o=%.d) + +clean: + @echo "CLEAN"; rm -f $(APP) $(OBJS) *.d + diff --git a/Servers/TelnetServer/Parser.cpp b/Servers/TelnetServer/Parser.cpp new file mode 100644 index 0000000000..6c05ce7e2e --- /dev/null +++ b/Servers/TelnetServer/Parser.cpp @@ -0,0 +1,63 @@ +#include +#include +#include + +#include "Parser.h" + +void Parser::write(const StringView& data) +{ + for (int i = 0; i < data.length(); i++) { + u8 ch = data[i]; + + switch (m_state) { + case State::Free: + switch (ch) { + case IAC: + m_state = State::ReadCommand; + break; + case '\r': + if (on_data) + on_data("\n"); + break; + default: + if (on_data) + on_data(StringView(&ch, 1)); + break; + } + break; + case State::ReadCommand: + switch (ch) { + case IAC: { + m_state = State::Free; + if (on_data) + on_data("\xff"); + break; + } + case CMD_WILL: + case CMD_WONT: + case CMD_DO: + case CMD_DONT: + m_command = ch; + m_state = State::ReadSubcommand; + break; + default: + m_state = State::Error; + if (on_error) + on_error(); + break; + } + break; + case State::ReadSubcommand: { + auto command = m_command; + m_command = 0; + m_state = State::Free; + if (on_command) + on_command({ command, ch }); + break; + } + case State::Error: + // ignore everything + break; + } + } +} diff --git a/Servers/TelnetServer/Parser.h b/Servers/TelnetServer/Parser.h new file mode 100644 index 0000000000..24700a0149 --- /dev/null +++ b/Servers/TelnetServer/Parser.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +#include "Command.h" + +#define IAC 0xff + +class Parser { +public: + Function on_command; + Function on_data; + Function on_error; + + void write(const StringView&); + +protected: + enum State { + Free, + ReadCommand, + ReadSubcommand, + Error, + }; + + void write(const String& str); + +private: + State m_state { State::Free }; + u8 m_command { 0 }; +}; diff --git a/Servers/TelnetServer/main.cpp b/Servers/TelnetServer/main.cpp new file mode 100644 index 0000000000..d8cfd364c8 --- /dev/null +++ b/Servers/TelnetServer/main.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Client.h" + +static void run_command(int ptm_fd, String command) +{ + pid_t pid = fork(); + if (pid == 0) { + const char* tty_name = ptsname(ptm_fd); + if (!tty_name) { + perror("ptsname"); + exit(1); + } + close(ptm_fd); + int pts_fd = open(tty_name, O_RDWR); + if (pts_fd < 0) { + perror("open"); + exit(1); + } + + // NOTE: It's okay if this fails. + (void)ioctl(0, TIOCNOTTY); + + close(0); + close(1); + close(2); + + int rc = dup2(pts_fd, 0); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = dup2(pts_fd, 1); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = dup2(pts_fd, 2); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = close(pts_fd); + if (rc < 0) { + perror("close"); + exit(1); + } + rc = ioctl(0, TIOCSCTTY); + if (rc < 0) { + perror("ioctl(TIOCSCTTY)"); + exit(1); + } + const char* args[4] = { "/bin/Shell", nullptr, nullptr, nullptr }; + if (!command.is_empty()) { + args[1] = "-c"; + args[2] = command.characters(); + } + const char* envs[] = { "TERM=xterm", "PATH=/bin:/usr/bin:/usr/local/bin", nullptr }; + rc = execve("/bin/Shell", const_cast(args), const_cast(envs)); + if (rc < 0) { + perror("execve"); + exit(1); + } + ASSERT_NOT_REACHED(); + } +} + +int main(int argc, char** argv) +{ + CEventLoop event_loop; + CTCPServer server; + + int opt; + u16 port = 23; + while ((opt = getopt(argc, argv, "p:")) != -1) { + switch (opt) { + case 'p': + port = atoi(optarg); + break; + default: + fprintf(stderr, "Usage: %s [-p port]", argv[0]); + exit(1); + } + } + + if (!server.listen({}, port)) { + perror("listen"); + exit(1); + } + + HashMap> clients; + int next_id = 0; + + server.on_ready_to_accept = [&next_id, &clients, &server] { + int id = next_id++; + + auto* client_socket = server.accept(); + if (!client_socket) { + perror("accept"); + return; + } + + int ptm_fd = open("/dev/ptmx", O_RDWR); + if (ptm_fd < 0) { + perror("open(ptmx)"); + client_socket->close(); + return; + } + + run_command(ptm_fd, ""); + + auto client = Client::create(id, client_socket, ptm_fd); + client->on_exit = [&clients, id] { clients.remove(id); }; + clients.set(id, client); + }; + + int rc = event_loop.exec(); + if (rc != 0) { + fprintf(stderr, "event loop exited badly; rc=%d", rc); + exit(1); + } + + return 0; +} -- cgit v1.2.3