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).
933 lines
25 KiB
ObjectPascal
933 lines
25 KiB
ObjectPascal
{
|
|
Comet - Direct TCP File Transfer for FidoNet
|
|
cometdaemon.pas - Multi-session daemon core
|
|
|
|
Manages:
|
|
- Inbound listener: accepts TCP connections on port 26638,
|
|
detects Comet vs BinkP protocol, spawns session threads
|
|
- Outbound scanner: polls BSO outbound directory, initiates
|
|
calls to nodes with pending mail
|
|
- Session threads: one per active connection, runs handshake
|
|
then file transfer, cleans up on completion
|
|
- BSY locking: prevents duplicate sessions to same node
|
|
|
|
Thread model:
|
|
- Main thread runs the listener accept loop
|
|
- Outbound scanner runs on a timer within the main loop
|
|
- Each inbound/outbound session gets its own TThread
|
|
- Thread-safe session list with critical section
|
|
|
|
On DOS (no threads): runs single-session in the main loop.
|
|
|
|
Copyright (C) 2026 Ken Johnson
|
|
License: GPL-2.0
|
|
}
|
|
unit cometdaemon;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
SysUtils, Classes, cometdef, cometcfg, cometpath, comettcp, cometfrm,
|
|
cometses, cometxfer, cometbso, cometbinkp, cometfile, cometlog;
|
|
|
|
const
|
|
COMET_MAX_SESSIONS = 32; { Hard limit on concurrent sessions }
|
|
COMET_BSY_STALE_MIN = 10; { Stale BSY lock age in minutes }
|
|
|
|
type
|
|
{ Session status }
|
|
TCometSessStatus = (
|
|
cssConnecting, { TCP connected, handshake in progress }
|
|
cssActive, { Handshake done, transferring files }
|
|
cssClosing, { Session ending, cleanup in progress }
|
|
cssDone { Thread finished, ready for removal }
|
|
);
|
|
|
|
{ Session direction }
|
|
TCometSessDir = (
|
|
csdInbound, { They called us }
|
|
csdOutbound { We called them }
|
|
);
|
|
|
|
{ Session info (visible to main thread) }
|
|
TCometSessInfo = record
|
|
Active: Boolean;
|
|
Direction: TCometSessDir;
|
|
Status: TCometSessStatus;
|
|
RemoteAddr: TCometAddress; { Remote node address (after handshake)}
|
|
RemoteIP: string; { Remote IP address }
|
|
RemotePort: Word;
|
|
StartTime: TDateTime;
|
|
FilesSent: Integer;
|
|
FilesRecvd: Integer;
|
|
BytesSent: Int64;
|
|
BytesRecvd: Int64;
|
|
end;
|
|
|
|
{ Forward declaration }
|
|
TCometDaemon = class;
|
|
|
|
{ Session thread }
|
|
TCometSessionThread = class(TThread)
|
|
private
|
|
FDaemon: TCometDaemon;
|
|
FSock: TCometSocket;
|
|
FDirection: TCometSessDir;
|
|
FRemoteIP: string;
|
|
FRemotePort: Word;
|
|
FTargetAddr: TCometAddress; { For outbound: who we're calling }
|
|
FSlotIndex: Integer; { Index in daemon's session array }
|
|
FUseComet: Boolean; { True=Comet, False=BinkP only }
|
|
protected
|
|
procedure Execute; override;
|
|
procedure RunInbound;
|
|
procedure RunOutbound;
|
|
procedure RunSession(var State: TCometSessionState);
|
|
public
|
|
constructor Create(ADaemon: TCometDaemon; ASock: TCometSocket;
|
|
ADirection: TCometSessDir; const ARemoteIP: string;
|
|
ARemotePort: Word; ASlotIndex: Integer);
|
|
end;
|
|
|
|
{ Main daemon class }
|
|
TCometDaemon = class
|
|
private
|
|
FCfg: TCometConfig;
|
|
FListenSock: TCometSocket;
|
|
FRunning: Boolean;
|
|
FShutdown: Boolean;
|
|
|
|
{ Session management }
|
|
FSessions: array[0..COMET_MAX_SESSIONS - 1] of TCometSessInfo;
|
|
FThreads: array[0..COMET_MAX_SESSIONS - 1] of TCometSessionThread;
|
|
FSessLock: TRTLCriticalSection;
|
|
|
|
{ Outbound scanning }
|
|
FLastPoll: TDateTime;
|
|
|
|
procedure CleanFinishedSessions;
|
|
function FindFreeSlot: Integer;
|
|
function IsNodeInSession(const Addr: TCometAddress): Boolean;
|
|
procedure HandleInbound;
|
|
procedure PollOutbound;
|
|
procedure CallNode(const Addr: TCometAddress; const Host: string;
|
|
Port: Word);
|
|
public
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
|
|
{ Load configuration }
|
|
function LoadConfig(const CfgPath: string): Boolean;
|
|
|
|
{ Start the daemon (blocking - runs until shutdown) }
|
|
procedure Run;
|
|
|
|
{ Signal shutdown (call from signal handler or other thread) }
|
|
procedure Shutdown;
|
|
|
|
{ Get current session info for status display }
|
|
function GetSessionInfo(Index: Integer): TCometSessInfo;
|
|
function GetActiveSessionCount: Integer;
|
|
|
|
{ Update a session slot (called by session threads) }
|
|
procedure UpdateSession(SlotIndex: Integer; const Info: TCometSessInfo);
|
|
procedure MarkSessionDone(SlotIndex: Integer);
|
|
|
|
property Config: TCometConfig read FCfg;
|
|
property Running: Boolean read FRunning;
|
|
end;
|
|
|
|
|
|
implementation
|
|
|
|
|
|
{ ---- TCometSessionThread ---- }
|
|
|
|
constructor TCometSessionThread.Create(ADaemon: TCometDaemon;
|
|
ASock: TCometSocket; ADirection: TCometSessDir;
|
|
const ARemoteIP: string; ARemotePort: Word; ASlotIndex: Integer);
|
|
begin
|
|
inherited Create(True); { Create suspended }
|
|
FreeOnTerminate := True;
|
|
FDaemon := ADaemon;
|
|
FSock := ASock;
|
|
FDirection := ADirection;
|
|
FRemoteIP := ARemoteIP;
|
|
FRemotePort := ARemotePort;
|
|
FSlotIndex := ASlotIndex;
|
|
end;
|
|
|
|
procedure TCometSessionThread.Execute;
|
|
begin
|
|
try
|
|
case FDirection of
|
|
csdInbound: RunInbound;
|
|
csdOutbound: RunOutbound;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
LogError('Session thread exception: %s', [E.Message]);
|
|
end;
|
|
|
|
{ Clean up socket }
|
|
CometTcpClose(FSock);
|
|
|
|
{ Unlock BSY if outbound }
|
|
if FDirection = csdOutbound then
|
|
BSOUnlock(FDaemon.FCfg.Outbound, FTargetAddr,
|
|
FDaemon.FCfg.Addresses[0].Zone);
|
|
|
|
{ Mark slot as done }
|
|
FDaemon.MarkSessionDone(FSlotIndex);
|
|
end;
|
|
|
|
|
|
procedure TCometSessionThread.RunInbound;
|
|
var
|
|
State: TCometSessionState;
|
|
PeekBuf: string;
|
|
HSResult: TCometHandshakeResult;
|
|
Info: TCometSessInfo;
|
|
BResult: TCometBinkpResult;
|
|
InDir: string;
|
|
begin
|
|
LogInfo('Inbound connection from %s:%d', [FRemoteIP, FRemotePort]);
|
|
|
|
{ Step 1: Read banner to detect protocol }
|
|
if not CometSendBanner(FSock) then
|
|
begin
|
|
LogWarning('Failed to send banner to %s', [FRemoteIP]);
|
|
Exit;
|
|
end;
|
|
|
|
HSResult := CometRecvBanner(FSock, FDaemon.FCfg.TimeoutHandshake, PeekBuf);
|
|
|
|
case HSResult of
|
|
chrBinkP:
|
|
begin
|
|
LogInfo('BinkP detected from %s - running BinkP session', [FRemoteIP]);
|
|
if FDaemon.FCfg.SecInbound <> '' then
|
|
InDir := FDaemon.FCfg.SecInbound
|
|
else
|
|
InDir := FDaemon.FCfg.Inbound;
|
|
BResult := BinkpRunInbound(FSock, FDaemon.FCfg,
|
|
InDir, FDaemon.FCfg.TempDir, PeekBuf);
|
|
if BResult.Success then
|
|
begin
|
|
Info := FDaemon.GetSessionInfo(FSlotIndex);
|
|
Info.FilesSent := BResult.FilesSent;
|
|
Info.FilesRecvd := BResult.FilesRecvd;
|
|
Info.BytesSent := BResult.BytesSent;
|
|
Info.BytesRecvd := BResult.BytesRecvd;
|
|
if Length(BResult.RemoteAddrs) > 0 then
|
|
Info.RemoteAddr := BResult.RemoteAddrs[0];
|
|
Info.Status := cssClosing;
|
|
FDaemon.UpdateSession(FSlotIndex, Info);
|
|
end;
|
|
Exit;
|
|
end;
|
|
chrOK:
|
|
; { Comet protocol - continue }
|
|
else
|
|
LogWarning('Banner exchange failed with %s: %s',
|
|
[FRemoteIP, IntToStr(Ord(HSResult))]);
|
|
Exit;
|
|
end;
|
|
|
|
{ Step 2: Full Comet handshake (INIT/INITACK) }
|
|
CometSessionInit(State, FSock, False, FRemoteIP, FRemotePort);
|
|
try
|
|
HSResult := CometHandshake(State, FDaemon.FCfg);
|
|
if HSResult <> chrOK then
|
|
begin
|
|
LogWarning('Handshake failed with %s: %s',
|
|
[FRemoteIP, IntToStr(Ord(HSResult))]);
|
|
Exit;
|
|
end;
|
|
|
|
{ Update session info with remote address }
|
|
if Length(State.RemoteInit.Addresses) > 0 then
|
|
begin
|
|
FTargetAddr := State.RemoteInit.Addresses[0];
|
|
Info := FDaemon.GetSessionInfo(FSlotIndex);
|
|
Info.RemoteAddr := FTargetAddr;
|
|
Info.Status := cssActive;
|
|
FDaemon.UpdateSession(FSlotIndex, Info);
|
|
end;
|
|
|
|
{ Step 3: Run file transfer }
|
|
RunSession(State);
|
|
|
|
finally
|
|
CometSessionDone(State);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TCometSessionThread.RunOutbound;
|
|
var
|
|
State: TCometSessionState;
|
|
HSResult: TCometHandshakeResult;
|
|
Info: TCometSessInfo;
|
|
BResult: TCometBinkpResult;
|
|
InDir: string;
|
|
SendFiles: TStringList;
|
|
Flav: TCometFlavour;
|
|
FloPath, PktPath: string;
|
|
FloEntries: TCometFloEntryArray;
|
|
I: Integer;
|
|
begin
|
|
LogInfo('Outbound call to %s at %s:%d (%s)',
|
|
[CometAddrToStr(FTargetAddr), FRemoteIP, FRemotePort,
|
|
BoolToStr(FUseComet, 'Comet', 'BinkP')]);
|
|
|
|
if FUseComet then
|
|
begin
|
|
{ ---- Comet protocol path ---- }
|
|
CometSessionInit(State, FSock, True, FRemoteIP, FRemotePort);
|
|
try
|
|
State.OurInit.Password := CometCfgGetPassword(FDaemon.FCfg, FTargetAddr);
|
|
|
|
HSResult := CometHandshake(State, FDaemon.FCfg);
|
|
if HSResult <> chrOK then
|
|
begin
|
|
LogWarning('Outbound handshake failed with %s: %s',
|
|
[CometAddrToStr(FTargetAddr), IntToStr(Ord(HSResult))]);
|
|
Exit;
|
|
end;
|
|
|
|
Info := FDaemon.GetSessionInfo(FSlotIndex);
|
|
Info.Status := cssActive;
|
|
FDaemon.UpdateSession(FSlotIndex, Info);
|
|
|
|
RunSession(State);
|
|
finally
|
|
CometSessionDone(State);
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
{ ---- BinkP fallback path ---- }
|
|
if FDaemon.FCfg.SecInbound <> '' then
|
|
InDir := FDaemon.FCfg.SecInbound
|
|
else
|
|
InDir := FDaemon.FCfg.Inbound;
|
|
|
|
{ Build list of files to send from BSO outbound }
|
|
SendFiles := TStringList.Create;
|
|
try
|
|
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
|
|
begin
|
|
if Flav = cfHold then Continue;
|
|
|
|
{ Add .?UT packet files }
|
|
PktPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
|
|
FDaemon.FCfg.Addresses[0].Zone, BSOPktExt(Flav));
|
|
if CometFileExists(PktPath) then
|
|
SendFiles.Add(PktPath);
|
|
|
|
{ Add files from .FLO flow files }
|
|
FloPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
|
|
FDaemon.FCfg.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 <> '') and
|
|
FileExists(FloEntries[I].FilePath) then
|
|
SendFiles.Add(FloEntries[I].FilePath);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
Info := FDaemon.GetSessionInfo(FSlotIndex);
|
|
Info.Status := cssActive;
|
|
FDaemon.UpdateSession(FSlotIndex, Info);
|
|
|
|
BResult := BinkpRunOutbound(FSock, FDaemon.FCfg, FTargetAddr,
|
|
InDir, FDaemon.FCfg.TempDir, SendFiles);
|
|
|
|
if BResult.Success then
|
|
begin
|
|
{ Mark sent files in flow files }
|
|
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
|
|
begin
|
|
if Flav = cfHold then Continue;
|
|
PktPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
|
|
FDaemon.FCfg.Addresses[0].Zone, BSOPktExt(Flav));
|
|
if FileExists(PktPath) then
|
|
DeleteFile(PktPath);
|
|
|
|
FloPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
|
|
FDaemon.FCfg.Addresses[0].Zone, BSOFloExt(Flav));
|
|
if FileExists(FloPath) then
|
|
BSOCleanFlo(FloPath);
|
|
end;
|
|
|
|
Info := FDaemon.GetSessionInfo(FSlotIndex);
|
|
Info.FilesSent := BResult.FilesSent;
|
|
Info.FilesRecvd := BResult.FilesRecvd;
|
|
Info.BytesSent := BResult.BytesSent;
|
|
Info.BytesRecvd := BResult.BytesRecvd;
|
|
Info.Status := cssClosing;
|
|
FDaemon.UpdateSession(FSlotIndex, Info);
|
|
end;
|
|
finally
|
|
SendFiles.Free;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TCometSessionThread.RunSession(var State: TCometSessionState);
|
|
var
|
|
XS: TCometXferState;
|
|
FloPath: string;
|
|
FloEntries: TCometFloEntryArray;
|
|
Flav: TCometFlavour;
|
|
PktPath: string;
|
|
I: Integer;
|
|
XResult: Integer;
|
|
InboundDir: string;
|
|
TF: file;
|
|
Info: TCometSessInfo;
|
|
begin
|
|
{ Determine inbound directory: secure if password matched }
|
|
if State.Session.PasswordOk and (FDaemon.FCfg.SecInbound <> '') then
|
|
InboundDir := FDaemon.FCfg.SecInbound
|
|
else
|
|
InboundDir := FDaemon.FCfg.Inbound;
|
|
|
|
CometXferInit(XS, State, InboundDir, FDaemon.FCfg.TempDir,
|
|
CometAddSlash(FDaemon.FCfg.TempDir) + 'comet-abort.log');
|
|
try
|
|
if FDirection = csdOutbound then
|
|
begin
|
|
{ Send our files to the remote }
|
|
{ First send any .?UT packet files }
|
|
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
|
|
begin
|
|
if Flav = cfHold then Continue; { Don't send hold mail }
|
|
|
|
PktPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
|
|
FDaemon.FCfg.Addresses[0].Zone, BSOPktExt(Flav));
|
|
|
|
if CometFileExists(PktPath) then
|
|
begin
|
|
XResult := CometTransfer(XS, PktPath, '');
|
|
if XResult = XFER_ABORT then Exit;
|
|
if XResult = XFER_OK then
|
|
DeleteFile(PktPath);
|
|
end;
|
|
end;
|
|
|
|
{ Then send files from .FLO flow files }
|
|
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
|
|
begin
|
|
if Flav = cfHold then Continue;
|
|
|
|
FloPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
|
|
FDaemon.FCfg.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 Exit;
|
|
|
|
if XResult = XFER_OK then
|
|
begin
|
|
{ Mark sent in flow file }
|
|
case FloEntries[I].Action of
|
|
csaDelete:
|
|
DeleteFile(FloEntries[I].FilePath);
|
|
csaTruncate:
|
|
begin
|
|
{ Truncate to zero }
|
|
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;
|
|
|
|
{ Clean processed entries }
|
|
BSOCleanFlo(FloPath);
|
|
end;
|
|
end;
|
|
|
|
{ Signal end of batch - this also handles receiving remaining files }
|
|
CometTransfer(XS, '', '');
|
|
|
|
{ Update final stats }
|
|
Info := FDaemon.GetSessionInfo(FSlotIndex);
|
|
Info.FilesSent := XS.FilesSent;
|
|
Info.FilesRecvd := XS.FilesRecvd;
|
|
Info.BytesSent := XS.BytesSent;
|
|
Info.BytesRecvd := XS.BytesRecvd;
|
|
Info.Status := cssClosing;
|
|
FDaemon.UpdateSession(FSlotIndex, Info);
|
|
|
|
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;
|
|
end;
|
|
|
|
|
|
{ ---- TCometDaemon ---- }
|
|
|
|
constructor TCometDaemon.Create;
|
|
var
|
|
I: Integer;
|
|
begin
|
|
inherited Create;
|
|
FListenSock := COMET_TCP_INVALID;
|
|
FRunning := False;
|
|
FShutdown := False;
|
|
FLastPoll := 0;
|
|
InitCriticalSection(FSessLock);
|
|
|
|
for I := 0 to COMET_MAX_SESSIONS - 1 do
|
|
begin
|
|
FillChar(FSessions[I], SizeOf(TCometSessInfo), 0);
|
|
FThreads[I] := nil;
|
|
end;
|
|
end;
|
|
|
|
destructor TCometDaemon.Destroy;
|
|
begin
|
|
if FListenSock <> COMET_TCP_INVALID then
|
|
CometTcpClose(FListenSock);
|
|
DoneCriticalSection(FSessLock);
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
function TCometDaemon.LoadConfig(const CfgPath: string): Boolean;
|
|
begin
|
|
Result := CometCfgLoad(CfgPath, FCfg);
|
|
if Result then
|
|
CometCfgApply(FCfg);
|
|
end;
|
|
|
|
|
|
procedure TCometDaemon.Run;
|
|
var
|
|
Deadline: TDateTime;
|
|
begin
|
|
if Length(FCfg.Addresses) = 0 then
|
|
begin
|
|
LogFatal('No FidoNet addresses configured');
|
|
Exit;
|
|
end;
|
|
|
|
if FCfg.Inbound = '' then
|
|
begin
|
|
LogFatal('No inbound directory configured');
|
|
Exit;
|
|
end;
|
|
|
|
if FCfg.Outbound = '' then
|
|
begin
|
|
LogFatal('No outbound directory configured');
|
|
Exit;
|
|
end;
|
|
|
|
{ Ensure directories exist }
|
|
CometMakePath(FCfg.Inbound);
|
|
if FCfg.SecInbound <> '' then
|
|
CometMakePath(FCfg.SecInbound);
|
|
CometMakePath(FCfg.Outbound);
|
|
if FCfg.TempDir <> '' then
|
|
CometMakePath(FCfg.TempDir);
|
|
|
|
{ Start listener }
|
|
FListenSock := CometTcpListen(FCfg.BindAddr, FCfg.ListenPort);
|
|
if FListenSock = COMET_TCP_INVALID then
|
|
begin
|
|
LogFatal('Cannot listen on port %d', [FCfg.ListenPort]);
|
|
Exit;
|
|
end;
|
|
|
|
LogInfo('Comet %s started - listening on port %d', [COMET_VERSION, FCfg.ListenPort]);
|
|
LogInfo('Address: %s System: %s SysOp: %s',
|
|
[CometAddrToStr(FCfg.Addresses[0]), FCfg.SysName, FCfg.SysOp]);
|
|
|
|
FRunning := True;
|
|
|
|
{ Main loop }
|
|
while not FShutdown do
|
|
begin
|
|
{ Accept inbound connections }
|
|
HandleInbound;
|
|
|
|
{ Poll outbound on interval }
|
|
if (FCfg.PollInterval > 0) and
|
|
(Now > FLastPoll + (FCfg.PollInterval / 86400.0)) then
|
|
begin
|
|
PollOutbound;
|
|
FLastPoll := Now;
|
|
end;
|
|
|
|
{ Clean up finished session threads }
|
|
CleanFinishedSessions;
|
|
|
|
{ Brief sleep to prevent CPU spin }
|
|
Sleep(100);
|
|
end;
|
|
|
|
{ Shutdown: wait for active sessions }
|
|
LogInfo('Shutting down - waiting for active sessions...');
|
|
{ Give sessions a few seconds to finish }
|
|
begin
|
|
Deadline := Now + (10.0 / 86400.0);
|
|
while (GetActiveSessionCount > 0) and (Now < Deadline) do
|
|
begin
|
|
CleanFinishedSessions;
|
|
Sleep(200);
|
|
end;
|
|
end;
|
|
|
|
if FListenSock <> COMET_TCP_INVALID then
|
|
CometTcpClose(FListenSock);
|
|
|
|
FRunning := False;
|
|
LogInfo('Comet daemon stopped');
|
|
end;
|
|
|
|
|
|
procedure TCometDaemon.Shutdown;
|
|
begin
|
|
FShutdown := True;
|
|
end;
|
|
|
|
|
|
{ ---- Session slot management ---- }
|
|
|
|
function TCometDaemon.FindFreeSlot: Integer;
|
|
var
|
|
I: Integer;
|
|
begin
|
|
Result := -1;
|
|
for I := 0 to COMET_MAX_SESSIONS - 1 do
|
|
begin
|
|
if not FSessions[I].Active then
|
|
begin
|
|
Result := I;
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TCometDaemon.IsNodeInSession(const Addr: TCometAddress): Boolean;
|
|
var
|
|
I: Integer;
|
|
begin
|
|
Result := False;
|
|
EnterCriticalSection(FSessLock);
|
|
try
|
|
for I := 0 to COMET_MAX_SESSIONS - 1 do
|
|
begin
|
|
if FSessions[I].Active and
|
|
(FSessions[I].RemoteAddr.Zone = Addr.Zone) and
|
|
(FSessions[I].RemoteAddr.Net = Addr.Net) and
|
|
(FSessions[I].RemoteAddr.Node = Addr.Node) and
|
|
(FSessions[I].RemoteAddr.Point = Addr.Point) then
|
|
begin
|
|
Result := True;
|
|
Exit;
|
|
end;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection(FSessLock);
|
|
end;
|
|
end;
|
|
|
|
function TCometDaemon.GetSessionInfo(Index: Integer): TCometSessInfo;
|
|
begin
|
|
Result := Default(TCometSessInfo);
|
|
if (Index < 0) or (Index >= COMET_MAX_SESSIONS) then Exit;
|
|
EnterCriticalSection(FSessLock);
|
|
try
|
|
Result := FSessions[Index];
|
|
finally
|
|
LeaveCriticalSection(FSessLock);
|
|
end;
|
|
end;
|
|
|
|
function TCometDaemon.GetActiveSessionCount: Integer;
|
|
var
|
|
I: Integer;
|
|
begin
|
|
Result := 0;
|
|
EnterCriticalSection(FSessLock);
|
|
try
|
|
for I := 0 to COMET_MAX_SESSIONS - 1 do
|
|
if FSessions[I].Active then Inc(Result);
|
|
finally
|
|
LeaveCriticalSection(FSessLock);
|
|
end;
|
|
end;
|
|
|
|
procedure TCometDaemon.UpdateSession(SlotIndex: Integer;
|
|
const Info: TCometSessInfo);
|
|
begin
|
|
if (SlotIndex < 0) or (SlotIndex >= COMET_MAX_SESSIONS) then Exit;
|
|
EnterCriticalSection(FSessLock);
|
|
try
|
|
FSessions[SlotIndex] := Info;
|
|
finally
|
|
LeaveCriticalSection(FSessLock);
|
|
end;
|
|
end;
|
|
|
|
procedure TCometDaemon.MarkSessionDone(SlotIndex: Integer);
|
|
begin
|
|
if (SlotIndex < 0) or (SlotIndex >= COMET_MAX_SESSIONS) then Exit;
|
|
EnterCriticalSection(FSessLock);
|
|
try
|
|
FSessions[SlotIndex].Status := cssDone;
|
|
finally
|
|
LeaveCriticalSection(FSessLock);
|
|
end;
|
|
end;
|
|
|
|
procedure TCometDaemon.CleanFinishedSessions;
|
|
var
|
|
I: Integer;
|
|
begin
|
|
EnterCriticalSection(FSessLock);
|
|
try
|
|
for I := 0 to COMET_MAX_SESSIONS - 1 do
|
|
begin
|
|
if FSessions[I].Active and (FSessions[I].Status = cssDone) then
|
|
begin
|
|
FSessions[I].Active := False;
|
|
FThreads[I] := nil; { Thread is FreeOnTerminate }
|
|
end;
|
|
end;
|
|
finally
|
|
LeaveCriticalSection(FSessLock);
|
|
end;
|
|
end;
|
|
|
|
|
|
{ ---- Inbound handling ---- }
|
|
|
|
procedure TCometDaemon.HandleInbound;
|
|
var
|
|
ConnInfo: TCometConnInfo;
|
|
Slot: Integer;
|
|
SessInfo: TCometSessInfo;
|
|
Thread: TCometSessionThread;
|
|
begin
|
|
if not CometTcpAccept(FListenSock, ConnInfo) then Exit;
|
|
|
|
{ Check session limit }
|
|
EnterCriticalSection(FSessLock);
|
|
try
|
|
if GetActiveSessionCount >= FCfg.MaxSessions then
|
|
begin
|
|
LogWarning('Max sessions reached, rejecting %s:%d',
|
|
[ConnInfo.RemoteIP, ConnInfo.RemotePort]);
|
|
CometTcpClose(ConnInfo.Socket);
|
|
Exit;
|
|
end;
|
|
|
|
Slot := FindFreeSlot;
|
|
if Slot < 0 then
|
|
begin
|
|
CometTcpClose(ConnInfo.Socket);
|
|
Exit;
|
|
end;
|
|
|
|
{ Reserve the slot }
|
|
FillChar(SessInfo, SizeOf(SessInfo), 0);
|
|
SessInfo.Active := True;
|
|
SessInfo.Direction := csdInbound;
|
|
SessInfo.Status := cssConnecting;
|
|
SessInfo.RemoteIP := ConnInfo.RemoteIP;
|
|
SessInfo.RemotePort := ConnInfo.RemotePort;
|
|
SessInfo.StartTime := Now;
|
|
FSessions[Slot] := SessInfo;
|
|
|
|
{ Create and start session thread }
|
|
Thread := TCometSessionThread.Create(Self, ConnInfo.Socket,
|
|
csdInbound, ConnInfo.RemoteIP, ConnInfo.RemotePort, Slot);
|
|
FThreads[Slot] := Thread;
|
|
finally
|
|
LeaveCriticalSection(FSessLock);
|
|
end;
|
|
|
|
Thread.Start;
|
|
end;
|
|
|
|
|
|
{ ---- Outbound polling ---- }
|
|
|
|
procedure TCometDaemon.PollOutbound;
|
|
var
|
|
Items: TCometOutboundItemArray;
|
|
I: Integer;
|
|
NodeIdx: Integer;
|
|
Host: string;
|
|
Port: Word;
|
|
begin
|
|
if FCfg.Outbound = '' then Exit;
|
|
|
|
Items := BSOScanOutbound(FCfg.Outbound, FCfg.Addresses[0].Zone);
|
|
|
|
for I := 0 to High(Items) do
|
|
begin
|
|
{ Skip hold-only, busy, or already-in-session nodes }
|
|
if Items[I].IsBusy then Continue;
|
|
if (not Items[I].HasMail) and (not Items[I].HasFiles) then Continue;
|
|
if IsNodeInSession(Items[I].Address) then Continue;
|
|
|
|
{ Check session limit }
|
|
if GetActiveSessionCount >= FCfg.MaxSessions then Break;
|
|
|
|
{ Determine how to reach this node }
|
|
NodeIdx := CometCfgFindNode(FCfg, Items[I].Address);
|
|
if NodeIdx >= 0 then
|
|
begin
|
|
Host := FCfg.Nodes[NodeIdx].Host;
|
|
Port := FCfg.Nodes[NodeIdx].Port;
|
|
if Port = 0 then Port := COMET_PORT;
|
|
end
|
|
else
|
|
begin
|
|
{ No config entry - would need nodelist lookup.
|
|
For now, skip nodes without explicit config. }
|
|
Continue;
|
|
end;
|
|
|
|
if Host = '' then Continue; { No IP/hostname known }
|
|
|
|
CallNode(Items[I].Address, Host, Port);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TCometDaemon.CallNode(const Addr: TCometAddress;
|
|
const Host: string; Port: Word);
|
|
var
|
|
Sock: TCometSocket;
|
|
Slot: Integer;
|
|
SessInfo: TCometSessInfo;
|
|
Thread: TCometSessionThread;
|
|
NodeIdx: Integer;
|
|
BinkpPort: Word;
|
|
UseComet: Boolean;
|
|
begin
|
|
{ Try to lock the node }
|
|
if not BSOLock(FCfg.Outbound, Addr, FCfg.Addresses[0].Zone,
|
|
COMET_BSY_STALE_MIN) then
|
|
begin
|
|
LogDebug('Node %s is busy', [CometAddrToStr(Addr)]);
|
|
Exit;
|
|
end;
|
|
|
|
{ Check per-node settings for protocol preference }
|
|
NodeIdx := CometCfgFindNode(FCfg, Addr);
|
|
UseComet := True;
|
|
BinkpPort := FCfg.BinkpPort;
|
|
if NodeIdx >= 0 then
|
|
begin
|
|
if FCfg.Nodes[NodeIdx].NoComet then UseComet := False;
|
|
if FCfg.Nodes[NodeIdx].BinkpPort <> 0 then
|
|
BinkpPort := FCfg.Nodes[NodeIdx].BinkpPort;
|
|
end;
|
|
|
|
Sock := COMET_TCP_INVALID;
|
|
|
|
{ Try Comet protocol first (unless NoComet set for this node) }
|
|
if UseComet then
|
|
begin
|
|
LogInfo('Calling %s at %s:%d (Comet)', [CometAddrToStr(Addr), Host, Port]);
|
|
Sock := CometTcpConnect(Host, Port, 15000);
|
|
end;
|
|
|
|
{ If Comet connect failed and BinkP is enabled, try BinkP port }
|
|
if (Sock = COMET_TCP_INVALID) and FCfg.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
|
|
begin
|
|
LogWarning('Cannot connect to %s at %s (tried ports %d and %d)',
|
|
[CometAddrToStr(Addr), Host, Port, BinkpPort]);
|
|
BSOUnlock(FCfg.Outbound, Addr, FCfg.Addresses[0].Zone);
|
|
Exit;
|
|
end;
|
|
|
|
{ Connected on BinkP port - mark as non-Comet so the session thread
|
|
knows to run BinkP directly instead of Comet handshake }
|
|
UseComet := False;
|
|
end;
|
|
|
|
if Sock = COMET_TCP_INVALID then
|
|
begin
|
|
LogWarning('Cannot connect to %s at %s:%d',
|
|
[CometAddrToStr(Addr), Host, Port]);
|
|
BSOUnlock(FCfg.Outbound, Addr, FCfg.Addresses[0].Zone);
|
|
Exit;
|
|
end;
|
|
|
|
{ Reserve session slot }
|
|
EnterCriticalSection(FSessLock);
|
|
try
|
|
Slot := FindFreeSlot;
|
|
if Slot < 0 then
|
|
begin
|
|
CometTcpClose(Sock);
|
|
BSOUnlock(FCfg.Outbound, Addr, FCfg.Addresses[0].Zone);
|
|
Exit;
|
|
end;
|
|
|
|
FillChar(SessInfo, SizeOf(SessInfo), 0);
|
|
SessInfo.Active := True;
|
|
SessInfo.Direction := csdOutbound;
|
|
SessInfo.Status := cssConnecting;
|
|
SessInfo.RemoteAddr := Addr;
|
|
SessInfo.RemoteIP := Host;
|
|
SessInfo.RemotePort := Port;
|
|
SessInfo.StartTime := Now;
|
|
FSessions[Slot] := SessInfo;
|
|
|
|
Thread := TCometSessionThread.Create(Self, Sock,
|
|
csdOutbound, Host, Port, Slot);
|
|
Thread.FTargetAddr := Addr;
|
|
Thread.FUseComet := UseComet;
|
|
FThreads[Slot] := Thread;
|
|
finally
|
|
LeaveCriticalSection(FSessLock);
|
|
end;
|
|
|
|
Thread.Start;
|
|
end;
|
|
|
|
|
|
end.
|