Files
comet/comet.pas
Ken Johnson bade0eb593 Comet 1.00 - Initial commit: complete standalone FidoNet TCP mailer
Standalone FidoNet mailer daemon implementing the Comet protocol
(TCP variant of the Nova protocol family) with BinkP/1.1 fallback.
Written in Free Pascal for DOS/Win/OS2/Linux/FreeBSD.

15 source files, ~10K lines:
- Protocol: length-prefixed frames, SHA-256/384/512, CRC-32,
  bidirectional transfer with sliding window, adaptive block sizing
- Session: own TCP handshake with BinkP auto-detection on port 26638
- Outbound: BSO (Binkley), FrontDoor, D'Bridge format support
- Daemon: multi-session with thread pool, outbound scanner
- Paths: DOS<->Linux bridge with case-insensitive lookup, drive mapping
- Config: INI-style with heavily documented sample (COMET.SAM)

All 17 Nova interop bug fixes baked in from the start.
18/18 tests passing (CRC-32, SHA-256/384/512, frame encode/decode).
2026-03-29 20:02:37 -07:00

463 lines
12 KiB
ObjectPascal

{
Comet - Direct TCP File Transfer for FidoNet
comet.pas - Main program
Usage:
comet Run as daemon (listen + poll outbound)
comet -c config.cfg Use specified config file
comet call 1:213/723 Single outbound call to a node
comet -h Show help
comet -v Show version
Signal handling (Unix):
SIGHUP = Reload configuration
SIGTERM = Clean shutdown
SIGINT = Clean shutdown
Copyright (C) 2026 Ken Johnson
License: GPL-2.0
}
program comet;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
BaseUnix,
{$ENDIF}
SysUtils, Classes, cometdef, cometcfg, cometpath, comettcp, cometlog,
cometses, cometxfer, cometbso, cometbinkp, cometfile, cometdaemon;
const
DEFAULT_CFG = 'comet.cfg';
var
Daemon: TCometDaemon;
CfgPath: string;
CmdMode: (cmDaemon, cmCall, cmHelp, cmVersion);
CallTarget: string;
{$IFDEF UNIX}
{ Signal handlers }
procedure HandleSigTerm(Sig: cint); cdecl;
begin
if Daemon <> nil then
Daemon.Shutdown;
end;
procedure HandleSigHup(Sig: cint); cdecl;
begin
LogInfo('SIGHUP received - config reload not yet implemented');
end;
procedure InstallSignalHandlers;
var
Act: SigActionRec;
begin
FillChar(Act, SizeOf(Act), 0);
Act.sa_handler := SigActionHandler(@HandleSigTerm);
fpSigAction(SIGTERM, @Act, nil);
fpSigAction(SIGINT, @Act, nil);
Act.sa_handler := SigActionHandler(@HandleSigHup);
fpSigAction(SIGHUP, @Act, nil);
{ Ignore SIGPIPE - critical for TCP code }
Act.sa_handler := SigActionHandler(SIG_IGN);
fpSigAction(SIGPIPE, @Act, nil);
end;
{$ENDIF}
procedure ShowVersion;
begin
WriteLn(COMET_NAME, ' ', COMET_VERSION,
' - Direct TCP File Transfer for FidoNet');
WriteLn('Copyright (C) 2026 Ken Johnson');
WriteLn('Port 26638 (C-O-M-E-T) with BinkP fallback');
end;
procedure ShowHelp;
begin
ShowVersion;
WriteLn;
WriteLn('Usage: comet [options] [command]');
WriteLn;
WriteLn('Commands:');
WriteLn(' (none) Run as daemon (listen + poll outbound)');
WriteLn(' call <address> Single outbound call to a FidoNet node');
WriteLn(' Address format: zone:net/node[.point]');
WriteLn;
WriteLn('Options:');
WriteLn(' -c <file> Use specified config file');
WriteLn(' (default: comet.cfg in current directory)');
WriteLn(' -d Enable debug/trace logging');
WriteLn(' -q Quiet mode (errors only on console)');
WriteLn(' -v Show version and exit');
WriteLn(' -h, --help Show this help and exit');
WriteLn;
WriteLn('Signals (Unix):');
WriteLn(' SIGHUP Reload configuration');
WriteLn(' SIGTERM, SIGINT Clean shutdown');
WriteLn;
WriteLn('Configuration:');
WriteLn(' Edit comet.cfg or run CSETUP for interactive configuration.');
WriteLn(' See COMET.DOC for complete documentation of all options.');
WriteLn;
WriteLn('Supported outbound formats:');
WriteLn(' BSO (Binkley-Style Outbound) - primary');
WriteLn(' FrontDoor .MSG style');
WriteLn(' D''Bridge Q-file queue');
WriteLn;
WriteLn('Report bugs: https://github.com/kenj/comet/issues');
end;
procedure ParseArgs;
var
I: Integer;
Arg: string;
begin
CfgPath := DEFAULT_CFG;
CmdMode := cmDaemon;
CallTarget := '';
I := 1;
while I <= ParamCount do
begin
Arg := ParamStr(I);
if (Arg = '-h') or (Arg = '--help') then
begin
CmdMode := cmHelp;
Exit;
end
else if Arg = '-v' then
begin
CmdMode := cmVersion;
Exit;
end
else if Arg = '-c' then
begin
Inc(I);
if I <= ParamCount then
CfgPath := ParamStr(I)
else
begin
WriteLn('Error: -c requires a filename argument');
Halt(1);
end;
end
else if Arg = '-d' then
begin
CometLogSetDebug(True);
CometLogSetConsoleLevel(cllDebug);
end
else if Arg = '-q' then
begin
CometLogSetConsoleLevel(cllError);
end
else if Arg = 'call' then
begin
CmdMode := cmCall;
Inc(I);
if I <= ParamCount then
CallTarget := ParamStr(I)
else
begin
WriteLn('Error: call requires a FidoNet address (e.g., 1:213/723)');
Halt(1);
end;
end
else
begin
WriteLn('Error: Unknown option: ', Arg);
WriteLn('Try: comet -h');
Halt(1);
end;
Inc(I);
end;
end;
procedure RunDaemon;
begin
Daemon := TCometDaemon.Create;
try
if not Daemon.LoadConfig(CfgPath) then
begin
LogFatal('Cannot load config: %s', [CfgPath]);
LogFatal('Run CSETUP to create a configuration, or copy COMET.SAM to comet.cfg');
Exit;
end;
{$IFDEF UNIX}
InstallSignalHandlers;
{$ENDIF}
Daemon.Run;
finally
Daemon.Free;
Daemon := nil;
end;
end;
procedure RunCall;
var
Addr: TCometAddress;
Cfg: TCometConfig;
NodeIdx: Integer;
Sock: TCometSocket;
Host: string;
Port, BinkpPort: Word;
State: TCometSessionState;
XS: TCometXferState;
HSResult: TCometHandshakeResult;
UseComet: Boolean;
Flav: TCometFlavour;
FloPath, PktPath, InDir: string;
FloEntries: TCometFloEntryArray;
I, XResult: Integer;
BResult: TCometBinkpResult;
SendFiles: TStringList;
TF: file;
{$IFDEF UNIX}
Act: SigActionRec;
{$ENDIF}
begin
if not CometStrToAddr(CallTarget, Addr) then
begin
WriteLn('Error: Invalid FidoNet address: ', CallTarget);
Halt(1);
end;
{ Load config }
if not CometCfgLoad(CfgPath, Cfg) then
begin
WriteLn('Error: Cannot load config: ', CfgPath);
Halt(1);
end;
CometCfgApply(Cfg);
{ Find node config for host/port }
NodeIdx := CometCfgFindNode(Cfg, Addr);
if NodeIdx < 0 then
begin
WriteLn('Error: No host configured for ', CometAddrToStr(Addr));
WriteLn('Add a [Node:', CometAddrToStr(Addr), '] section to ', CfgPath);
Halt(1);
end;
Host := Cfg.Nodes[NodeIdx].Host;
Port := Cfg.Nodes[NodeIdx].Port;
if Port = 0 then Port := COMET_PORT;
BinkpPort := Cfg.Nodes[NodeIdx].BinkpPort;
if BinkpPort = 0 then BinkpPort := Cfg.BinkpPort;
UseComet := not Cfg.Nodes[NodeIdx].NoComet;
if Host = '' then
begin
WriteLn('Error: No host/IP configured for ', CometAddrToStr(Addr));
Halt(1);
end;
{$IFDEF UNIX}
{ Ignore SIGPIPE - critical for TCP }
FillChar(Act, SizeOf(Act), 0);
Act.sa_handler := SigActionHandler(SIG_IGN);
fpSigAction(SIGPIPE, @Act, nil);
{$ENDIF}
{ Determine inbound directory }
if Cfg.SecInbound <> '' then
InDir := Cfg.SecInbound
else
InDir := Cfg.Inbound;
{ Ensure directories exist }
CometMakePath(InDir);
if Cfg.TempDir <> '' then CometMakePath(Cfg.TempDir);
{ Try Comet first, then BinkP fallback }
Sock := COMET_TCP_INVALID;
if UseComet then
begin
LogInfo('Calling %s at %s:%d (Comet)', [CometAddrToStr(Addr), Host, Port]);
Sock := CometTcpConnect(Host, Port, 15000);
end;
if (Sock = COMET_TCP_INVALID) and Cfg.BinkpEnabled then
begin
if UseComet then
LogInfo('Comet connect failed, trying BinkP on port %d', [BinkpPort])
else
LogInfo('Calling %s at %s:%d (BinkP)', [CometAddrToStr(Addr), Host, BinkpPort]);
Sock := CometTcpConnect(Host, BinkpPort, 15000);
if Sock <> COMET_TCP_INVALID then
UseComet := False;
end;
if Sock = COMET_TCP_INVALID then
begin
LogError('Cannot connect to %s', [CometAddrToStr(Addr)]);
Halt(1);
end;
if UseComet then
begin
{ ---- Comet protocol session ---- }
CometSessionInit(State, Sock, True, Host, Port);
try
State.OurInit.Password := CometCfgGetPassword(Cfg, Addr);
HSResult := CometHandshake(State, Cfg);
if HSResult <> chrOK then
begin
LogError('Handshake failed: %d', [Ord(HSResult)]);
CometTcpClose(Sock);
Halt(1);
end;
CometXferInit(XS, State, InDir, Cfg.TempDir,
CometAddSlash(Cfg.TempDir) + 'comet-abort.log');
try
{ Send .?UT packet files }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
PktPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOPktExt(Flav));
if CometFileExists(PktPath) then
begin
XResult := CometTransfer(XS, PktPath, '');
if XResult = XFER_ABORT then Break;
if XResult = XFER_OK then
DeleteFile(PktPath);
end;
end;
{ Send files from .FLO flow files }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
FloPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOFloExt(Flav));
if not CometFileExists(FloPath) then Continue;
FloEntries := BSOReadFlo(FloPath);
for I := 0 to High(FloEntries) do
begin
if FloEntries[I].Sent then Continue;
if FloEntries[I].FilePath = '' then Continue;
if not FileExists(FloEntries[I].FilePath) then Continue;
XResult := CometTransfer(XS, FloEntries[I].FilePath, '');
if XResult = XFER_ABORT then Break;
if XResult = XFER_OK then
begin
case FloEntries[I].Action of
csaDelete:
DeleteFile(FloEntries[I].FilePath);
csaTruncate:
begin
AssignFile(TF, FloEntries[I].FilePath);
{$I-} Rewrite(TF, 1); {$I+}
if IOResult = 0 then CloseFile(TF);
end;
end;
BSOMarkSent(FloPath, FloEntries[I].OrigPath);
end;
end;
BSOCleanFlo(FloPath);
end;
{ End of batch - also receives any remaining files from remote }
CometTransfer(XS, '', '');
LogInfo('Session complete: sent %d files (%s), rcvd %d files (%s)',
[XS.FilesSent, CometFormatSize(XS.BytesSent),
XS.FilesRecvd, CometFormatSize(XS.BytesRecvd)]);
finally
CometXferDone(XS);
end;
finally
CometSessionDone(State);
CometTcpClose(Sock);
end;
end
else
begin
{ ---- BinkP fallback session ---- }
SendFiles := TStringList.Create;
try
{ Build file list from BSO outbound }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
PktPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOPktExt(Flav));
if CometFileExists(PktPath) then
SendFiles.Add(PktPath);
FloPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOFloExt(Flav));
if CometFileExists(FloPath) then
begin
FloEntries := BSOReadFlo(FloPath);
for I := 0 to High(FloEntries) do
if not FloEntries[I].Sent and (FloEntries[I].FilePath <> '') and
FileExists(FloEntries[I].FilePath) then
SendFiles.Add(FloEntries[I].FilePath);
end;
end;
BResult := BinkpRunOutbound(Sock, Cfg, Addr,
InDir, Cfg.TempDir, SendFiles);
if BResult.Success then
begin
{ Clean up outbound }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
PktPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOPktExt(Flav));
if FileExists(PktPath) then DeleteFile(PktPath);
FloPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOFloExt(Flav));
if FileExists(FloPath) then BSOCleanFlo(FloPath);
end;
LogInfo('BinkP session complete: sent %d files (%s), rcvd %d files (%s)',
[BResult.FilesSent, CometFormatSize(BResult.BytesSent),
BResult.FilesRecvd, CometFormatSize(BResult.BytesRecvd)]);
end
else
LogError('BinkP session failed: %s', [BResult.ErrorMsg]);
finally
SendFiles.Free;
CometTcpClose(Sock);
end;
end;
end;
{ ---- Entry point ---- }
begin
Daemon := nil;
CometLogSetConsole(True);
CometLogSetConsoleTimestamp(False);
ParseArgs;
case CmdMode of
cmHelp: ShowHelp;
cmVersion: ShowVersion;
cmDaemon: RunDaemon;
cmCall: RunCall;
end;
end.