From 3cc942a4d4cbbf4f6cfa3ea625af0e54972d88fb Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 11 Mar 2026 19:41:17 -0500 Subject: [PATCH] Encryption and other serial fixes. Terminal cursor not being erased fixed. Ability to auto-size windows added. --- dosbox-x.conf | 5 +- dvx/dvxApp.c | 43 +++++- dvx/dvxApp.h | 5 + dvx/dvxWidget.h | 9 ++ dvx/widgets/widgetAnsiTerm.c | 170 +++++++++++++++++++++ packet/packet.c | 43 ++++-- packet/packet.h | 4 + proxy/proxy.c | 277 +++++++++++++++++++++++++++++++---- proxy/sockShim.c | 12 +- rs232/rs232.c | 10 +- seclink/secLink.c | 11 +- security/security.c | 5 +- termdemo/termdemo.c | 42 +++++- 13 files changed, 579 insertions(+), 57 deletions(-) diff --git a/dosbox-x.conf b/dosbox-x.conf index 8d56337..9444a15 100644 --- a/dosbox-x.conf +++ b/dosbox-x.conf @@ -8,7 +8,7 @@ memsize = 64 quit warning = false [cpu] -core = auto +core = normal cputype = 486 cycles = 33445 @@ -27,6 +27,9 @@ umb = true xms = true ems = true +[serial] +serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1 + [autoexec] mount c . c: diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 4f6882a..8f662bc 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -1,6 +1,8 @@ // dvx_app.c — Layer 5: Application API for DV/X GUI #include "dvxApp.h" +#include "dvxWidget.h" +#include "widgets/widgetInternal.h" #include "dvxFont.h" #include "dvxCursor.h" @@ -355,6 +357,44 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) { } +// ============================================================ +// dvxFitWindow +// ============================================================ + +void dvxFitWindow(AppContextT *ctx, WindowT *win) { + if (!ctx || !win || !win->widgetRoot) { + return; + } + + // Measure the widget tree to get minimum content size + widgetCalcMinSizeTree(win->widgetRoot, &ctx->font); + + int32_t contentW = win->widgetRoot->calcMinW; + int32_t contentH = win->widgetRoot->calcMinH; + + // Compute chrome overhead + int32_t topChrome = CHROME_TOTAL_TOP; + if (win->menuBar) { + topChrome += CHROME_MENU_HEIGHT; + } + + int32_t newW = contentW + CHROME_TOTAL_SIDE * 2; + int32_t newH = contentH + topChrome + CHROME_TOTAL_BOTTOM; + + // Dirty old position + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + + // Resize + win->w = newW; + win->h = newH; + wmUpdateContentRect(win); + wmReallocContentBuf(win, &ctx->display); + + // Dirty new position + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); +} + + // ============================================================ // dvxGetBlitOps // ============================================================ @@ -506,8 +546,9 @@ bool dvxUpdate(AppContextT *ctx) { if (ctx->dirty.count > 0) { compositeAndFlush(ctx); + } else if (ctx->idleCallback) { + ctx->idleCallback(ctx->idleCtx); } else { - // Nothing to do — yield timeslice __dpmi_yield(); } diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index b69ac19..c891718 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -39,6 +39,8 @@ typedef struct AppContextT { int32_t lastCloseClickId; // window ID of last-clicked close gadget (-1 = none) int32_t iconRefreshIdx; // next minimized icon to refresh (staggered) int32_t frameCount; // frame counter for periodic tasks + void (*idleCallback)(void *ctx); // called instead of yield when non-NULL + void *idleCtx; } AppContextT; // Initialize the application (VESA mode, input, etc.) @@ -60,6 +62,9 @@ WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t // Destroy a window void dvxDestroyWindow(AppContextT *ctx, WindowT *win); +// Resize a window to fit its widget tree's minimum size +void dvxFitWindow(AppContextT *ctx, WindowT *win); + // Invalidate a region of a window's content area (triggers repaint) void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h); diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 4ff3f1c..565ad20 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -307,6 +307,10 @@ typedef struct WidgetT { int32_t scrollbackCount; // current number of lines stored int32_t scrollbackHead; // write position (circular index) int32_t scrollPos; // view position (scrollbackCount = live) + // Dirty tracking for fast repaint + uint32_t dirtyRows; // bitmask of rows needing repaint + int32_t lastCursorRow; // cursor row at last repaint + int32_t lastCursorCol; // cursor col at last repaint // Communications interface (all NULL = disconnected) void *commCtx; int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); @@ -495,6 +499,11 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines); // Poll the comm interface for incoming data and process it. Returns bytes processed. int32_t wgtAnsiTermPoll(WidgetT *w); +// Fast repaint: renders only dirty rows directly into the window's content +// buffer, bypassing the full widget paint pipeline. Returns number of rows +// repainted (0 if nothing was dirty). +int32_t wgtAnsiTermRepaint(WidgetT *w); + // ============================================================ // Operations // ============================================================ diff --git a/dvx/widgets/widgetAnsiTerm.c b/dvx/widgets/widgetAnsiTerm.c index b2e30ff..18bdb99 100644 --- a/dvx/widgets/widgetAnsiTerm.c +++ b/dvx/widgets/widgetAnsiTerm.c @@ -52,6 +52,9 @@ static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 }; static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow); static void ansiTermDeleteLines(WidgetT *w, int32_t count); +static void ansiTermDirtyAll(WidgetT *w); +static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count); +static void ansiTermDirtyRow(WidgetT *w, int32_t row); static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd); static void ansiTermEraseDisplay(WidgetT *w, int32_t mode); static void ansiTermEraseLine(WidgetT *w, int32_t mode); @@ -93,6 +96,43 @@ static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { } +// ============================================================ +// ansiTermDirtyAll +// ============================================================ + +static void ansiTermDirtyAll(WidgetT *w) { + w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; +} + + +// ============================================================ +// ansiTermDirtyRange +// ============================================================ +// +// Mark rows dirty that are touched by a cell range [startCell, startCell+count). + +static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { + int32_t cols = w->as.ansiTerm.cols; + int32_t startRow = startCell / cols; + int32_t endRow = (startCell + count - 1) / cols; + + for (int32_t r = startRow; r <= endRow && r < 32; r++) { + w->as.ansiTerm.dirtyRows |= (1U << r); + } +} + + +// ============================================================ +// ansiTermDirtyRow +// ============================================================ + +static void ansiTermDirtyRow(WidgetT *w, int32_t row) { + if (row >= 0 && row < 32) { + w->as.ansiTerm.dirtyRows |= (1U << row); + } +} + + // ============================================================ // ansiTermDeleteLines // ============================================================ @@ -122,6 +162,11 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) { for (int32_t r = rows - count; r < rows; r++) { ansiTermFillCells(w, r * cols, cols); } + + // All rows from cursorRow down are affected + for (int32_t r = row; r < rows; r++) { + ansiTermDirtyRow(w, r); + } } @@ -383,6 +428,8 @@ static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { cells[(start + i) * 2] = ' '; cells[(start + i) * 2 + 1] = attr; } + + ansiTermDirtyRange(w, start, count); } @@ -442,6 +489,11 @@ static void ansiTermInsertLines(WidgetT *w, int32_t count) { for (int32_t r = row; r < row + count; r++) { ansiTermFillCells(w, r * cols, cols); } + + // All rows from cursorRow down are affected + for (int32_t r = row; r < rows; r++) { + ansiTermDirtyRow(w, r); + } } @@ -610,6 +662,7 @@ static void ansiTermPutChar(WidgetT *w, uint8_t ch) { int32_t idx = (row * cols + col) * 2; w->as.ansiTerm.cells[idx] = ch; w->as.ansiTerm.cells[idx + 1] = w->as.ansiTerm.curAttr; + ansiTermDirtyRow(w, row); } w->as.ansiTerm.cursorCol++; @@ -640,6 +693,8 @@ static void ansiTermScrollDown(WidgetT *w) { // Clear the top line ansiTermFillCells(w, 0, cols); + + ansiTermDirtyAll(w); } @@ -672,6 +727,8 @@ static void ansiTermScrollUp(WidgetT *w) { if (wasAtBottom) { w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; } + + ansiTermDirtyAll(w); } @@ -833,6 +890,119 @@ int32_t wgtAnsiTermPoll(WidgetT *w) { } +// ============================================================ +// wgtAnsiTermRepaint +// ============================================================ +// +// Fast repaint: renders only dirty rows directly into the window's +// content buffer, bypassing the full widget paint pipeline (no clear, +// no relayout, no other widgets). This keeps ACK turnaround fast +// for the serial link. + +int32_t wgtAnsiTermRepaint(WidgetT *w) { + if (!w || w->type != WidgetAnsiTermE || !w->window) { + return 0; + } + + // Always repaint the row the cursor was on last time (to erase it) + // and the row it's on now (to draw it) + int32_t prevRow = w->as.ansiTerm.lastCursorRow; + int32_t curRow = w->as.ansiTerm.cursorRow; + int32_t prevCol = w->as.ansiTerm.lastCursorCol; + int32_t curCol = w->as.ansiTerm.cursorCol; + + if (prevRow != curRow || prevCol != curCol) { + if (prevRow >= 0 && prevRow < w->as.ansiTerm.rows) { + w->as.ansiTerm.dirtyRows |= (1U << prevRow); + } + if (curRow >= 0 && curRow < w->as.ansiTerm.rows) { + w->as.ansiTerm.dirtyRows |= (1U << curRow); + } + } + + uint32_t dirty = w->as.ansiTerm.dirtyRows; + if (dirty == 0) { + return 0; + } + + WindowT *win = w->window; + if (!win->contentBuf || !win->widgetRoot) { + return 0; + } + + AppContextT *ctx = (AppContextT *)win->widgetRoot->userData; + if (!ctx) { + return 0; + } + + // Set up display context pointing at the content buffer + DisplayT cd = ctx->display; + cd.backBuf = win->contentBuf; + cd.width = win->contentW; + cd.height = win->contentH; + cd.pitch = win->contentPitch; + cd.clipX = 0; + cd.clipY = 0; + cd.clipW = win->contentW; + cd.clipH = win->contentH; + + const BlitOpsT *ops = &ctx->blitOps; + const BitmapFontT *font = &ctx->font; + + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + int32_t cellW = font->charWidth; + int32_t cellH = font->charHeight; + int32_t baseX = w->x + ANSI_BORDER; + int32_t baseY = w->y + ANSI_BORDER; + + // Build palette + uint32_t palette[16]; + for (int32_t i = 0; i < 16; i++) { + palette[i] = packColor(&cd, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); + } + + bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + int32_t repainted = 0; + + for (int32_t row = 0; row < rows; row++) { + if (!(dirty & (1U << row))) { + continue; + } + + int32_t lineIndex = w->as.ansiTerm.scrollPos + row; + const uint8_t *lineData = ansiTermGetLine(w, lineIndex); + + for (int32_t col = 0; col < cols; col++) { + uint8_t ch = lineData[col * 2]; + uint8_t attr = lineData[col * 2 + 1]; + + uint32_t fg = palette[attr & 0x0F]; + uint32_t bg = palette[(attr >> 4) & 0x0F]; + + if (viewingLive && w->as.ansiTerm.cursorVisible && w->focused && + row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) { + uint32_t tmp = fg; + fg = bg; + bg = tmp; + } + + int32_t cx = baseX + col * cellW; + int32_t cy = baseY + row * cellH; + + drawChar(&cd, ops, font, cx, cy, (char)ch, fg, bg, true); + } + + repainted++; + } + + w->as.ansiTerm.dirtyRows = 0; + w->as.ansiTerm.lastCursorRow = w->as.ansiTerm.cursorRow; + w->as.ansiTerm.lastCursorCol = w->as.ansiTerm.cursorCol; + return repainted; +} + + // ============================================================ // wgtAnsiTermSetComm // ============================================================ diff --git a/packet/packet.c b/packet/packet.c index 239af2a..37d35ac 100644 --- a/packet/packet.c +++ b/packet/packet.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "packet.h" #include "../rs232/rs232.h" @@ -46,8 +47,8 @@ // Receive buffer must hold at least one max-size stuffed frame #define RX_BUF_SIZE (MAX_STUFFED_SIZE + 64) -// Retransmit timeout in poll cycles (caller-dependent; ~50ms worth at typical poll rates) -#define RETRANSMIT_TIMEOUT 500 +// Retransmit timeout in milliseconds +#define RETRANSMIT_TIMEOUT_MS 500 // Receive state machine #define RX_STATE_HUNT 0 // scanning for FLAG_BYTE @@ -64,7 +65,7 @@ typedef struct { uint8_t data[PKT_MAX_PAYLOAD]; int len; uint8_t seq; - uint32_t timer; + clock_t timer; } TxSlotT; // Connection state @@ -86,8 +87,6 @@ struct PktConnS { uint8_t rxFrame[MAX_FRAME_SIZE]; int rxFrameLen; - // Poll counter (simple timer) - uint32_t pollCount; }; @@ -203,6 +202,9 @@ static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t * } } + // Trailing flag to close the frame immediately + stuffed[out++] = FLAG_BYTE; + // Send via serial port (blocking write) rs232Write(conn->com, (const char *)stuffed, out); } @@ -309,7 +311,7 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) { for (int i = idx; i < conn->txCount; i++) { TxSlotT *slot = &conn->txSlots[i]; sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); - slot->timer = conn->pollCount; + slot->timer = clock(); } } break; @@ -378,11 +380,14 @@ static void rxProcessByte(PktConnT *conn, uint8_t byte) { // ======================================================================== static void retransmitCheck(PktConnT *conn) { + clock_t now = clock(); + clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000; + for (int i = 0; i < conn->txCount; i++) { TxSlotT *slot = &conn->txSlots[i]; - if (conn->pollCount - slot->timer >= RETRANSMIT_TIMEOUT) { + if (now - slot->timer >= timeout) { sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); - slot->timer = conn->pollCount; + slot->timer = now; } } } @@ -399,6 +404,14 @@ void pktClose(PktConnT *conn) { } +bool pktCanSend(PktConnT *conn) { + if (!conn) { + return false; + } + return conn->txCount < conn->windowSize; +} + + int pktGetPending(PktConnT *conn) { if (!conn) { return PKT_ERR_INVALID_PARAM; @@ -432,7 +445,6 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *call conn->rxExpectSeq = 0; conn->rxState = RX_STATE_HUNT; conn->rxFrameLen = 0; - conn->pollCount = 0; return conn; } @@ -447,8 +459,6 @@ int pktPoll(PktConnT *conn) { return PKT_ERR_INVALID_PARAM; } - conn->pollCount++; - // Read available serial data and feed to state machine while ((nRead = rs232Read(conn->com, buf, sizeof(buf))) > 0) { for (int i = 0; i < nRead; i++) { @@ -460,6 +470,11 @@ int pktPoll(PktConnT *conn) { } } + // Detect disconnected socket/port + if (nRead < 0) { + return PKT_ERR_DISCONNECTED; + } + // Check for retransmit timeouts retransmitCheck(conn); @@ -498,7 +513,9 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) { // Wait for window space if blocking if (block) { while (conn->txCount >= conn->windowSize) { - pktPoll(conn); + if (pktPoll(conn) == PKT_ERR_DISCONNECTED) { + return PKT_ERR_DISCONNECTED; + } } } else { if (conn->txCount >= conn->windowSize) { @@ -511,7 +528,7 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) { memcpy(slot->data, data, len); slot->len = len; slot->seq = conn->txNextSeq; - slot->timer = conn->pollCount; + slot->timer = clock(); conn->txCount++; conn->txNextSeq++; diff --git a/packet/packet.h b/packet/packet.h index 7f99287..44bda1d 100644 --- a/packet/packet.h +++ b/packet/packet.h @@ -26,6 +26,7 @@ #define PKT_ERR_INVALID_PARAM -6 #define PKT_ERR_TX_FULL -7 #define PKT_ERR_NO_DATA -8 +#define PKT_ERR_DISCONNECTED -9 // Callback for received packets // ctx: user context pointer @@ -62,6 +63,9 @@ int pktPoll(PktConnT *conn); // Sends a RST frame to the remote side. int pktReset(PktConnT *conn); +// Check if there is room in the transmit window. +bool pktCanSend(PktConnT *conn); + // Get number of unacknowledged packets in the transmit window. int pktGetPending(PktConnT *conn); diff --git a/proxy/proxy.c b/proxy/proxy.c index 96c67b0..c7ffaa1 100644 --- a/proxy/proxy.c +++ b/proxy/proxy.c @@ -4,7 +4,7 @@ // secLink protocol plain telnet // // Usage: proxy [listen_port] [bbs_host] [bbs_port] -// Defaults: 2323 bbs.duensing.digital 23 +// Defaults: 2323 10.1.0.244 2023 #include #include @@ -28,18 +28,46 @@ // ======================================================================== #define DEFAULT_LISTEN_PORT 2323 -#define DEFAULT_BBS_HOST "bbs.duensing.digital" -#define DEFAULT_BBS_PORT 23 +#define DEFAULT_BBS_HOST "10.1.0.244" +#define DEFAULT_BBS_PORT 2023 #define CHANNEL_TERMINAL 0 #define POLL_TIMEOUT_MS 10 +// Telnet protocol bytes +#define TEL_IAC 255 +#define TEL_DONT 254 +#define TEL_DO 253 +#define TEL_WONT 252 +#define TEL_WILL 251 +#define TEL_SB 250 +#define TEL_SE 240 + +// Telnet options we accept +#define TELOPT_ECHO 1 +#define TELOPT_SGA 3 +#define TELOPT_TTYPE 24 +#define TELOPT_NAWS 31 + +// Telnet parser states +#define TS_DATA 0 +#define TS_IAC 1 +#define TS_WILL 2 +#define TS_WONT 3 +#define TS_DO 4 +#define TS_DONT 5 +#define TS_SB 6 +#define TS_SB_IAC 7 + // ======================================================================== // Static globals // ======================================================================== -static volatile bool sRunning = true; +static volatile bool sRunning = true; +static volatile bool sGotEnter = false; +static int sTelState = TS_DATA; +static int sClientFd = -1; // ======================================================================== @@ -48,9 +76,12 @@ static volatile bool sRunning = true; static int connectToBbs(const char *host, int port); static int createListenSocket(int port); +static void hexDump(const char *label, const uint8_t *data, int len); static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel); static void seedRng(void); static void sigHandler(int sig); +static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt); +static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out); // ======================================================================== @@ -121,11 +152,42 @@ static int createListenSocket(int port) { } +static void hexDump(const char *label, const uint8_t *data, int len) { + printf("%s (%d bytes):", label, len); + for (int i = 0; i < len && i < 64; i++) { + if (i % 16 == 0) { + printf("\n "); + } + printf("%02X ", data[i]); + } + if (len > 64) { + printf("\n ..."); + } + printf("\n"); +} + + static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel) { int bbsFd = *(int *)ctx; (void)channel; + hexDump("DOS->proxy (decrypted)", data, len); + + // Check for ENTER before BBS is connected + if (!sGotEnter) { + for (int i = 0; i < len; i++) { + if (data[i] == '\r' || data[i] == '\n') { + sGotEnter = true; + printf("Got ENTER from terminal.\n"); + break; + } + } + return; + } + + hexDump("DOS->BBS", data, len); + int sent = 0; while (sent < len) { ssize_t n = write(bbsFd, data + sent, len - sent); @@ -156,6 +218,123 @@ static void seedRng(void) { static void sigHandler(int sig) { (void)sig; sRunning = false; + // Shutdown the client socket to break any blocking reads in the packet layer + if (sClientFd >= 0) { + shutdown(sClientFd, SHUT_RDWR); + } +} + + +// Send a telnet negotiation response +static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt) { + uint8_t resp[3] = {TEL_IAC, cmd, opt}; + ssize_t n = write(bbsFd, resp, 3); + (void)n; + printf(" TEL TX: %s %d\n", + cmd == TEL_WILL ? "WILL" : cmd == TEL_WONT ? "WONT" : + cmd == TEL_DO ? "DO" : "DONT", opt); +} + + +// Filter telnet IAC sequences from BBS data. +// Handles negotiation by responding appropriately. +// Returns the number of clean data bytes written to 'out'. +static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) { + int outLen = 0; + + for (int i = 0; i < inLen; i++) { + uint8_t b = in[i]; + + switch (sTelState) { + case TS_DATA: + if (b == TEL_IAC) { + sTelState = TS_IAC; + } else { + out[outLen++] = b; + } + break; + + case TS_IAC: + switch (b) { + case TEL_IAC: + // Escaped 0xFF — emit literal + out[outLen++] = 0xFF; + sTelState = TS_DATA; + break; + case TEL_WILL: + sTelState = TS_WILL; + break; + case TEL_WONT: + sTelState = TS_WONT; + break; + case TEL_DO: + sTelState = TS_DO; + break; + case TEL_DONT: + sTelState = TS_DONT; + break; + case TEL_SB: + sTelState = TS_SB; + break; + default: + // Unknown command, skip + sTelState = TS_DATA; + break; + } + break; + + case TS_WILL: + // Server offers to do something — accept ECHO and SGA, refuse others + printf(" TEL RX: WILL %d\n", b); + if (b == TELOPT_ECHO || b == TELOPT_SGA) { + telnetRespond(bbsFd, TEL_DO, b); + } else { + telnetRespond(bbsFd, TEL_DONT, b); + } + sTelState = TS_DATA; + break; + + case TS_WONT: + printf(" TEL RX: WONT %d\n", b); + telnetRespond(bbsFd, TEL_DONT, b); + sTelState = TS_DATA; + break; + + case TS_DO: + // Server asks us to do something — accept TTYPE and NAWS, refuse others + printf(" TEL RX: DO %d\n", b); + if (b == TELOPT_TTYPE || b == TELOPT_NAWS) { + telnetRespond(bbsFd, TEL_WILL, b); + } else { + telnetRespond(bbsFd, TEL_WONT, b); + } + sTelState = TS_DATA; + break; + + case TS_DONT: + printf(" TEL RX: DONT %d\n", b); + telnetRespond(bbsFd, TEL_WONT, b); + sTelState = TS_DATA; + break; + + case TS_SB: + // Inside subnegotiation — skip until IAC SE + if (b == TEL_IAC) { + sTelState = TS_SB_IAC; + } + break; + + case TS_SB_IAC: + if (b == TEL_SE) { + sTelState = TS_DATA; + } else { + sTelState = TS_SB; + } + break; + } + } + + return outLen; } @@ -193,8 +372,12 @@ int main(int argc, char *argv[]) { } signal(SIGPIPE, SIG_IGN); - signal(SIGINT, sigHandler); - signal(SIGTERM, sigHandler); + struct sigaction sa; + sa.sa_handler = sigHandler; + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); // Listen for 86Box connection listenFd = createListenSocket(listenPort); @@ -204,35 +387,34 @@ int main(int argc, char *argv[]) { } printf("Listening on port %d...\n", listenPort); - // Accept connection from 86Box + // Accept connection from 86Box (poll so Ctrl+C works) + struct pollfd listenPoll = {listenFd, POLLIN, 0}; + while (sRunning) { + int pr = poll(&listenPoll, 1, 500); + if (pr > 0) { + break; + } + } clientLen = sizeof(clientAddr); clientFd = accept(listenFd, (struct sockaddr *)&clientAddr, &clientLen); close(listenFd); - if (clientFd < 0) { - fprintf(stderr, "Accept failed: %s\n", strerror(errno)); - return 1; + if (!sRunning || clientFd < 0) { + if (sRunning) { + fprintf(stderr, "Accept failed: %s\n", strerror(errno)); + } + return sRunning ? 1 : 0; } printf("86Box connected.\n"); + sClientFd = clientFd; // Associate socket with COM0 for the secLink stack sockShimSetFd(0, clientFd); - // Connect to BBS - printf("Connecting to %s:%d...\n", bbsHost, bbsPort); - bbsFd = connectToBbs(bbsHost, bbsPort); - if (bbsFd < 0) { - fprintf(stderr, "Failed to connect to BBS: %s\n", strerror(errno)); - close(clientFd); - return 1; - } - printf("BBS connected.\n"); - // Seed RNG from /dev/urandom and open secLink seedRng(); link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecvFromDos, &bbsFd); if (!link) { fprintf(stderr, "Failed to open secLink.\n"); - close(bbsFd); close(clientFd); return 1; } @@ -243,10 +425,38 @@ int main(int argc, char *argv[]) { if (rc != SECLINK_SUCCESS) { fprintf(stderr, "Handshake failed: %d\n", rc); secLinkClose(link); - close(bbsFd); return 1; } - printf("Handshake complete. Proxying traffic.\n"); + printf("Handshake complete.\n"); + + // Wait for ENTER from terminal before connecting to BBS + printf("Waiting for terminal to send ENTER...\n"); + while (sRunning) { + secLinkPoll(link); + if (sGotEnter) { + break; + } + usleep(10000); + } + if (!sRunning) { + secLinkClose(link); + return 0; + } + + // Send test string to verify data path + const char *testMsg = "SecLink proxy ready.\r\n"; + secLinkSend(link, (const uint8_t *)testMsg, (int)strlen(testMsg), CHANNEL_TERMINAL, true, false); + printf("Sent test message to terminal.\n"); + + // Now connect to BBS + printf("Connecting to %s:%d...\n", bbsHost, bbsPort); + bbsFd = connectToBbs(bbsHost, bbsPort); + if (bbsFd < 0) { + fprintf(stderr, "Failed to connect to BBS: %s\n", strerror(errno)); + secLinkClose(link); + return 1; + } + printf("BBS connected. Proxying traffic.\n"); // Set BBS socket non-blocking for the main loop int flags = fcntl(bbsFd, F_GETFL, 0); @@ -265,15 +475,28 @@ int main(int argc, char *argv[]) { // (callback forwards decrypted data to BBS) secLinkPoll(link); - // Read from BBS and send encrypted to 86Box + // Read from BBS, filter telnet, send clean data to 86Box if (fds[1].revents & POLLIN) { - uint8_t buf[SECLINK_MAX_PAYLOAD]; - ssize_t n = read(bbsFd, buf, sizeof(buf)); + uint8_t raw[64]; + uint8_t clean[64]; + ssize_t n = read(bbsFd, raw, sizeof(raw)); if (n <= 0) { printf("BBS disconnected.\n"); break; } - secLinkSend(link, buf, (int)n, CHANNEL_TERMINAL, true, true); + + int cleanLen = telnetFilter(bbsFd, raw, (int)n, clean); + if (cleanLen > 0) { + hexDump("BBS->DOS", clean, cleanLen); + + // Retry with ACK processing if the send window is full + int rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false); + while (rc != SECLINK_SUCCESS && sRunning) { + secLinkPoll(link); + usleep(1000); + rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false); + } + } } // Check for disconnects diff --git a/proxy/sockShim.c b/proxy/sockShim.c index 7ff69c0..9f1e3ff 100644 --- a/proxy/sockShim.c +++ b/proxy/sockShim.c @@ -53,12 +53,18 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int int rs232Read(int com, char *data, int len) { if (com < 0 || com >= MAX_PORTS || sFds[com] < 0) { - return 0; + return -1; } ssize_t n = recv(sFds[com], data, len, MSG_DONTWAIT); - if (n <= 0) { - return 0; + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return -1; + } + if (n == 0) { + return -1; } return (int)n; diff --git a/rs232/rs232.c b/rs232/rs232.c index ba31c00..99ea01f 100644 --- a/rs232/rs232.c +++ b/rs232/rs232.c @@ -1321,11 +1321,11 @@ int rs232SetParity(int com, char parity) { } switch (parity) { - case 'n': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_NONE); return RS232_SUCCESS; - case 'e': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_EVEN); return RS232_SUCCESS; - case 'o': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_ODD); return RS232_SUCCESS; - case 'm': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_MARK); return RS232_SUCCESS; - case 's': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_SPACE); return RS232_SUCCESS; + case 'n': case 'N': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_NONE); return RS232_SUCCESS; + case 'e': case 'E': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_EVEN); return RS232_SUCCESS; + case 'o': case 'O': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_ODD); return RS232_SUCCESS; + case 'm': case 'M': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_MARK); return RS232_SUCCESS; + case 's': case 'S': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_SPACE); return RS232_SUCCESS; } return RS232_ERR_INVALID_PARITY; } diff --git a/seclink/secLink.c b/seclink/secLink.c index 7e371c0..305df95 100644 --- a/seclink/secLink.c +++ b/seclink/secLink.c @@ -73,7 +73,6 @@ static void completeHandshake(SecLinkT *link) { uint8_t txKey[SEC_XTEA_KEY_SIZE]; uint8_t rxKey[SEC_XTEA_KEY_SIZE]; bool weAreLower; - secDhComputeSecret(link->dh, link->remoteKey, SEC_DH_KEY_SIZE); secDhDeriveKey(link->dh, masterKey, SEC_XTEA_KEY_SIZE); @@ -212,7 +211,9 @@ int secLinkHandshake(SecLinkT *link) { // Poll until the callback completes the handshake while (link->state != STATE_READY) { - pktPoll(link->pkt); + if (pktPoll(link->pkt) == PKT_ERR_DISCONNECTED) { + return SECLINK_ERR_HANDSHAKE; + } } return SECLINK_SUCCESS; @@ -286,6 +287,12 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b return SECLINK_ERR_PARAM; } + // For non-blocking sends, check window space BEFORE encrypting + // to avoid advancing the cipher counter on a failed send + if (!block && !pktCanSend(link->pkt)) { + return SECLINK_ERR_SEND; + } + // Build channel header byte buf[0] = channel & CHANNEL_MASK; if (encrypt) { diff --git a/security/security.c b/security/security.c index 6ba471c..f88cf7f 100644 --- a/security/security.c +++ b/security/security.c @@ -339,8 +339,8 @@ static void computeR2(BigNumT *r2, const BigNumT *m) { bnSet(r2, 1); for (int i = 0; i < 2 * BN_BITS; i++) { - bnShiftLeft1(r2); - if (bnCmp(r2, m) >= 0) { + int carry = bnShiftLeft1(r2); + if (carry || bnCmp(r2, m) >= 0) { bnSub(r2, r2, m); } } @@ -586,6 +586,7 @@ int secDhGenerateKeys(SecDhT *dh) { // public = g^private mod p bnModExp(&dh->publicKey, &sDhGenerator, &dh->privateKey, &sDhPrime, sDhM0Inv, &sDhR2); + dh->hasKeys = true; dh->hasSecret = false; diff --git a/termdemo/termdemo.c b/termdemo/termdemo.c index 86447ed..5f0fbde 100644 --- a/termdemo/termdemo.c +++ b/termdemo/termdemo.c @@ -11,6 +11,7 @@ #include "dvxWidget.h" #include "secLink.h" #include "security.h" +#include "../rs232/rs232.h" #include #include @@ -46,6 +47,7 @@ typedef struct { static int32_t commRead(void *ctx, uint8_t *buf, int32_t maxLen); static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len); +static void idlePoll(void *ctx); static void onCloseCb(WindowT *win); static void onMenuCb(WindowT *win, int32_t menuId); static void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel); @@ -80,6 +82,16 @@ static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len) { } +// ============================================================ +// idlePoll — poll serial link instead of yielding CPU +// ============================================================ + +static void idlePoll(void *ctx) { + TermContextT *tc = (TermContextT *)ctx; + secLinkPoll(tc->link); +} + + // ============================================================ // onCloseCb — quit when the terminal window is closed // ============================================================ @@ -178,6 +190,15 @@ int main(int argc, char *argv[]) { TermContextT tc; memset(&tc, 0, sizeof(tc)); + // Test rs232 directly first for diagnostics + printf("Testing rs232Open on COM%d...\n", comPort + 1); + int rsRc = rs232Open(comPort, baudRate, 8, 'N', 1, 0); + printf("rs232Open returned %d\n", rsRc); + if (rsRc == RS232_SUCCESS) { + printf("UART type: %d\n", rs232GetUartType(comPort)); + rs232Close(comPort); + } + // Open secLink on the specified COM port printf("Opening SecLink on COM%d...\n", comPort + 1); tc.link = secLinkOpen(comPort, baudRate, 8, 'N', 1, 0, onRecv, &tc); @@ -213,7 +234,7 @@ int main(int argc, char *argv[]) { tc.app = &ctx; // Create the terminal window - WindowT *win = dvxCreateWindow(&ctx, "SecLink Terminal", 40, 30, 680, 460, true); + WindowT *win = dvxCreateWindow(&ctx, "SecLink Terminal", 40, 30, 680, 460, false); if (!win) { dvxShutdown(&ctx); @@ -253,12 +274,27 @@ int main(int argc, char *argv[]) { snprintf(statusText, sizeof(statusText), "COM%d %ld 8N1 [Encrypted]", comPort + 1, (long)baudRate); wgtLabel(sb, statusText); + // Fit window to widget tree + dvxFitWindow(&ctx, win); wgtInvalidate(root); - // Main loop — poll secLink and terminal each frame - while (dvxUpdate(&ctx)) { + // Poll serial during idle instead of yielding CPU + ctx.idleCallback = idlePoll; + ctx.idleCtx = &tc; + + // Main loop — poll serial, render immediately, composite + while (ctx.running) { secLinkPoll(tc.link); wgtAnsiTermPoll(term); + + if (wgtAnsiTermRepaint(term) > 0) { + win->contentDirty = true; + dvxInvalidateWindow(&ctx, win); + } + + if (!dvxUpdate(&ctx)) { + break; + } } // Cleanup