Encryption and other serial fixes. Terminal cursor not being erased fixed. Ability to auto-size windows added.

This commit is contained in:
Scott Duensing 2026-03-11 19:41:17 -05:00
parent 79b2825a98
commit 3cc942a4d4
13 changed files with 579 additions and 57 deletions

View file

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

View file

@ -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();
}

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pc.h>
#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++;

View file

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

View file

@ -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 <stdio.h>
#include <stdlib.h>
@ -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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,6 +11,7 @@
#include "dvxWidget.h"
#include "secLink.h"
#include "security.h"
#include "../rs232/rs232.h"
#include <stdio.h>
#include <stdlib.h>
@ -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