Some checks failed
Build and Release / build-and-release (push) Failing after 13m54s
Version scheme: Major.Minor.Build-Revision. BinkP gains every major Argus/binkd extension: - PLZ (zlib) compression with adaptive block sizing (4KB→16KB) - NR mode inbound resume via .bkp-part partials (FSP-1029) - ND/NDA deferred cleanup: mid-session abort preserves outbound (FSP-1038) - MBT multi-batch: FREQ response rides same session via second EOB - M_NUL TRF traffic advisory and M_NUL FREQ (FRL-1026) - M_NUL NDL/PHN info strings (new Phone, NodelistFlags config) - RFC 2822 date format for M_NUL TIME - Strict M_GET validation and duplicate-file pre-check - TBinkpPostAuthCallback: host can route InboundDir before transfer (models binkd select_inbound / complete_login) - TCometBinkpResult: Authenticated / AuthMethod fields Comet native extensions keep the protocol ahead of BinkP: - INIT payload adds Location/Time/Phone/NodelistFlags (trailing strings, backward-compatible) - LST file listing: NPKT_LSTREQ/LSTITEM/LSTEND + COPT_LST - Transactional file cleanup: destructive actions deferred until successful session close (matches ND semantics) - Shared CometRFCDateStr across protocols — no drift between BinkP TIME and Comet INIT.Time Daemon: - BinkP inbound now starts unsecure and promotes to secure only after auth (fixes pre-1.2.1 bug where SecInbound was selected unconditionally). TCometFileProvider: GetPartialSize and OpenForReceiveNamed for NR partials; defaults preserve the random-temp scheme for providers that don't track partials (Fastway plugin safe). WebUI: /src/web/ + /src/webui/ backend, modeled after the Argus GUI. Live session activity, outbound polls, FREQ requests, nodelist, config editor, scheduler, SSE event stream.
770 lines
21 KiB
ObjectPascal
770 lines
21 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}
|
|
cmem, { C memory manager - MUST be first }
|
|
cthreads, { pthreads - MUST be before all units }
|
|
BaseUnix,
|
|
{$ENDIF}
|
|
{$IFDEF GO32V2}
|
|
cometlibc, { DJGPP libc stubs for Watt-32 }
|
|
{$ENDIF}
|
|
SysUtils, Classes, cometdef, cometcfg, cometpath, comettcp,
|
|
cometfrm, cometlog, cometses, cometxfer, cometbso, cometbinkp,
|
|
cometfile, cometnodelist, cometed25519, cometdaemon, cometio
|
|
{$IFNDEF GO32V2}, cometweb, cometwebauth, cometwebsse{$ENDIF};
|
|
|
|
const
|
|
DEFAULT_CFG = 'comet.cfg';
|
|
|
|
var
|
|
Daemon: TCometDaemon;
|
|
CfgPath: string;
|
|
CmdMode: (cmDaemon, cmCall, cmHelp, cmVersion, cmKeygen, cmShowKey
|
|
{$IFNDEF GO32V2}, cmWebPassword{$ENDIF});
|
|
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
|
|
if Daemon <> nil then
|
|
Daemon.SignalReload;
|
|
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_FULLVER,
|
|
' - Direct TCP File Transfer for FidoNet');
|
|
WriteLn('Copyright (C) 2026 Ken Johnson');
|
|
WriteLn('Port 24554 - Comet native and BinkP on same port');
|
|
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(' keygen Generate ED25519 keypair for authentication');
|
|
WriteLn(' showkey Print public key derived from configured private key');
|
|
{$IFNDEF GO32V2}
|
|
WriteLn(' webui-password Set the WebUI admin password');
|
|
{$ENDIF}
|
|
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 if Arg = 'keygen' then
|
|
begin
|
|
CmdMode := cmKeygen;
|
|
end
|
|
else if Arg = 'showkey' then
|
|
begin
|
|
CmdMode := cmShowKey;
|
|
end
|
|
{$IFNDEF GO32V2}
|
|
else if Arg = 'webui-password' then
|
|
begin
|
|
CmdMode := cmWebPassword;
|
|
end
|
|
{$ENDIF}
|
|
else
|
|
begin
|
|
WriteLn('Error: Unknown option: ', Arg);
|
|
WriteLn('Try: comet -h');
|
|
Halt(1);
|
|
end;
|
|
|
|
Inc(I);
|
|
end;
|
|
end;
|
|
|
|
|
|
{ ---- Event callback for standalone mode ---- }
|
|
|
|
{$IFNDEF GO32V2}
|
|
procedure HandleLogForWeb(Level: TCometLogLevel; const Msg: string);
|
|
begin
|
|
if Assigned(SSEBus) then
|
|
SSEBus.HandleLog(Ord(Level), Msg);
|
|
end;
|
|
{$ENDIF}
|
|
|
|
procedure HandleEvent(const Event: TCometEventData);
|
|
var
|
|
Pct: Integer;
|
|
CPSStr: string;
|
|
begin
|
|
{ Forward to WebUI SSE bus if active }
|
|
{$IFNDEF GO32V2}
|
|
if Assigned(SSEBus) then
|
|
SSEBus.HandleEvent(Event);
|
|
{$ENDIF}
|
|
|
|
case Event.EventType of
|
|
cetSessionStart:
|
|
LogInfo('Event: session with %s [%s] via %s',
|
|
[Event.RemoteName, Event.RemoteAddr, Event.Protocol]);
|
|
cetSessionAuth:
|
|
case Event.AuthMethod of
|
|
AUTH_ED25519:
|
|
if Event.Encrypted then
|
|
LogInfo('Event: auth ED25519 + encrypted')
|
|
else
|
|
LogInfo('Event: auth ED25519');
|
|
AUTH_CRAM: LogInfo('Event: auth CRAM-MD5');
|
|
AUTH_NOPWD: LogInfo('Event: auth passwordless (NOPWD)');
|
|
end;
|
|
cetFileStart:
|
|
if Event.Sending then
|
|
LogInfo('Event: sending %s (%s)',
|
|
[Event.FileName, CometFormatSize(Event.FileSize)])
|
|
else
|
|
LogInfo('Event: receiving %s (%s)',
|
|
[Event.FileName, CometFormatSize(Event.FileSize)]);
|
|
cetFileProgress:
|
|
begin
|
|
if Event.FileSize > 0 then
|
|
Pct := (Event.Position * 100) div Event.FileSize
|
|
else
|
|
Pct := 0;
|
|
if Event.CPS > 0 then
|
|
CPSStr := Format(' %s/s', [CometFormatSize(Event.CPS)])
|
|
else
|
|
CPSStr := '';
|
|
if Event.Sending then
|
|
Write(#13, Format(' TX: %s %d%% (%s/%s)%s ',
|
|
[Event.FileName, Pct, CometFormatSize(Event.Position),
|
|
CometFormatSize(Event.FileSize), CPSStr]))
|
|
else
|
|
Write(#13, Format(' RX: %s %d%% (%s/%s)%s ',
|
|
[Event.FileName, Pct, CometFormatSize(Event.Position),
|
|
CometFormatSize(Event.FileSize), CPSStr]));
|
|
end;
|
|
cetFileEnd:
|
|
begin
|
|
WriteLn; { New line after progress }
|
|
if Event.CPS > 0 then
|
|
CPSStr := Format(' @ %s/s', [CometFormatSize(Event.CPS)])
|
|
else
|
|
CPSStr := '';
|
|
if Event.Sending then
|
|
LogInfo('Event: sent %s%s', [Event.FileName, CPSStr])
|
|
else
|
|
LogInfo('Event: received %s%s', [Event.FileName, CPSStr]);
|
|
end;
|
|
cetSessionEnd:
|
|
; { Already logged by daemon as "Session complete:" }
|
|
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 RunKeygen;
|
|
var
|
|
Seed: TED25519Seed;
|
|
PubKey: TED25519PublicKey;
|
|
PrivKey: TED25519PrivateKey;
|
|
begin
|
|
WriteLn('Generating ED25519 keypair...');
|
|
WriteLn;
|
|
|
|
ED25519RandomSeed(Seed);
|
|
ED25519CreateKeypair(Seed, PubKey, PrivKey);
|
|
|
|
WriteLn('Add to [System] in your comet.cfg:');
|
|
WriteLn(' PrivateKey = ', ED25519ToHex(Seed, 32));
|
|
WriteLn(' ; PublicKey = ', ED25519ToHex(PubKey, 32));
|
|
WriteLn;
|
|
WriteLn('Give the public key to remote nodes. They add it to their config:');
|
|
WriteLn(' [Node:', CometAddrToStr(Default(TCometAddress)), ']');
|
|
WriteLn(' PublicKey = ', ED25519ToHex(PubKey, 32));
|
|
|
|
{ Wipe sensitive data }
|
|
FillChar(Seed, 32, 0);
|
|
FillChar(PrivKey, 64, 0);
|
|
end;
|
|
|
|
|
|
procedure RunShowKey;
|
|
var
|
|
Cfg: TCometConfig;
|
|
Seed: TED25519Seed;
|
|
PubKey: TED25519PublicKey;
|
|
PrivKey: TED25519PrivateKey;
|
|
begin
|
|
if not CometCfgLoad(CfgPath, Cfg) then
|
|
begin
|
|
WriteLn('Error: Cannot load config: ', CfgPath);
|
|
Halt(1);
|
|
end;
|
|
|
|
if Cfg.PrivateKey = '' then
|
|
begin
|
|
WriteLn('No PrivateKey configured in [System] section.');
|
|
WriteLn('Run "comet keygen" to generate one.');
|
|
Halt(1);
|
|
end;
|
|
|
|
ED25519FromHex(Cfg.PrivateKey, Seed, 32);
|
|
ED25519CreateKeypair(Seed, PubKey, PrivKey);
|
|
|
|
WriteLn(ED25519ToHex(PubKey, 32));
|
|
|
|
FillChar(Seed, 32, 0);
|
|
FillChar(PrivKey, 64, 0);
|
|
end;
|
|
|
|
|
|
{$IFNDEF GO32V2}
|
|
procedure RunWebPassword;
|
|
var
|
|
Password, Salt, Hash: string;
|
|
begin
|
|
Write('Enter new WebUI admin password: ');
|
|
ReadLn(Password);
|
|
if Password = '' then
|
|
begin
|
|
WriteLn('Error: Password cannot be empty');
|
|
Halt(1);
|
|
end;
|
|
if Length(Password) < 6 then
|
|
begin
|
|
WriteLn('Error: Password must be at least 6 characters');
|
|
Halt(1);
|
|
end;
|
|
|
|
Salt := TCometWebAuth.GenerateSalt;
|
|
Hash := TCometWebAuth.HashPassword(Password, Salt);
|
|
|
|
if CometWebCfgSavePassword(CfgPath, Hash, Salt) then
|
|
begin
|
|
WriteLn('Password saved to ', CfgPath);
|
|
WriteLn('Ensure [WebUI] section has Enabled = yes to activate.');
|
|
end
|
|
else
|
|
begin
|
|
WriteLn('Error: Could not write to ', CfgPath);
|
|
WriteLn('Add these lines to your [WebUI] section manually:');
|
|
WriteLn(' PasswordHash = ', Hash);
|
|
WriteLn(' PasswordSalt = ', Salt);
|
|
end;
|
|
end;
|
|
{$ENDIF}
|
|
|
|
|
|
procedure RunCall;
|
|
var
|
|
Addr: TCometAddress;
|
|
Cfg: TCometConfig;
|
|
NodeIdx: Integer;
|
|
Sock: TCometSocket;
|
|
Host: string;
|
|
Port: Word;
|
|
State: TCometSessionState;
|
|
XS: TCometXferState;
|
|
HSResult: TCometHandshakeResult;
|
|
UseComet: Boolean;
|
|
Flav: TCometFlavour;
|
|
FloPath, PktPath, InDir: string;
|
|
FloEntries: TCometFloEntryArray;
|
|
I, XResult: Integer;
|
|
BResult: TCometBinkpResult;
|
|
BEntries: array of TBinkpSendEntry;
|
|
BEntryCount: Integer;
|
|
BEntry: TBinkpSendEntry;
|
|
TF: TextFile;
|
|
NL: TCometNodelist;
|
|
FileIO: TCometLocalFileProvider;
|
|
{$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.
|
|
Priority: per-node config > nodelist > default }
|
|
NodeIdx := CometCfgFindNode(Cfg, Addr);
|
|
Host := '';
|
|
Port := COMET_PORT;
|
|
UseComet := True;
|
|
|
|
if NodeIdx >= 0 then
|
|
begin
|
|
Host := Cfg.Nodes[NodeIdx].Host;
|
|
if Cfg.Nodes[NodeIdx].Port <> 0 then
|
|
Port := Cfg.Nodes[NodeIdx].Port;
|
|
UseComet := not Cfg.Nodes[NodeIdx].NoComet;
|
|
end;
|
|
|
|
{ If no host from config, try nodelist lookup }
|
|
if Host = '' then
|
|
begin
|
|
if Cfg.NodelistDir <> '' then
|
|
begin
|
|
NL := Default(TCometNodelist);
|
|
CometNodelistInit(NL);
|
|
if CometNodelistLoadDir(NL, Cfg.NodelistDir) > 0 then
|
|
begin
|
|
if CometNodelistGetBinkp(NL, Addr, Host, Port) then
|
|
LogInfo('Nodelist lookup: %s -> %s:%d',
|
|
[CometAddrToStr(Addr), Host, Port]);
|
|
end;
|
|
CometNodelistFree(NL);
|
|
end;
|
|
|
|
if Host = '' then
|
|
begin
|
|
WriteLn('Error: No host configured for ', CometAddrToStr(Addr));
|
|
WriteLn('Add a [Node:', CometAddrToStr(Addr), '] section to ', CfgPath);
|
|
WriteLn(' or set Nodelist = /path/to/nodelist/ in config');
|
|
Halt(1);
|
|
end;
|
|
end;
|
|
|
|
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);
|
|
|
|
{ Create file I/O provider for protocol engines }
|
|
FileIO := TCometLocalFileProvider.Create;
|
|
try
|
|
|
|
{ Connect to remote - single port, protocol auto-detected }
|
|
if UseComet then
|
|
LogInfo('Calling %s at %s:%d', [CometAddrToStr(Addr), Host, Port])
|
|
else
|
|
LogInfo('Calling %s at %s:%d (BinkP)', [CometAddrToStr(Addr), Host, Port]);
|
|
Sock := CometTcpConnect(Host, Port, 15000);
|
|
|
|
if Sock = COMET_TCP_INVALID then
|
|
begin
|
|
LogError('Cannot connect to %s', [CometAddrToStr(Addr)]);
|
|
Halt(1);
|
|
end;
|
|
|
|
if UseComet then
|
|
begin
|
|
{ ---- Sniff protocol before sending anything ---- }
|
|
{ Uses MSG_PEEK so the byte stays in the socket buffer.
|
|
BinkP answerers send M_NUL immediately (high bit set).
|
|
Comet answerers send banner immediately (low byte).
|
|
If no data, remote is Comet waiting for our banner. }
|
|
HSResult := CometSniffProtocol(Sock, 5);
|
|
if HSResult = chrBinkP then
|
|
begin
|
|
LogInfo('Remote speaks BinkP - proceeding on same connection');
|
|
UseComet := False;
|
|
end
|
|
else if HSResult = chrDisconnect then
|
|
begin
|
|
LogError('Connection lost during protocol detection');
|
|
CometTcpClose(Sock);
|
|
Halt(1);
|
|
end;
|
|
{ chrOK (Comet banner seen) or chrTimeout (no data) = proceed with Comet }
|
|
end;
|
|
|
|
if UseComet then
|
|
begin
|
|
{ ---- Comet handshake ---- }
|
|
CometSessionInit(State, Sock, True, Host, Port);
|
|
State.OurInit.Password := CometCfgGetPassword(Cfg, Addr);
|
|
HSResult := CometHandshake(State, Cfg);
|
|
if HSResult <> chrOK then
|
|
begin
|
|
LogError('Handshake failed: %d', [Ord(HSResult)]);
|
|
CometSessionDone(State);
|
|
CometTcpClose(Sock);
|
|
Halt(1);
|
|
end;
|
|
end;
|
|
|
|
if UseComet then
|
|
begin
|
|
{ ---- Comet protocol session ---- }
|
|
try
|
|
CometXferInit(XS, State, InDir, Cfg.TempDir,
|
|
CometAddSlash(Cfg.TempDir) + 'comet-abort.log', FileIO);
|
|
XS.FreqDir := Cfg.FreqDir;
|
|
XS.FreqAliases := Cfg.FreqAliases;
|
|
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
|
|
CometFileSent(FloPath, FloEntries[I].OrigPath,
|
|
FloEntries[I].FilePath, FloEntries[I].Action);
|
|
end;
|
|
end;
|
|
|
|
{ Send FREQ requests via NPKT_FREQ for each .REQ line }
|
|
PktPath := BSONodeFile(Cfg.Outbound, Addr,
|
|
Cfg.Addresses[0].Zone, '.req');
|
|
if CometFileExists(PktPath) then
|
|
begin
|
|
AssignFile(TF, PktPath);
|
|
{$I-} Reset(TF); {$I+}
|
|
if IOResult = 0 then
|
|
begin
|
|
while not EOF(TF) do
|
|
begin
|
|
ReadLn(TF, FloPath); { reuse FloPath as temp string }
|
|
FloPath := Trim(FloPath);
|
|
if (FloPath <> '') and (FloPath[1] <> ';') then
|
|
begin
|
|
LogInfo('FREQ request: %s', [FloPath]);
|
|
CometFrameSend(State.Sock, NPKT_FREQ, 0,
|
|
@FloPath[1], Length(FloPath));
|
|
end;
|
|
end;
|
|
CloseFile(TF);
|
|
end;
|
|
DeleteFile(PktPath);
|
|
end;
|
|
|
|
{ Send any FREQ response files queued during the session }
|
|
for I := 0 to XS.FreqCount - 1 do
|
|
begin
|
|
if FileExists(XS.FreqQueue[I]) then
|
|
begin
|
|
XResult := CometTransfer(XS, XS.FreqQueue[I], '');
|
|
if XResult = XFER_ABORT then Break;
|
|
end;
|
|
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 ---- }
|
|
{ Build send queue with .FLO tracking (per-file cleanup on M_GOT) }
|
|
BEntryCount := 0;
|
|
SetLength(BEntries, 64);
|
|
|
|
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
|
|
begin
|
|
if Flav = cfHold then Continue;
|
|
|
|
{ .?UT packet files: delete after send }
|
|
PktPath := BSONodeFile(Cfg.Outbound, Addr,
|
|
Cfg.Addresses[0].Zone, BSOPktExt(Flav));
|
|
if CometFileExists(PktPath) then
|
|
begin
|
|
FillChar(BEntry, SizeOf(BEntry), 0);
|
|
BEntry.FilePath := PktPath;
|
|
BEntry.Action := csaDelete;
|
|
if BEntryCount >= Length(BEntries) then
|
|
SetLength(BEntries, BEntryCount + 32);
|
|
BEntries[BEntryCount] := BEntry;
|
|
Inc(BEntryCount);
|
|
end;
|
|
|
|
{ Files from .FLO }
|
|
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
|
|
begin
|
|
if FloEntries[I].Sent then Continue;
|
|
if (FloEntries[I].FilePath = '') or
|
|
not FileExists(FloEntries[I].FilePath) then Continue;
|
|
|
|
FillChar(BEntry, SizeOf(BEntry), 0);
|
|
BEntry.FilePath := FloEntries[I].FilePath;
|
|
BEntry.FloPath := FloPath;
|
|
BEntry.FloLine := FloEntries[I].OrigPath;
|
|
BEntry.Action := FloEntries[I].Action;
|
|
if BEntryCount >= Length(BEntries) then
|
|
SetLength(BEntries, BEntryCount + 32);
|
|
BEntries[BEntryCount] := BEntry;
|
|
Inc(BEntryCount);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ .REQ file request }
|
|
PktPath := BSONodeFile(Cfg.Outbound, Addr,
|
|
Cfg.Addresses[0].Zone, '.req');
|
|
if CometFileExists(PktPath) then
|
|
begin
|
|
FillChar(BEntry, SizeOf(BEntry), 0);
|
|
BEntry.FilePath := PktPath;
|
|
BEntry.Action := csaDelete;
|
|
if BEntryCount >= Length(BEntries) then
|
|
SetLength(BEntries, BEntryCount + 32);
|
|
BEntries[BEntryCount] := BEntry;
|
|
Inc(BEntryCount);
|
|
end;
|
|
|
|
SetLength(BEntries, BEntryCount);
|
|
BResult := BinkpRunOutbound(Sock, Cfg, Addr,
|
|
InDir, Cfg.TempDir, BEntries, FileIO);
|
|
|
|
if BResult.Success then
|
|
LogInfo('BinkP session complete: sent %d files (%s), rcvd %d files (%s)',
|
|
[BResult.FilesSent, CometFormatSize(BResult.BytesSent),
|
|
BResult.FilesRecvd, CometFormatSize(BResult.BytesRecvd)])
|
|
else
|
|
LogError('BinkP session failed: %s', [BResult.ErrorMsg]);
|
|
|
|
CometTcpClose(Sock);
|
|
end;
|
|
|
|
finally
|
|
FileIO.Free;
|
|
end;
|
|
end;
|
|
|
|
|
|
{ ---- Entry point ---- }
|
|
|
|
begin
|
|
Daemon := nil;
|
|
CometLogSetConsole(True);
|
|
CometLogSetConsoleTimestamp(False);
|
|
CometLogSetEventCallback(@HandleEvent);
|
|
{$IFNDEF GO32V2}
|
|
CometLogSetCallback(@HandleLogForWeb);
|
|
{$ENDIF}
|
|
|
|
ParseArgs;
|
|
|
|
case CmdMode of
|
|
cmHelp: ShowHelp;
|
|
cmVersion: ShowVersion;
|
|
cmDaemon: RunDaemon;
|
|
cmCall: RunCall;
|
|
cmKeygen: RunKeygen;
|
|
cmShowKey: RunShowKey;
|
|
{$IFNDEF GO32V2}
|
|
cmWebPassword: RunWebPassword;
|
|
{$ENDIF}
|
|
end;
|
|
end.
|