Files
fpc-comet/examples/example_outbound.pas
Ken Johnson 24a2350a51 Live interop fixes against the existing Comet daemon
End-to-end test against the canonical Comet daemon
(comet 1.2.1, both bbsnode2 and a local instance from
build/linux/comet) surfaced three wire-level mismatches.
fpc-comet now talks the existing daemon's protocol exactly:

  1) BOTH sides emit NPKT_INITACK.  My driver only had
     the inbound (answerer) sending INITACK.  The existing
     daemon waits for the originator's INITACK after
     sending its own; without it the answerer hangs in
     its post-INIT wait loop and times out (TimeoutSecs).
     SendOurInitAck now fires on the outbound side too,
     after authentication completes.

  2) Transfer-phase frame routing widened.  The peer can
     race ahead and send transfer frames (FINFO end-of-
     batch, FINFOACK, etc.) before we've fully transitioned
     out of cmpAuth.  Driver now routes transfer-typed
     frames to the xfer engine in any post-auth phase
     (cmpAuth / cmpKeyExchange / cmpTransfer / cmpShutdown)
     instead of dropping them with "unhandled frame in
     phase 3" warnings.

  3) TComFsProvider.NextOutbound now computes SHA-256 of
     each outbound file before returning the TCometSendItem.
     Previously left Item.SHA256 as zeros, which the receiver
     compared against its own freshly-computed hash and
     correctly reported "SHA-256 mismatch" -- the bytes
     transferred fine, the hash advertised in FINFO didn't.
     Added HashFileSHA256 helper.

example_outbound also takes:
  - CM_AKA env var for outbound local AKA (e.g. "1:213/725")
  - CM_PRIVKEY env var for ED25519 private-key seed (hex)
  - CM_LOG=trace for verbose diagnostics
  - CM_REQUEST="*.txt" to FREQ files from peer
  - CM_LIST="*" to LSTREQ a listing
  - File argument now optional (handshake-only is allowed)

Validated against bbsnode2 (1:213/721, FreeBSD x64,
unmodified canonical Comet daemon code at port 26638) AND
against a local Linux build of the same code:

  bbsnode2 dial:   ED25519 verified, 82-byte file accepted
                   + EOF acked, clean cmpDone, byte-identical
                   landing in inbound dir.
  local dial:      NOPWD path, same end-to-end success.

12 unit tests still pass; all 7 cross-targets clean.

That closes the wire-format gap.  fpc-comet v0.1.0 is
interop-validated against the canonical implementation.
2026-04-22 14:58:36 -07:00

150 lines
4.4 KiB
ObjectPascal

{ example_outbound -- connect to a remote Comet peer and send
one or more files using the library's reference TCP
transport + filesystem provider.
Build:
fpc -Mobjfpc -Sh -Fu../src -Fu../../fpc-msgbase/src \
-Fu../../fpc-log/src example_outbound.pas
Run:
./example_outbound host[:port] inbound-dir file [file ...]
Example:
./example_outbound 192.0.2.5:24554 ./inbound /tmp/TEST.PKT
inbound-dir is where files received during the bidir batch
land (Comet sessions are bidirectional even when you call
the peer just to send).
UNIX only (Linux / FreeBSD) -- reference transport is
UNIX-only for v0.1.x. Windows / OS2 / DOS consumers plug
in their own IComTransport. }
program example_outbound;
{$mode objfpc}{$H+}
uses
SysUtils,
log.types, log.console,
mb.address,
cm.types, cm.config, cm.transport, cm.provider, cm.driver,
cm.xfer, cm.transport.tcp, cm.provider.fs;
procedure Usage;
begin
WriteLn('usage: example_outbound host[:port] inbound-dir file [file ...]');
WriteLn(' set CM_PASSWORD env to authenticate (else session goes NOPWD)');
Halt(2);
end;
var
HostArg, InboundDir, Host: string;
Port: Word;
ColonPos, I: Integer;
Cfg: TCometSessionConfig;
Transport: TComTcpTransport;
Provider: TComFsProvider;
Session: TComSession;
Xfer: TCometXfer;
Logger: TConsoleLogger;
R: TCometSessionResult;
begin
if ParamCount < 2 then Usage;
HostArg := ParamStr(1);
InboundDir := ParamStr(2);
Port := 24554;
ColonPos := Pos(':', HostArg);
if ColonPos > 0 then
begin
Port := StrToIntDef(Copy(HostArg, ColonPos + 1, Length(HostArg)), 24554);
Host := Copy(HostArg, 1, ColonPos - 1);
end
else
Host := HostArg;
ForceDirectories(InboundDir);
if GetEnvironmentVariable('CM_LOG') = 'trace' then
Logger := TConsoleLogger.Create(llTrace)
else
Logger := TConsoleLogger.Create(llDebug);
Provider := TComFsProvider.Create(InboundDir,
IncludeTrailingPathDelimiter(InboundDir) + 'tmp');
for I := 3 to ParamCount do
Provider.Enqueue(ParamStr(I), cmFsKeep);
try
Transport := TComTcpTransport.CreateClient(Host, Port);
except
on E: Exception do
begin
WriteLn('connect failed: ', E.Message);
Halt(3);
end;
end;
CMConfigDefaults(Cfg);
SetLength(Cfg.LocalAddrs, 1);
Cfg.LocalAddrs[0] := MakeFTNAddress(1, 218, 720, 0);
if GetEnvironmentVariable('CM_AKA') <> '' then
if not ParseFTNAddress(GetEnvironmentVariable('CM_AKA'),
Cfg.LocalAddrs[0]) then
begin
WriteLn('CM_AKA: cannot parse "',
GetEnvironmentVariable('CM_AKA'), '"');
Halt(2);
end;
Cfg.SystemName := 'fpc-comet example';
Cfg.MailerName := 'example_outbound/0.1';
Cfg.Transport := Transport;
Cfg.Provider := Provider;
Cfg.Log := @Logger.Log;
Cfg.Password := GetEnvironmentVariable('CM_PASSWORD');
Cfg.PrivateKey := GetEnvironmentVariable('CM_PRIVKEY');
Session := TComSession.Create(cmDirOutbound, Cfg);
Xfer := TCometXfer.Create(Session);
try
Session.SetTransferHooks(@Xfer.HandleFrame, @Xfer.Step,
@Xfer.IsDone);
Xfer.Start;
if GetEnvironmentVariable('CM_REQUEST') <> '' then
Xfer.RequestFiles(GetEnvironmentVariable('CM_REQUEST'));
if GetEnvironmentVariable('CM_LIST') <> '' then
Xfer.RequestListing(GetEnvironmentVariable('CM_LIST'));
while Session.NextStep do
Transport.WaitReady(True, False, 50);
R := Session.Result_;
if R.Success then
begin
WriteLn(Format('OK: sent %d files (%d bytes), received %d files (%d bytes)',
[Xfer.FilesSent, Xfer.BytesSent,
Xfer.FilesReceived, Xfer.BytesReceived]));
WriteLn(Format(' wire: %d sent / %d recv',
[R.WireBytesSent, R.WireBytesRecv]));
case R.AuthMethod of
cmAuthNoPwd: WriteLn(' auth: NOPWD');
cmAuthPlain: WriteLn(' auth: plain');
cmAuthCRAM: WriteLn(' auth: CRAM-MD5');
cmAuthED25519: WriteLn(' auth: ED25519');
else WriteLn(' auth: other ', Ord(R.AuthMethod));
end;
Halt(0);
end
else
begin
WriteLn(Format('FAIL: %s (code %d)',
[R.ErrorMessage, Ord(R.ErrorCode)]));
Halt(1);
end;
finally
Xfer.Free;
Session.Free;
Logger.Free;
{ Transport + Provider are TInterfacedObject; freed via
ref-count when Cfg goes out of scope. }
end;
end.