ZealOS/src/Home/Telnet/Telnet.ZC
y4my4my4m 1e68c497f2 Fix
2023-05-13 15:44:03 +09:00

834 lines
26 KiB
HolyC

// Telnet client for ZealOS by y4my4m
// Public Domain
Cd(__DIR__);;
// PaletteSet("Temple");
#define TELNET_PORT 23
#define BUF_SIZE 8192 // way too big?
#define INPUT_BUF_SIZE 32
#define TIMEOUT_DURATION 500000
#define NEGOTIATE 0xFF
#define ANSI_ESC 0x1B
#define ANSI_CSI 0x5B // [
#define MAX_ANSI_PARAMS 32
#include "TelnetNegotiation"
#include "TelnetHelpers"
Bool force_disconnect = FALSE;
class Terminal {
I64 sock;
Bool sock_ready;
I64 window_width;
I64 window_height;
CDoc *doc;
CTask *task;
I64 current_color;
I64 current_bgcolor;
I64 cursor_x;
I64 cursor_y;
U8 buffer[BUF_SIZE];
I64 buffer_len;
} term;
I64 TelnetOpen(U8 *host, U16 port) {
I64 sock;
if (host == NULL) {
return -1;
}
sock = TCPConnectionCreate(host, port);
"$$GREEN$$Connecting to %s:%d.$$FG$$$$BG$$\n", host, port;
if (sock <= 0) {
PrintErr("Failed to connect to %s:%d\n", host, port);
return sock;
}
// sock(CTCPSocket *)->timeout = 0;
// sock(CTCPSocket *)->timeout = TCP_TIMEOUT;
return sock;
}
U0 HandleControlCodes(U8 ch) {
if (ch < 32) { // ASCII code below 32 (control character)
switch (ch) {
case 0: // NUL (Null) - Typically ignored
break;
case 7: // BEL (Bell)
Beep;
break;
case 8: // BS (Backspace)
// "%c%c%c", 8, ' ', 8; // Move cursor back, erase character, move cursor back again
DocPrint(term.doc, "$$CM,-8,0$$");
break;
case 9: // HT (Horizontal Tab)
// " "; // 8 spaces
DocPrint(term.doc, "$$CM,8,0$$");
break;
case 10: // LF (Line Feed)
// DocPrint(term.doc, "\n");
break;
case 11: // VT (Vertical Tab)
SysLog("Vertical Tab\n");
break;
case 12: // FF (Form Feed)
DocClear(term.doc);
break;
case 13: // CR (Carriage Return)
DocPrint(term.doc, "\r");
break;
case 14: // SO (Shift Out) - Switch to an alternate character set
case 15: // SI (Shift In) - Switch back to the default character set
SysLog("Shift In/Out\n");
break;
case 22:
SysLog("Synchronous Idle\n");
break;
case 23:
SysLog("End of Transmission Block\n");
break;
case 24:
SysLog("Cancel\n");
break;
case 25:
SysLog("End of Medium\n");
break;
case 26:
SysLog("Sub\n");
break;
case 27:
SysLog("Esc\n");
break;
case 28:
SysLog("Fs\n");
break;
case 29:
SysLog("Gs\n");
break;
case 30:
SysLog("Rs\n");
break;
case 31:
SysLog("Unit Separator\n");
break;
default:
// some ch make Zeal crash or behave weird because they're commands?
// SysLog("CC %c happened\n", ch);
SysLog("CC happened\n");
break;
}
}
else {
if (ch == 127) {
SysLog("case 127");
}
if (ch == 0x24) {
DocPrint(term.doc, "%s", "//$$$$");
}
if (ch >= 32 && ch < 256) // ZealOS's ASCII is up to 255
{
DocPrint(term.doc, "%c", ch);
}
else {
DocPrint(term.doc, "%c", '?'); // unrecognized character
}
}
}
I64 LoadSplashScreen(U8 *filename) {
CFile *file = FOpen(filename, "rb");
if (!file) {
PrintErr("Failed to open file");
return -1;
}
// Ensure that the file size isn't larger than the buffer
if (file->de.size > BUF_SIZE) {
PrintErr("File is too large for the buffer.");
FClose(file);
return -1;
}
// Calculate the number of full blocks to read based on file size and block size
I64 full_blocks = file->de.size / BLK_SIZE;
I64 remaining_bytes = file->de.size % BLK_SIZE;
SysLog("File size: %d, Number of full blocks: %d, Remaining bytes: %d\n", file->de.size, full_blocks, remaining_bytes);
// Read the full blocks into the buffer
I64 i, blocks_read = 0;
for (i = 0; i < full_blocks; i++) {
blocks_read += FBlkRead(file, term.buffer + i * BLK_SIZE, i, 1);
}
// Check if there are any remaining bytes in the last block
if (remaining_bytes != 0) {
// Read the remaining bytes
U8 temp_buffer[BLK_SIZE];
if (FBlkRead(file, temp_buffer, full_blocks, 1)) {
blocks_read++;
MemCopy(term.buffer + full_blocks * BLK_SIZE, temp_buffer, remaining_bytes);
}
}
FClose(file);
if (blocks_read != (full_blocks + (remaining_bytes != 0))) {
PrintErr("Failed to read all the blocks");
return -1;
}
return file->de.size; // Return the number of bytes read
}
U0 ANSIParse()
{
// Basic Telnet protocol parser
U8 *ptr = term.buffer;
while (ptr < term.buffer + term.buffer_len) {
// disable all SAUCE00 art signature? dsnt work
// if (StrNCompare(ptr, "\033SAUCE", 6) == 0)
// {
// SysLog("SAUCE found\n");
// term.buffer_len = ptr - term.buffer;
// }
// Telnet negotiation sequence
if (*ptr == NEGOTIATE) {
// FIXME: i don't think the telnet negotiation is actually working properly?
TelnetNegotiate(term.sock, ptr);
ptr += 3;
}
else if (*ptr == ANSI_ESC) {
// ANSI escape sequence
ptr++;
if (*ptr == ANSI_CSI) {
ptr++;
I64 ansi_code[MAX_ANSI_PARAMS], counter;
for (counter = 0; counter < MAX_ANSI_PARAMS; counter++) {
ansi_code[counter] = 0; // Initialize all elements to 0
}
I64 ansi_param_count = 0;
while (IsDigit(*ptr) || *ptr == ';') {
if (IsDigit(*ptr)) {
ansi_code[ansi_param_count] = ansi_code[ansi_param_count] * 10 + (*ptr - '0');
ptr++;
}
else if (*ptr == ';') {
ansi_param_count++;
if (ansi_param_count >= MAX_ANSI_PARAMS) {
// Error handling: too many parameters
break;
}
ptr++;
if(!IsDigit(*ptr) || *ptr == ';'){
break;
}
}
}
// Handle specific ANSI escape sequences
switch (*ptr) {
case 'n':
SysLog("Case n, %d\n",ansi_code[0]);
if (ansi_code[0] == 5) {
// Respond with terminal readiness
SysLog("reported terminal readiness\n");
U8 deviceStatusResponse[5];
deviceStatusResponse[0] = ANSI_ESC;
deviceStatusResponse[1] = ANSI_CSI;
deviceStatusResponse[2] = 0x30; // '0'
deviceStatusResponse[3] = 0x6E; // 'n'
deviceStatusResponse[4] = 0x00; // Null-terminator
TCPSocketSend(term.sock, deviceStatusResponse, 4);
// TCPSocketSendString(term.sock, "\x1B[0n");
}
else if (ansi_code[0] == 6) {
// Respond with cursor position
// U8 response[32] = "\x1B[%d;%dR", window_width, window_height;
SysLog("reported cursor position\n");
U8 cursorResponse[8];
cursorResponse[0] = ANSI_ESC;
cursorResponse[1] = ANSI_CSI;
cursorResponse[2] = 0x32;
cursorResponse[3] = 0x35;
cursorResponse[4] = 0x3B;
cursorResponse[5] = 0x38;
cursorResponse[6] = 0x30;
cursorResponse[6] = 0x52;
cursorResponse[7] = 0x00;
TCPSocketSend(term.sock, cursorResponse, 7);
// TCPSocketSendString(term.sock, "\x1B\[25;80R");
}
else if (ansi_code[0] == 255) {
// https://github.com/NuSkooler/enigma-bbs/blob/97cd0c3063b0c9f93a0fa4a44a85318ca81aef43/core/ansi_term.js#L140
SysLog("reported screensize?\n");
SendWindowSize(term.sock, 25, 80);
}
ptr++;
break;
case 'c':
// Respond with device attributes
SysLog("reported device attributes\n");
// TCPSocketSendString(term.sock, "\x1B[?1;0c");
// Reports at VT101 (not sure why though)
U8 deviceAttributesResponse[8];
deviceAttributesResponse[0] = ANSI_ESC;
deviceAttributesResponse[1] = ANSI_CSI;
deviceAttributesResponse[2] = 0x3F; // '?'
deviceAttributesResponse[3] = 0x31; // '1'
deviceAttributesResponse[4] = 0x3B; // ';'
deviceAttributesResponse[5] = 0x32; // '0'
deviceAttributesResponse[6] = 0x63; // 'c'
deviceAttributesResponse[7] = 0x00; // Null-terminator
TCPSocketSend(term.sock, deviceAttributesResponse, 7);
ptr++;
break;
case 'm':
// colors might be printed in the wrong order?
// like, <Esc>[1;40m and now <Esc>[40m;1m
I64 m;
Bool isBright = FALSE;
for (m = 0; m <= ansi_param_count; m++) {
if (ansi_code[m] <= 10) {
switch (ansi_code[m]) {
case 0:
DocPrint(term.doc, "$$BG$$$$FG$$");
isBright = FALSE;
break; // reset
case 1: isBright = TRUE; break;
case 2: isBright = FALSE; break;
}
}
else if ((ansi_code[m] >= 30 && ansi_code[m] <= 39) || (ansi_code[m] >= 90 && ansi_code[m] <= 97)) {
// Set foreground color
// SysLog("ansi_code[%d] = %d\n", m, ansi_code[m]);
if(!isBright){
switch (ansi_code[m]) {
case 30:
DocPrint(term.doc, "$$BLACK$$");
break;
case 31:
DocPrint(term.doc, "$$RED$$");
break;
case 32:
DocPrint(term.doc, "$$GREEN$$");
break;
case 33:
DocPrint(term.doc, "$$YELLOW$$");
break;
case 34:
DocPrint(term.doc, "$$BLUE$$");
break;
case 35:
DocPrint(term.doc, "$$PURPLE$$");
break;
case 36:
DocPrint(term.doc, "$$CYAN$$");
break;
case 37:
DocPrint(term.doc, "$$WHITE$$");
break;
case 39:
DocPrint(term.doc, "$$FG$$");
break;
default: break;
}
}
else {
switch (ansi_code[m]) {
case 90:
case 30:
DocPrint(term.doc, "$$DKGRAY$$");
break;
case 91:
case 31:
DocPrint(term.doc, "$$LTRED$$");
break;
case 92:
case 32:
DocPrint(term.doc, "$$LTGREEN$$");
break;
case 93:
case 33:
DocPrint(term.doc, "$$YELLOW$$");
break;
case 94:
case 34:
DocPrint(term.doc, "$$LTBLUE$$");
break;
case 95:
case 35:
DocPrint(term.doc, "$$LTPURPLE$$");
break;
case 96:
case 36:
DocPrint(term.doc, "$$LTCYAN$$");
break;
case 97:
case 37:
DocPrint(term.doc, "$$LTGRAY$$");
break;
case 39:
DocPrint(term.doc, "$$FG$$");
break;
default: break;
}
}
}
// this is a dumb approach, just do a CatPrint or something
// until we properly catch the `;` it will stay buggy
else if ((ansi_code[m] >= 40 && ansi_code[m] <= 49) || (ansi_code[m] >= 100 && ansi_code[m] <= 107)) {
// Set background color
// SysLog("ansi_code[%d] = %d\n", m, ansi_code[m]);
if(!isBright){
switch (ansi_code[m]) {
case 40:
DocPrint(term.doc,"$$BG,BLACK$$");
break;
case 41:
DocPrint(term.doc,"$$BG,RED$$");
break;
case 42:
DocPrint(term.doc,"$$BG,GREEN$$");
break;
case 43:
DocPrint(term.doc,"$$BG,YELLOW$$");
break;
case 44:
DocPrint(term.doc,"$$BG,BLUE$$");
break;
case 45:
DocPrint(term.doc,"$$BG,PURPLE$$");
break;
case 46:
DocPrint(term.doc,"$$BG,CYAN$$");
break;
case 47:
DocPrint(term.doc,"$$BG,WHITE$$");
break;
case 49:
DocPrint(term.doc,"$$BG$$");
break;
default: break;
}
}
else {
switch (ansi_code[m]) {
case 100:
case 40:
DocPrint(term.doc,"$$BG,DKGRAY$$");
break;
case 101:
case 41:
DocPrint(term.doc,"$$BG,LTRED$$");
break;
case 102:
case 42:
DocPrint(term.doc,"$$BG,LTGREEN$$");
break;
case 103:
case 43:
DocPrint(term.doc,"$$BG,YELLOW$$");
break;
case 104:
case 44:
DocPrint(term.doc,"$$BG,LTBLUE$$");
break;
case 105:
case 45:
DocPrint(term.doc,"$$BG,LTPURPLE$$");
break;
case 106:
case 46:
DocPrint(term.doc,"$$BG,LTCYAN$$");
break;
case 107:
case 47:
DocPrint(term.doc,"$$BG,LTGRAY$$");
break;
case 49:
DocPrint(term.doc,"$$BG$$");
break;
// reset
default: break;
}
}
}
}
ptr++;
break;
case 'A':
// Cursor Up
SysLog("Cursor Up\n");
// "$$CM+TY,0,-%d$$", ansi_code[0];
DocPrint(term.doc, "$$CM,0,-%d$$", ansi_code[0]);
ptr++;
break;
case 'B':
// Cursor Down
SysLog("Cursor Down\n");
DocPrint(term.doc, "$$CM,0,%d$$", ansi_code[0]);
ptr++;
break;
case 'C':
// Cursor Right
// SysLog("Cursor Right %d %d\n", ansi_param_count, ansi_code[0]);
DocPrint(term.doc, "$$CM,%d,0$$", ansi_code[0]);
// NOTE: this has been "fixed" since we now change the window's background color
// if we just move the cursor,
// you dont get the colored background since we skip over it directly
// I64 C;
// for (C = 0; C < ansi_code[0]; C++) {
// " ";
// }
ptr++;
break;
case 'D':
// Cursor Left
SysLog("Cursor Left\n");
DocPrint(term.doc, "$$CM,-%d,0$$", ansi_code[0]);
ptr++;
break;
case 'E':
// Cursor Next Line
SysLog("Cursor Next Line\n");
// "$$CM+TY,0,+%d$$", ansi_code[0];
DocPrint(term.doc, "\n");
ptr++;
break;
case 'F':
// Cursor Previous Line
SysLog("Cursor Previous Line\n");
DocPrint(term.doc, "$$CM+LY,0,-%d$$", ansi_code[0]);
// "\n";
ptr++;
break;
case 'G':
// Cursor Horizontal Absolute
SysLog("Cursor Horizontal Absolute\n");
DocPrint(term.doc, "$$CM,%d,0$$", ansi_code[0]);
// "\n";
ptr++;
break;
case 'H':
case 'f':
I64 row = 1, col = 1; // default values
// Parse the row number
if(ansi_code[0] != 1)
row = ansi_code[0];
if(ansi_code[1] != 1)
col = ansi_code[1];
// TODO: This is a hack, dont skip row 0, col 0 (maybe?)
if (row == 0 && col == 0) {
ptr++;
break;
}
// SysLog("H or f AFTER row:%d, col:%d, cnt:%d\n", row, col, ansi_param_count);
if (row > term.window_height)
row = term.window_height-1;
if (col > term.window_width)
col = term.window_width-1;
// "$$CM,0,0$$";
DocPrint(term.doc, "$$CM+LX+TY,LE=%d,RE=%d$$", col-1, row-1);
ptr++;
break;
case 'J':
// SysLog("J code, %d %d\n", ansi_param_count, ansi_code[0]);
// Erase in Display
if (ansi_code[0] == 0) {
// Erase from cursor to end of display
// DocDelToNum(Fs->display_doc, Fs->display_doc->cur_entry->line_num);
} else if (ansi_code[0] == 1) {
// Erase from cursor to beginning of display
// DocDelToEntry(Fs->display_doc, Fs->display_doc->cur_entry, FALSE);
} else if (ansi_code[0] == 2) {
// Erase entire display
DocClear(term.doc);
}
ptr++;
break;
case 'K':
// TODO: I have no idea if this actually works
SysLog("K code\n");
// Erase in Line
// CDocEntry *cur_entry = Fs->display_doc->cur_entry;
// CDocEntry *next_entry = cur_entry->next;
// // Delete the current entry
// if (!(cur_entry->de_flags & (DOCEF_HOLD | DOCEF_FILTER_SKIP))) {
// Fs->display_doc->cur_entry = next_entry;
// Fs->display_doc->cur_col = next_entry->min_col;
// DocEntryDel(Fs->display_doc, cur_entry);
// }
// // Create a new entry (line) in its place
// CDocEntry *new_entry = DocEntryNewTag(Fs->display_doc, cur_entry, "");
// DocInsEntry(Fs->display_doc, new_entry);
ptr++;
break;
case 'L':
SysLog("L code\n");
ptr++;
break;
case 'S':
// TODO: Scroll Up
SysLog("Scroll Up");
ptr++;
break;
case 'T':
// TODO: Scroll Down
SysLog("Scroll Down");
ptr++;
break;
case 'M':
SysLog("Case M\n");
// TODO: is this correct? cursor should go one line up
DocPrint(term.doc, "$$CM,0,-1$$");
ptr++;
break;
case '?':
ptr++;
I64 code = 0;
while (IsDigit(*ptr)) {
code = code * 10 + (*ptr - '0');
ptr++;
}
switch (code) {
case 25:
// need to specify which doc?
if (*ptr == 'l') DocCursor(OFF); // Hide cursor
if (*ptr == 'h') DocCursor(ON); // Show cursor
ptr++; // Move past 'l' or 'h'
break;
case 47:
if (*ptr == 'l') SysLog("code 47l\n"); // restore screen
if (*ptr == 'h') SysLog("code 47h\n"); // save screen
ptr++; // Move past 'l' or 'h'
break;
case 1049:
if (*ptr == 'l') SysLog("code 1049l\n"); // enables the alternative buffer
if (*ptr == 'h') SysLog("code 1049h\n"); // disables the alternative buffer
ptr++; // Move past 'l' or 'h'
break;
default:
ptr++;
break;
}
break;
case 's':
SysLog("SaveCurrentCursorPosition\n");
ptr++;
break;
case 'u':
SysLog("RestoreCurrentCursorPosition\n");
ptr++;
break;
case 'r':
// self.restoreCursorPosition();
SysLog("r case \n");
ptr++;
break;
case 'h':
case 'l':
// TODO: Handle 'h' (set mode) or 'l' (reset mode) codes
SysLog("h or l case \n");
ptr++; // Skip 'h' or 'l'
break;
case '=':
SysLog("ScreenMode attempt\n");
ptr++;
break;
default:
if(!IsDigit(*ptr)) {
SysLog("Unknown code: %c\n", *ptr);
}
ptr++;
break;
}
}
}
else {
// Print the received character
HandleControlCodes(*ptr);
ptr++;
}
}
}
U0 TerminalTask() {
while (!term.sock_ready) {
Sleep(100); // Avoid busy waiting
}
while (!force_disconnect) {
term.buffer_len = TCPSocketReceive(term.sock, term.buffer, BUF_SIZE - 1);
if (term.buffer_len > 0) {
term.buffer[term.buffer_len] = '\0';
// parse the buffer
ANSIParse;
} else {
"Error: Connection closed by the remote host.\n";
break;
}
}
}
U0 Telnet(U8 *host=NULL, U16 port=TELNET_PORT) {
I64 sc;
term.window_width = 80;
term.window_height = 25;
term.doc = Fs->display_doc;
StrCopy(Fs->task_title, "TELNET");
Fs->border_src = BDS_CONST;
Fs->border_attr = LTGREEN << 4 + DriveTextAttrGet(':') & 15;
Fs->text_attr = BLACK << 4 + WHITE;
Fs->title_src = TTS_LOCKED_CONST;
DocClear(Fs->border_doc, TRUE);
Fs->win_width = term.window_width;
WinHorz((TEXT_COLS / 2) - (Fs->win_width / 2),
(TEXT_COLS / 2) - (Fs->win_width / 2) +
(Fs->win_width - 1),
Fs);
Fs->win_height = term.window_height;
WinVert((TEXT_ROWS / 2) - (Fs->win_height / 2),
(TEXT_ROWS / 2) - (Fs->win_height / 2) +
(Fs->win_height - 1),
Fs);
DocClear;
// SplashScreen
term.buffer_len = LoadSplashScreen("Art/TelnetSplash.ans");
if (term.buffer_len > 0) {
term.buffer[term.buffer_len] = '\0';
// parse the buffer
ANSIParse;
}
else {
"Error: Could not load splash screen.\n";
}
// PopUp if no host is specified
if (host == NULL) {
try
{
while (host == NULL) {
CHostForm form;
TelnetPrompt(&form);
host = form.host;
port = form.port;
DocClear;
break;
}
}
catch
PutExcept;
}
// Spawn a task to receive data from the socket
term.task = Spawn(&TerminalTask, NULL, "Telnet");
// probably should use word wrap?
DocPrint(, "$$WW,1$$");
DocCursor(OFF);
term.sock_ready = 0; // Initialize the semaphore
term.sock = TelnetOpen(host, port);
if (term.sock <= 0) {
return;
}
term.sock_ready = 1; // Signal that the socket is ready
"$$BG,GREEN$$$$WHITE$$Connected$$FG$$$$BG$$\n";
Sleep(1000);
DocClear;
try
{
while (!force_disconnect) {
U8 key = KeyGet(&sc);
switch (key)
{
case 0:
switch (sc.u8[0])
{
case SC_CURSOR_LEFT:
TCPSocketSendString(term.sock, "\x1B[D");
break;
case SC_CURSOR_RIGHT:
TCPSocketSendString(term.sock, "\x1B[C");
break;
case SC_CURSOR_UP:
TCPSocketSendString(term.sock, "\x1B[A");
break;
case SC_CURSOR_DOWN:
TCPSocketSendString(term.sock, "\x1B[B");
break;
default:
break;
}
break;
case 9:
switch (sc.u8[0])
{
case SC_TAB:
TCPSocketSendString(term.sock, "\x09");
break;
default:
break;
}
case CH_BACKSPACE:
TCPSocketSendString(term.sock, "\x08");
break;
case CH_ESC:
TCPSocketSendString(term.sock, "\x1B");
break;
case CH_SHIFT_ESC:
force_disconnect = TRUE;
break;
// send buffer on enter
case '\n':
TCPSocketSendString(term.sock, "\r\n");
break;
default:
if (key >= ' ' && key <= '~') {
// Handle regular keys
U8 input_buf[2];
input_buf[0] = key;
input_buf[1] = '\0';
TCPSocketSend(term.sock, input_buf, 1);
}
break;
}
}
}
catch
PutExcept;
TCPSocketClose(term.sock);
"Telnet connection closed.\n";
}
// dev server
Telnet("localhost", 8888);
// Telnet;