Skip to content

Commit

Permalink
feat: Complete telnet client
Browse files Browse the repository at this point in the history
  • Loading branch information
michidk committed Dec 30, 2023
1 parent b7341d9 commit 2e9ace2
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 79 deletions.
225 changes: 160 additions & 65 deletions src/client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ const State = enum {
normal,
iac,
negotiating,
subnegotiating,
};

const StateInfo = union(State) {
normal: void,
iac: void,
negotiating: telnet.Command,
subnegotiating: telnet.Option,
};

pub const TelnetClient = struct {
Expand All @@ -35,146 +37,239 @@ pub const TelnetClient = struct {
};
}

pub fn write(self: *TelnetClient, data: []u8) anyerror!void {
std.log.debug("Writing {d} bytes", .{data.len});

// TODO: escape IAC bytes
try self.writer.writeAll(data);
}

pub fn read(self: *TelnetClient) anyerror!void {
const byte = try self.reader.readByte();

switch (self.state) {

// Normal state: print characters and wait for IAC byte
.normal => {
if (byte == telnet.IAC_BYTE) {
print("Server ({s}): IAC ", .{@tagName(self.state)});
self.state = .iac;
} else {
print("{c}", .{byte});
}
},

// Command state: determine command and set negotiating state
.iac => {
var cmd: telnet.Command = @enumFromInt(byte);
switch (cmd) {
.se => {
print("SE \n", .{});
self.state = .normal;
},
.nop => {
print("NOP \n", .{});
self.state = .normal;
},
.dm => {
print("DM \n", .{});
// Do nothing
std.log.debug("Recieved NOP", .{});
self.state = .normal;
},
.brk => {
print("BRK \n", .{});
.iac => {
// Escaped IAC byte
print("{c}", .{telnet.IAC_BYTE});
self.state = .normal;
},
.ip => {
print("IP \n", .{});
self.state = .normal;
},
.ao => {
print("AO \n", .{});
self.state = .normal;
},
.ayt => {
print("AYT \n", .{});
self.state = .normal;
.will, .wont, .do, .dont, .sb => {
self.state = StateInfo{ .negotiating = cmd };
},
.ec => {
print("EC \n", .{});
self.state = .normal;
},
.el => {
print("EL \n", .{});
self.state = .normal;
},
.ga => {
print("GA \n", .{});
.se => {
// Subnegotiation end
self.state = .normal;
},
.sb => {
print("SB \n", .{});
else => {
std.log.warn("Unhandled command: {s} (state: {s})", .{ @tagName(cmd), @tagName(self.state) });
self.state = .normal;
},
.will => {
print("WILL ", .{});
self.state = StateInfo{ .negotiating = .will };
},
.wont => {
print("WONT ", .{});
self.state = StateInfo{ .negotiating = .wont };
},
.do => {
print("DO ", .{});
self.state = StateInfo{ .negotiating = .do };
},
.dont => {
print("DONT ", .{});
self.state = StateInfo{ .negotiating = .dont };
},
}
},

// Negotiating state: determine option and send response
.negotiating => |command| {
const opt: telnet.Option = @enumFromInt(byte);
print("{s}\n", .{@tagName(opt)});
const option: telnet.Option = @enumFromInt(byte);
std.log.debug("S: {s} {s}", .{ @tagName(command), @tagName(option) });

switch (opt) {
switch (option) {
.echo => {
// https://datatracker.ietf.org/doc/html/rfc857
switch (command) {
.will => {
std.log.debug("Server wants to echo, we allow him", .{});
try self.send(.do, .echo);
},
.wont => {
std.log.debug("Server does not want to echo, we are fine with that", .{});
try self.send(.dont, .echo);
},
else => {},
.do => {
std.log.debug("Server asks us to echo, we decline", .{});
try self.send(.wont, .echo);
},
.dont => {
std.log.debug("Server asks us not to echo, we won't", .{});
try self.send(.wont, .echo);
},
else => {
std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) });
},
}
self.state = .normal;
},
.suppressGoAhead => {
// https://datatracker.ietf.org/doc/html/rfc858
switch (command) {
.will => {
std.log.debug("Server wants to suppress go ahead, we allow him", .{});
try self.send(.do, .suppressGoAhead);
},
.wont => {
try self.send(.dont, .suppressGoAhead);
std.log.warn("Server refused to suppress go ahead", .{});
try self.send(.do, .suppressGoAhead);
},
.do => {
std.log.debug("Server asks us to suppress go ahead, we accept", .{});
try self.send(.will, .suppressGoAhead);
},
.dont => {
std.log.warn("Server asks us not to suppress go ahead, we won't", .{});
try self.send(.wont, .suppressGoAhead);
},
else => {
std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) });
},
else => {},
}
self.state = .normal;
},
.negotiateAboutWindowSize => {
// https://datatracker.ietf.org/doc/html/rfc1073
switch (command) {
.do => {
std.log.debug("Server wants to negotiate about window size, we send the info", .{});
try self.send(.will, .negotiateAboutWindowSize);
try self.writer.writeAll(&[_]u8{ telnet.IAC_BYTE, telnet.SB_BYTE, @intFromEnum(Option.negotiateAboutWindowSize), 0, 80, 0, 24, telnet.IAC_BYTE, telnet.SE_BYTE });

// TODO: get the correct width and height from the terminal
const windowSizeData = &[_]u8{
0, 80, // Width
0, 24, // Height
};
const negotiation = &telnet.subnegotiate(Option.negotiateAboutWindowSize, windowSizeData);
try self.writer.writeAll(negotiation);
},
.dont => {
std.log.debug("Server does not want to negotiate about window size, we accept", .{});
try self.send(.wont, .negotiateAboutWindowSize);
},
else => {},
else => {
std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) });
},
}
self.state = .normal;
},
.terminalType => {
// https://datatracker.ietf.org/doc/html/rfc1091
switch (command) {
.do => {
try self.send(.wont, .terminalType);
try self.writer.writeAll(&[_]u8{ telnet.IAC_BYTE, telnet.SB_BYTE, @intFromEnum(Option.terminalType), 0, 0, 0, 0, 0, 0, 0, 0, 0, telnet.IAC_BYTE, telnet.SE_BYTE });
std.log.debug("Server wants to ask us for our terminal type, we agree", .{});
try self.send(.will, .terminalType);

self.state = .normal;
},
.dont => {
std.log.debug("Server does not want to know our terminal type", .{});
try self.send(.wont, .terminalType);

self.state = .normal;
},
.sb => {
std.log.debug("Server wants to know our terminal type...", .{});

self.state = StateInfo{
.subnegotiating = Option.terminalType,
};
},
else => {
std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) });
},
}
},
.transmitBinary => {
// https://datatracker.ietf.org/doc/html/rfc856
switch (command) {
.do => {
std.log.debug("Server wants to transmit binary, we agree", .{});
try self.send(.will, .transmitBinary);
},
.dont => {
std.log.debug("Server does not want to transmit binary, we agree", .{});
try self.send(.wont, .transmitBinary);
},
.will => {
std.log.debug("Server wants us to transmit binary, we agree", .{});
try self.send(.do, .transmitBinary);
},
.wont => {
std.log.debug("Server does not want us to transmit binary, we agree", .{});
try self.send(.dont, .transmitBinary);
},
else => {
std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) });
},
else => {},
}
},
else => {},
else => {
switch (command) {
.do => {
std.log.debug("Server wants us to perform subcommand `{s}`, we refuse", .{@tagName(option)});
try self.send(.wont, option);
},
.dont => {
std.log.debug("Server does not want us to perform subcommand `{s}`, we refuse", .{@tagName(option)});
},
.will, .wont => {
std.log.warn("Server wants to negotiate option `{s}`", .{@tagName(option)});
},
else => {
std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) });
},
}
self.state = .normal;
},
}
},

self.state = .normal;
// Subnegotiating state: determine option and read until IAC SE
.subnegotiating => |option| {
std.log.debug("Subnegotiating option `{s}`", .{@tagName(option)});
switch (option) {
.terminalType => {
if (byte == telnet.SEND_BYTE) {
std.log.debug("Send terminal type", .{});
const terminalTypeData: []const u8 = &[_]u8{
telnet.IS_BYTE, // Is
'X', 'T', 'E', 'R', 'M', '-', '2', '5', '6', 'C', 'O', 'L', 'O', 'R', // Terminal type (`XTERM-256COLOR` is what the inetutils implementation sends)
};
const negotiation: []const u8 = &telnet.subnegotiate(Option.terminalType, terminalTypeData);
try self.writer.writeAll(negotiation);

self.state = .normal;
} else {
std.log.warn("Unsupported data byte {d} during subnegotiation option `{c}` (state: {s}),", .{ byte, @tagName(option), @tagName(self.state) });
self.state = .normal;
}
},
else => {
std.log.warn("Unsupported subnegotiation option `{s}` (state: {s})", .{ @tagName(option), @tagName(self.state) });
self.state = .normal;
},
}
},
}
}

fn send(self: *TelnetClient, command: Command, option: Option) anyerror!void {
print("Client ({s}): IAC {s} {s}\n", .{ @tagName(self.state), @tagName(command), @tagName(option) });
std.log.debug("C: {s} {s}", .{ @tagName(command), @tagName(option) });
try self.writer.writeAll(&telnet.instruction(command, option));
}
};
32 changes: 28 additions & 4 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ const client = @import("client.zig");
const opts = @import("opts.zig");
const clap = @import("clap");

const Errors = error{
MissingArgument,
InvalidUri,
pub const std_options = struct {
pub const log_level = .debug; // Set this to `.warn` to disable all debug info
};

pub fn main() !void {
// Setup allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
Expand All @@ -22,13 +20,39 @@ pub fn main() !void {
const parsedArgs = try opts.parse(alloc);
if (parsedArgs) |args| {
defer args.deinit();

// Commenct to the server
std.log.info("Connecting to {?s}:{?d}\n", .{ args.uri.host, args.uri.port });
const stream = try net.tcpConnectToHost(alloc, args.uri.host.?, args.uri.port.?);
defer stream.close();

var tnClient = client.TelnetClient.init(stream);

// Start the input thread
std.log.info("Press CTL-C to exit.", .{});
const handle = try std.Thread.spawn(.{}, readInput, .{&tnClient});
handle.detach();

while (true) {
try tnClient.read();
}
}
}

fn readInput(tnClient: *client.TelnetClient) !void {
// Read from stdin and write to the telnet client

// Sadly, this does not work, since data is only read once enter is pressed
// Normaly, telnet would send a key press as soon as it is pressed
// But for this to work, we would need to use a terminal library
// Also our enter would have to be translated from \n to \r\n

const stdin = std.io.getStdIn().reader();
while (true) {
var buf: [64]u8 = undefined;
const len = try stdin.read(&buf);
if (len > 0) {
try tnClient.write(buf[0..len]);
}
}
}
5 changes: 5 additions & 0 deletions src/opts.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ const utils = @import("utils.zig");
const telnet = @import("telnet.zig");
const io = std.io;

const OptErrors = error{
MissingArgument,
InvalidUri,
};

pub const Opts = struct {
alloc: std.mem.Allocator,
uriStr: []const u8,
Expand Down
Loading

0 comments on commit 2e9ace2

Please sign in to comment.