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.
150 lines
4.4 KiB
ObjectPascal
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.
|