1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 14:57:35 +00:00

TelnetServer: Implement basic telnet server

Fixes #407

Depends on #530 to run reliably.
This commit is contained in:
Conrad Pankoff 2019-09-08 17:51:28 +10:00 committed by Andreas Kling
parent 423807d772
commit 040947ee47
9 changed files with 521 additions and 0 deletions

View file

@ -101,6 +101,7 @@ cp ../Servers/SystemServer/SystemServer mnt/bin/SystemServer
cp ../Servers/WindowServer/WindowServer mnt/bin/WindowServer cp ../Servers/WindowServer/WindowServer mnt/bin/WindowServer
cp ../Servers/AudioServer/AudioServer mnt/bin/AudioServer cp ../Servers/AudioServer/AudioServer mnt/bin/AudioServer
cp ../Servers/TTYServer/TTYServer mnt/bin/TTYServer cp ../Servers/TTYServer/TTYServer mnt/bin/TTYServer
cp ../Servers/TelnetServer/TelnetServer mnt/bin/TelnetServer
cp ../Shell/Shell mnt/bin/Shell cp ../Shell/Shell mnt/bin/Shell
cp ../Libraries/LibHTML/tho mnt/bin/tho cp ../Libraries/LibHTML/tho mnt/bin/tho
echo "done" echo "done"

View file

@ -72,6 +72,7 @@ build_targets="$build_targets ../Games/Snake"
build_targets="$build_targets ../Servers/SystemServer" build_targets="$build_targets ../Servers/SystemServer"
build_targets="$build_targets ../Servers/TTYServer" build_targets="$build_targets ../Servers/TTYServer"
build_targets="$build_targets ../Servers/TelnetServer"
build_targets="$build_targets ../Servers/WindowServer" build_targets="$build_targets ../Servers/WindowServer"
build_targets="$build_targets ../Shell" build_targets="$build_targets ../Shell"

View file

@ -0,0 +1,162 @@
#include <AK/BufferStream.h>
#include <AK/ByteBuffer.h>
#include <AK/String.h>
#include <AK/StringBuilder.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include <LibCore/CNotifier.h>
#include <LibCore/CTCPSocket.h>
#include <stdio.h>
#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<Command> 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();
}

View file

@ -0,0 +1,43 @@
#pragma once
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include <LibCore/CNotifier.h>
#include <LibCore/CTCPSocket.h>
#include "Command.h"
#include "Parser.h"
class Client : public RefCounted<Client> {
public:
static NonnullRefPtr<Client> create(int id, CTCPSocket* socket, int ptm_fd)
{
return adopt(*new Client(id, socket, ptm_fd));
}
Function<void()> 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<Command> 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;
};

View file

@ -0,0 +1,56 @@
#pragma once
#include <AK/String.h>
#include <AK/StringBuilder.h>
#include <AK/Types.h>
#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();
};
};

View file

@ -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

View file

@ -0,0 +1,63 @@
#include <AK/Function.h>
#include <AK/String.h>
#include <AK/Types.h>
#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;
}
}
}

View file

@ -0,0 +1,33 @@
#pragma once
#include <AK/Function.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include "Command.h"
#define IAC 0xff
class Parser {
public:
Function<void(const Command&)> on_command;
Function<void(const StringView&)> on_data;
Function<void()> 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 };
};

View file

@ -0,0 +1,137 @@
#include <AK/BufferStream.h>
#include <AK/ByteBuffer.h>
#include <AK/HashMap.h>
#include <AK/IPv4Address.h>
#include <AK/String.h>
#include <AK/StringBuilder.h>
#include <AK/Types.h>
#include <LibCore/CEventLoop.h>
#include <LibCore/CTCPServer.h>
#include <LibCore/CTCPSocket.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <sys/ioctl.h>
#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<char**>(args), const_cast<char**>(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<int, NonnullRefPtr<Client>> 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;
}