Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cf1d1f8f7 | |||
| ba30ad5a4e | |||
| 46912a9798 | |||
| 945e7b17fa | |||
| cb430cfb50 | |||
| 1472bea823 | |||
| 9e8545c326 |
102
.gitea/workflows/release.yml
Normal file
102
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,102 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
GITEA_URL: https://kjgr.io
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: fpc
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build all platforms
|
||||
run: make all-platforms
|
||||
|
||||
- name: Run tests
|
||||
run: make test
|
||||
|
||||
- name: Package releases
|
||||
run: |
|
||||
VER="${{ steps.version.outputs.version }}"
|
||||
mkdir -p dist
|
||||
|
||||
sed "s/Version [0-9][0-9]*\.[0-9][0-9]*/Version $VER/g; s/Comet [0-9][0-9]*\.[0-9][0-9]*/Comet $VER/g" COMET.DOC > /tmp/COMET.DOC
|
||||
|
||||
package() {
|
||||
local plat="$1" binary="$2" fmt="$3" desc="$4"
|
||||
local dir="comet-${VER}-${plat}"
|
||||
mkdir -p "dist/$dir"
|
||||
cp "$binary" "dist/$dir/"
|
||||
cp COMET.SAM COMET.QA FSP-COMET.001 INSTALL.txt "dist/$dir/" 2>/dev/null || true
|
||||
cp /tmp/COMET.DOC "dist/$dir/"
|
||||
cat > "dist/$dir/FILE_ID.DIZ" <<FILEID
|
||||
Comet $VER - FidoNet TCP Mailer
|
||||
$desc
|
||||
Direct TCP file transfer for FidoNet
|
||||
SHA-256 verification, sliding window,
|
||||
ED25519/CRAM-MD5 authentication,
|
||||
per-block zlib compression, BinkP
|
||||
compatible on port 24554.
|
||||
Bidirectional transfers with adaptive
|
||||
block sizing (512B-64KB). Supports
|
||||
BSO, FrontDoor, D'Bridge outbound.
|
||||
(C) 2026 Ken Johnson 1:218/720 GPL2
|
||||
FILEID
|
||||
if [ "$fmt" = "tar" ]; then
|
||||
tar czf "dist/comet-${VER}-${plat}.tar.gz" -C dist "$dir"
|
||||
else
|
||||
(cd dist && zip -r "comet-${VER}-${plat}.zip" "$dir")
|
||||
fi
|
||||
rm -rf "dist/$dir"
|
||||
}
|
||||
|
||||
package linux-x64 build/linux/comet tar "Linux x86-64 binary."
|
||||
package freebsd-x64 build/freebsd/comet tar "FreeBSD x86-64 binary."
|
||||
package win64 build/win/comet.exe zip "Windows x86-64 binary."
|
||||
package dos build/go32v2/comet.exe zip "DOS (DJGPP/Watt-32) binary."
|
||||
package os2 build/os2/comet.exe zip "OS/2 binary."
|
||||
|
||||
- name: Create release
|
||||
run: |
|
||||
VER="${{ steps.version.outputs.version }}"
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
API="${GITEA_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
AUTH="Authorization: token ${{ secrets.RELEASE_TOKEN }}"
|
||||
|
||||
# Delete existing release for this tag (handles re-runs)
|
||||
OLD_ID=$(curl -s -H "$AUTH" "${API}/releases/tags/${TAG}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
|
||||
if [ -n "$OLD_ID" ] && [ "$OLD_ID" != "None" ]; then
|
||||
echo "Deleting existing release ${OLD_ID} for tag ${TAG}"
|
||||
curl -s -X DELETE -H "$AUTH" "${API}/releases/${OLD_ID}"
|
||||
fi
|
||||
|
||||
# Create release
|
||||
RELEASE_ID=$(curl -s -X POST \
|
||||
-H "$AUTH" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG}\", \"name\": \"Comet ${VER}\", \"body\": \"Comet ${VER}\"}" \
|
||||
"${API}/releases" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
|
||||
|
||||
echo "Created release ID: ${RELEASE_ID}"
|
||||
|
||||
# Upload assets
|
||||
for f in dist/comet-${VER}-*; do
|
||||
[ -f "$f" ] || continue
|
||||
echo "Uploading: $f"
|
||||
curl -s -X POST \
|
||||
-H "$AUTH" \
|
||||
-F "attachment=@${f}" \
|
||||
"${API}/releases/${RELEASE_ID}/assets"
|
||||
done
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -6,12 +6,30 @@
|
||||
link.res
|
||||
ppas.sh
|
||||
|
||||
# Build output
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Binaries
|
||||
comet
|
||||
test_crc
|
||||
test_sha
|
||||
test_frame
|
||||
test_sock
|
||||
test_md5
|
||||
test_crypt
|
||||
test_outbound
|
||||
test_x25519_*
|
||||
|
||||
# Release archives and staging
|
||||
releases/staging/
|
||||
releases/*.zip
|
||||
releases/*.tar.gz
|
||||
releases/*.tic
|
||||
|
||||
# Misc scripts/test files
|
||||
REGRESSION_*.txt
|
||||
run_speed_tests.sh
|
||||
|
||||
# Backup files
|
||||
*~
|
||||
|
||||
20
Makefile
20
Makefile
@@ -22,6 +22,7 @@ CROSSBIN = $(FPCUP)/cross/bin
|
||||
|
||||
# Directories
|
||||
SRCDIR = src
|
||||
TESTDIR = test
|
||||
BUILDDIR = build
|
||||
|
||||
# Source files
|
||||
@@ -110,17 +111,17 @@ test: $(TESTS)
|
||||
@./test_md5
|
||||
@echo "=== All tests passed ==="
|
||||
|
||||
test_crc: $(SRCDIR)/test_crc.pas $(SRCDIR)/cometcrc.pas
|
||||
$(FPC) $(FPCFLAGS) $(FPCOPT) -FE. $(SRCDIR)/test_crc.pas
|
||||
test_crc: $(TESTDIR)/test_crc.pas $(SRCDIR)/cometcrc.pas
|
||||
$(FPC) $(FPCFLAGS) $(FPCOPT) -Fu$(SRCDIR) -FE. $(TESTDIR)/test_crc.pas
|
||||
|
||||
test_sha: $(SRCDIR)/test_sha.pas $(SRCDIR)/cometsha.pas
|
||||
$(FPC) $(FPCFLAGS) $(FPCOPT) -FE. $(SRCDIR)/test_sha.pas
|
||||
test_sha: $(TESTDIR)/test_sha.pas $(SRCDIR)/cometsha.pas
|
||||
$(FPC) $(FPCFLAGS) $(FPCOPT) -Fu$(SRCDIR) -FE. $(TESTDIR)/test_sha.pas
|
||||
|
||||
test_frame: $(SRCDIR)/test_frame.pas $(SRCDIR)/cometfrm.pas $(SRCDIR)/comettcp.pas $(SRCDIR)/cometcrc.pas $(SRCDIR)/cometdef.pas $(SRCDIR)/cometlog.pas
|
||||
$(FPC) $(FPCFLAGS) $(FPCOPT) -FE. $(SRCDIR)/test_frame.pas
|
||||
test_frame: $(TESTDIR)/test_frame.pas $(SRCDIR)/cometfrm.pas $(SRCDIR)/comettcp.pas $(SRCDIR)/cometcrc.pas $(SRCDIR)/cometdef.pas $(SRCDIR)/cometlog.pas
|
||||
$(FPC) $(FPCFLAGS) $(FPCOPT) -Fu$(SRCDIR) -FE. $(TESTDIR)/test_frame.pas
|
||||
|
||||
test_md5: $(SRCDIR)/test_md5.pas $(SRCDIR)/cometmd5.pas $(SRCDIR)/cometcram.pas
|
||||
$(FPC) $(FPCFLAGS) $(FPCOPT) -FE. $(SRCDIR)/test_md5.pas
|
||||
test_md5: $(TESTDIR)/test_md5.pas $(SRCDIR)/cometmd5.pas $(SRCDIR)/cometcram.pas
|
||||
$(FPC) $(FPCFLAGS) $(FPCOPT) -Fu$(SRCDIR) -FE. $(TESTDIR)/test_md5.pas
|
||||
|
||||
# Debug build (with debugging symbols)
|
||||
debug: FPCOPT += -g -gl -dDEBUG
|
||||
@@ -130,12 +131,13 @@ debug: clean linux
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)
|
||||
rm -f $(SRCDIR)/*.o $(SRCDIR)/*.ppu $(SRCDIR)/*.rsj
|
||||
rm -f $(TESTDIR)/*.o $(TESTDIR)/*.ppu $(TESTDIR)/*.rsj
|
||||
rm -f *.o *.ppu *.rsj link.res ppas.sh
|
||||
rm -f $(TESTS)
|
||||
|
||||
# Full clean including backups
|
||||
distclean: clean
|
||||
rm -f $(SRCDIR)/*~ $(SRCDIR)/*.bak *~ *.bak
|
||||
rm -f $(SRCDIR)/*~ $(SRCDIR)/*.bak $(TESTDIR)/*~ $(TESTDIR)/*.bak *~ *.bak
|
||||
|
||||
# Install (copy binary to /usr/local/bin)
|
||||
install: linux
|
||||
|
||||
95
release.sh
Executable file
95
release.sh
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
# Build all platforms, package, and push a release to GitHub/Gitea
|
||||
# Usage: ./release.sh v1.01.01 "Release notes here"
|
||||
|
||||
set -e
|
||||
|
||||
TAG="$1"
|
||||
NOTES="$2"
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "Usage: $0 <tag> [release notes]"
|
||||
echo "Example: $0 v1.01.01 \"Nodelist year-rollover fix\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VER="${TAG#v}"
|
||||
|
||||
echo "=== Building Comet $VER ==="
|
||||
make clean
|
||||
make all-platforms
|
||||
|
||||
echo "=== Running tests ==="
|
||||
make test
|
||||
|
||||
echo "=== Packaging releases ==="
|
||||
mkdir -p dist
|
||||
rm -f dist/comet-${VER}-*
|
||||
|
||||
# Update COMET.DOC version
|
||||
sed "s/Version [0-9][0-9]*\.[0-9][0-9]*/Version $VER/g; s/Comet [0-9][0-9]*\.[0-9][0-9]*/Comet $VER/g" COMET.DOC > /tmp/COMET.DOC
|
||||
|
||||
DOCS="COMET.SAM COMET.QA FSP-COMET.001 INSTALL.txt"
|
||||
|
||||
build_platform() {
|
||||
local plat="$1" binary="$2" fmt="$3" desc="$4"
|
||||
local dir="comet-${VER}-${plat}"
|
||||
|
||||
mkdir -p "dist/$dir"
|
||||
cp "$binary" "dist/$dir/"
|
||||
cp $DOCS "dist/$dir/" 2>/dev/null || true
|
||||
cp /tmp/COMET.DOC "dist/$dir/"
|
||||
|
||||
# Include systemd service for Linux
|
||||
if [ "$plat" = "linux-x64" ] && [ -f releases/staging/comet-*-linux-x64/comet.service ]; then
|
||||
cp releases/staging/comet-*-linux-x64/comet.service "dist/$dir/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat > "dist/$dir/FILE_ID.DIZ" <<EOF
|
||||
Comet $VER - FidoNet TCP Mailer
|
||||
$desc
|
||||
Direct TCP file transfer for FidoNet
|
||||
SHA-256 verification, sliding window,
|
||||
ED25519/CRAM-MD5 authentication,
|
||||
per-block zlib compression, BinkP
|
||||
compatible on port 24554.
|
||||
Bidirectional transfers with adaptive
|
||||
block sizing (512B-64KB). Supports
|
||||
BSO, FrontDoor, D'Bridge outbound.
|
||||
(C) 2026 Ken Johnson 1:218/720 GPL2
|
||||
EOF
|
||||
|
||||
if [ "$fmt" = "tar" ]; then
|
||||
tar czf "dist/comet-${VER}-${plat}.tar.gz" -C dist "$dir"
|
||||
else
|
||||
(cd dist && zip -r "comet-${VER}-${plat}.zip" "$dir")
|
||||
fi
|
||||
rm -rf "dist/$dir"
|
||||
}
|
||||
|
||||
build_platform linux-x64 build/linux/comet tar "Linux x86-64 binary."
|
||||
build_platform freebsd-x64 build/freebsd/comet tar "FreeBSD x86-64 binary."
|
||||
build_platform win64 build/win/comet.exe zip "Windows x86-64 binary."
|
||||
build_platform dos build/go32v2/comet.exe zip "DOS (DJGPP/Watt-32) binary."
|
||||
build_platform os2 build/os2/comet.exe zip "OS/2 binary."
|
||||
|
||||
echo ""
|
||||
echo "=== Archives ==="
|
||||
ls -lh dist/comet-${VER}-*
|
||||
|
||||
echo ""
|
||||
echo "=== Pushing release $TAG ==="
|
||||
git tag "$TAG" 2>/dev/null || echo "Tag $TAG already exists"
|
||||
git push origin "$TAG" 2>/dev/null || echo "Tag already pushed (or no remote)"
|
||||
|
||||
gh release create "$TAG" \
|
||||
dist/comet-${VER}-linux-x64.tar.gz \
|
||||
dist/comet-${VER}-freebsd-x64.tar.gz \
|
||||
dist/comet-${VER}-win64.zip \
|
||||
dist/comet-${VER}-dos.zip \
|
||||
dist/comet-${VER}-os2.zip \
|
||||
--title "Comet $VER" \
|
||||
--notes "${NOTES:-Release $VER}"
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
@@ -1,10 +1,10 @@
|
||||
Area COMET
|
||||
Origin 1:218/720
|
||||
File comet101-dos.zip
|
||||
Desc Comet 1.01 FidoNet TCP Mailer - DOS (DJGPP)
|
||||
Size 619430
|
||||
Date 02 Apr 2026
|
||||
CRC 498A3997
|
||||
Desc Comet 1.01 FidoNet TCP Mailer - DOS (DJGPP/Watt-32)
|
||||
Size 618195
|
||||
Date 03 Apr 2026
|
||||
CRC 3194480E
|
||||
Replaces comet100*
|
||||
Created Comet 1.01
|
||||
Pw
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,9 +2,9 @@ Area COMET
|
||||
Origin 1:218/720
|
||||
File comet101-freebsd-x64.tar.gz
|
||||
Desc Comet 1.01 FidoNet TCP Mailer - FreeBSD x86-64
|
||||
Size 464270
|
||||
Date 02 Apr 2026
|
||||
CRC 0D09FE57
|
||||
Size 468498
|
||||
Date 03 Apr 2026
|
||||
CRC A94DC666
|
||||
Replaces comet100*
|
||||
Created Comet 1.01
|
||||
Pw
|
||||
|
||||
Binary file not shown.
@@ -2,9 +2,9 @@ Area COMET
|
||||
Origin 1:218/720
|
||||
File comet101-linux-x64.tar.gz
|
||||
Desc Comet 1.01 FidoNet TCP Mailer - Linux x86-64
|
||||
Size 465218
|
||||
Date 02 Apr 2026
|
||||
CRC F22B5955
|
||||
Size 474975
|
||||
Date 03 Apr 2026
|
||||
CRC BB35A4B7
|
||||
Replaces comet100*
|
||||
Created Comet 1.01
|
||||
Pw
|
||||
|
||||
@@ -2,9 +2,9 @@ Area COMET
|
||||
Origin 1:218/720
|
||||
File comet101-os2.zip
|
||||
Desc Comet 1.01 FidoNet TCP Mailer - OS/2
|
||||
Size 439321
|
||||
Date 02 Apr 2026
|
||||
CRC 17F5A720
|
||||
Size 426154
|
||||
Date 03 Apr 2026
|
||||
CRC 9B3E3B7E
|
||||
Replaces comet100*
|
||||
Created Comet 1.01
|
||||
Pw
|
||||
|
||||
Binary file not shown.
@@ -2,9 +2,9 @@ Area COMET
|
||||
Origin 1:218/720
|
||||
File comet101-win64.zip
|
||||
Desc Comet 1.01 FidoNet TCP Mailer - Windows x86-64
|
||||
Size 267759
|
||||
Date 02 Apr 2026
|
||||
CRC 4B5A01C5
|
||||
Size 266796
|
||||
Date 03 Apr 2026
|
||||
CRC 37F25BC3
|
||||
Replaces comet100*
|
||||
Created Comet 1.01
|
||||
Pw
|
||||
|
||||
Binary file not shown.
@@ -435,18 +435,45 @@ begin
|
||||
|
||||
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;
|
||||
{ ---- 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');
|
||||
XS.FreqDir := Cfg.FreqDir;
|
||||
|
||||
@@ -150,6 +150,7 @@ type
|
||||
LocalEOB: Boolean; { We sent M_EOB }
|
||||
RemoteEOB: Boolean; { Received M_EOB }
|
||||
Authenticated: Boolean; { Handshake complete }
|
||||
AuthMethod: Byte; { AUTH_* - how session was authenticated }
|
||||
Phase: Integer; { 0=setup, 1=transfer }
|
||||
|
||||
{ Remote info }
|
||||
@@ -238,6 +239,50 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
{ ---- Session event helpers ---- }
|
||||
|
||||
procedure BinkpFireSessionStart(var S: TBinkpState);
|
||||
var
|
||||
Ev: TCometEventData;
|
||||
begin
|
||||
FillChar(Ev, SizeOf(Ev), 0);
|
||||
Ev.EventType := cetSessionStart;
|
||||
Ev.Protocol := 'BinkP';
|
||||
Ev.RemoteName := S.RemoteSysName;
|
||||
if Length(S.RemoteAddrs) > 0 then
|
||||
Ev.RemoteAddr := CometAddrToStr(S.RemoteAddrs[0]);
|
||||
Ev.RemoteMailer := S.RemoteMailer;
|
||||
CometFireEvent(Ev);
|
||||
end;
|
||||
|
||||
procedure BinkpFireSessionAuth(var S: TBinkpState);
|
||||
var
|
||||
Ev: TCometEventData;
|
||||
begin
|
||||
FillChar(Ev, SizeOf(Ev), 0);
|
||||
Ev.EventType := cetSessionAuth;
|
||||
Ev.Protocol := 'BinkP';
|
||||
Ev.AuthMethod := S.AuthMethod;
|
||||
Ev.Encrypted := False; { BinkP encryption not yet implemented }
|
||||
CometFireEvent(Ev);
|
||||
end;
|
||||
|
||||
procedure BinkpFireSessionEnd(var S: TBinkpState);
|
||||
var
|
||||
Ev: TCometEventData;
|
||||
begin
|
||||
FillChar(Ev, SizeOf(Ev), 0);
|
||||
Ev.EventType := cetSessionEnd;
|
||||
Ev.Protocol := 'BinkP';
|
||||
Ev.FilesSent := S.Result.FilesSent;
|
||||
Ev.FilesRecvd := S.Result.FilesRecvd;
|
||||
Ev.BytesSent := S.Result.BytesSent;
|
||||
Ev.BytesRecvd := S.Result.BytesRecvd;
|
||||
Ev.Success := S.Result.Success;
|
||||
CometFireEvent(Ev);
|
||||
end;
|
||||
|
||||
|
||||
{ ---- Command name for logging ---- }
|
||||
|
||||
function BinkpCmdName(ID: Byte): string;
|
||||
@@ -283,6 +328,7 @@ begin
|
||||
SetLength(S.Msgs, S.MsgCount);
|
||||
S.Msgs[Idx].Data := Frame;
|
||||
S.Msgs[Idx].Sent := False;
|
||||
LogDebug('BinkP TX queue %s: %s', [BinkpCmdName(CmdID), Arg]);
|
||||
end;
|
||||
|
||||
|
||||
@@ -390,6 +436,8 @@ begin
|
||||
S.IMsg := (S.IBuf[0] and $80) <> 0;
|
||||
S.ISize := ((S.IBuf[0] and $7F) shl 8) or S.IBuf[1];
|
||||
S.IRead := 0;
|
||||
LogDebug('BinkP RX hdr: %s len=%d (raw=$%02X%02X)',
|
||||
[BoolToStr(S.IMsg, 'cmd', 'data'), S.ISize, S.IBuf[0], S.IBuf[1]]);
|
||||
|
||||
if S.ISize = 0 then
|
||||
{ Fall through to body processing with zero-length frame }
|
||||
@@ -478,9 +526,14 @@ begin
|
||||
end;
|
||||
|
||||
if Length(S.RemoteAddrs) > 0 then
|
||||
begin
|
||||
LogInfo('BinkP remote: %s (%s)',
|
||||
[CometAddrToStr(S.RemoteAddrs[0]), S.RemoteSysName]);
|
||||
|
||||
{ Fire session start event for host application }
|
||||
BinkpFireSessionStart(S);
|
||||
end;
|
||||
|
||||
{ Answerer: scan BSO outbound for files to send to the caller.
|
||||
Same pattern as Comet inbound — scan all remote AKAs.
|
||||
BSY locking is handled by the daemon, not the protocol. }
|
||||
@@ -515,7 +568,7 @@ begin
|
||||
begin
|
||||
if FloEntries[J].Sent then Continue;
|
||||
if (FloEntries[J].FilePath = '') or
|
||||
not FileExists(FloEntries[J].FilePath) then Continue;
|
||||
not CometFileExists(FloEntries[J].FilePath) then Continue;
|
||||
if S.SendQueueLen >= Length(S.SendQueue) then
|
||||
SetLength(S.SendQueue, S.SendQueueLen + 16);
|
||||
FillChar(S.SendQueue[S.SendQueueLen], SizeOf(TBinkpSendEntry), 0);
|
||||
@@ -552,16 +605,23 @@ begin
|
||||
Pwd := 'ED25519-' + ED25519ToHex(SigBytes, 64);
|
||||
FillChar(ED25519SK, 64, 0);
|
||||
FillChar(ChalBytes, 32, 0);
|
||||
S.AuthMethod := AUTH_ED25519;
|
||||
LogInfo('BinkP: using ED25519 authentication');
|
||||
end
|
||||
else if (Pwd <> '') and (S.CRAMOpt <> '') then
|
||||
begin
|
||||
{ CRAM-MD5: send hashed response }
|
||||
Pwd := CRAMBuildPassword(S.CRAMOpt, Pwd);
|
||||
S.AuthMethod := AUTH_CRAM;
|
||||
LogInfo('BinkP: using CRAM-MD5 authentication');
|
||||
end
|
||||
else if Pwd = '' then
|
||||
begin
|
||||
Pwd := '-';
|
||||
S.AuthMethod := AUTH_NOPWD;
|
||||
end
|
||||
else
|
||||
S.AuthMethod := AUTH_PLAIN;
|
||||
|
||||
QueueMsg(S, M_PWD, Pwd);
|
||||
end;
|
||||
@@ -590,9 +650,11 @@ begin
|
||||
if ED25519Verify(@ChalBytes[0], 32, SigBytes, ED25519PK) then
|
||||
begin
|
||||
S.Authenticated := True;
|
||||
S.AuthMethod := AUTH_ED25519;
|
||||
S.Phase := 1;
|
||||
QueueMsg(S, M_OK, 'secure');
|
||||
LogInfo('BinkP ED25519 signature verified');
|
||||
BinkpFireSessionAuth(S);
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -617,9 +679,11 @@ begin
|
||||
if (Challenge <> nil) and CRAMVerify(Challenge, CRAMResponse, Pwd) then
|
||||
begin
|
||||
S.Authenticated := True;
|
||||
S.AuthMethod := AUTH_CRAM;
|
||||
S.Phase := 1;
|
||||
QueueMsg(S, M_OK, '');
|
||||
LogInfo('BinkP CRAM-MD5 password verified');
|
||||
BinkpFireSessionAuth(S);
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -629,16 +693,25 @@ begin
|
||||
S.IOError := True;
|
||||
end;
|
||||
end
|
||||
else if (Pwd = '') or (Arg = '-') or (Arg = Pwd) then
|
||||
else if (Pwd = '') and ((Arg = '-') or (Arg = '')) then
|
||||
begin
|
||||
{ No password required, or plain password match }
|
||||
{ No password required on either side }
|
||||
S.Authenticated := True;
|
||||
S.AuthMethod := AUTH_NOPWD;
|
||||
S.Phase := 1;
|
||||
QueueMsg(S, M_OK, 'non-secure');
|
||||
if Pwd = '' then
|
||||
LogInfo('BinkP: no password - insecure session')
|
||||
else
|
||||
LogInfo('BinkP: plain password accepted');
|
||||
LogInfo('BinkP: no password - insecure session');
|
||||
BinkpFireSessionAuth(S);
|
||||
end
|
||||
else if (Pwd <> '') and (Arg = Pwd) then
|
||||
begin
|
||||
{ Plain password match }
|
||||
S.Authenticated := True;
|
||||
S.AuthMethod := AUTH_PLAIN;
|
||||
S.Phase := 1;
|
||||
QueueMsg(S, M_OK, '');
|
||||
LogInfo('BinkP: plain password accepted');
|
||||
BinkpFireSessionAuth(S);
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -662,6 +735,7 @@ begin
|
||||
S.Authenticated := True;
|
||||
S.Phase := 1;
|
||||
LogInfo('BinkP session established with %s', [S.RemoteSysName]);
|
||||
BinkpFireSessionAuth(S);
|
||||
end;
|
||||
|
||||
M_FILE:
|
||||
@@ -675,9 +749,11 @@ begin
|
||||
FParts.DelimitedText := Arg;
|
||||
if FParts.Count >= 3 then
|
||||
begin
|
||||
{ Close any previous receive file }
|
||||
{ Close any previous receive file and notify remote }
|
||||
if S.RxFile.Active then
|
||||
begin
|
||||
QueueMsg(S, M_SKIP, Format('%s %d %d',
|
||||
[S.RxFile.Name, S.RxFile.Size, S.RxFile.Time]));
|
||||
CloseFile(S.RxFile.F);
|
||||
CometDeleteTemp(S.RxFile.TempPath);
|
||||
S.RxFile.Active := False;
|
||||
@@ -725,6 +801,26 @@ begin
|
||||
S.RxStartTime := Now;
|
||||
BinkpFireFileEvent(cetFileStart, S.RxFile.Name,
|
||||
S.RxFile.Size, S.RxFile.Offset, False, S.RxStartTime);
|
||||
|
||||
{ Zero-length file: complete immediately since no data
|
||||
frames will arrive. Common for empty .PKT files. }
|
||||
if S.RxFile.Size = 0 then
|
||||
begin
|
||||
CloseFile(S.RxFile.F);
|
||||
S.RxFile.Active := False;
|
||||
FinalPath := CometFinalizeFile(S.RxFile.TempPath,
|
||||
S.InboundDir, S.RxFile.Name, LongInt(S.RxFile.Time));
|
||||
if FinalPath <> '' then
|
||||
begin
|
||||
Inc(S.Result.FilesRecvd);
|
||||
LogInfo('BinkP received: %s (0 bytes)',
|
||||
[S.RxFile.Name]);
|
||||
BinkpFireFileEvent(cetFileEnd, S.RxFile.Name, 0,
|
||||
0, False, S.RxStartTime);
|
||||
end;
|
||||
QueueMsg(S, M_GOT, Format('%s 0 %d',
|
||||
[S.RxFile.Name, S.RxFile.Time]));
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -813,9 +909,11 @@ begin
|
||||
begin
|
||||
S.RemoteEOB := True;
|
||||
LogInfo('BinkP: remote sent EOB');
|
||||
{ Close any incomplete receive file }
|
||||
{ Close any incomplete receive file and notify remote }
|
||||
if S.RxFile.Active then
|
||||
begin
|
||||
QueueMsg(S, M_SKIP, Format('%s %d %d',
|
||||
[S.RxFile.Name, S.RxFile.Size, S.RxFile.Time]));
|
||||
CloseFile(S.RxFile.F);
|
||||
CometDeleteTemp(S.RxFile.TempPath);
|
||||
S.RxFile.Active := False;
|
||||
@@ -1142,9 +1240,10 @@ begin
|
||||
SetLength(S.Msgs, 0);
|
||||
|
||||
{ Originator: consume any Comet banner before switching to non-blocking.
|
||||
A Comet daemon sends "COMET/1.0\n" before detecting protocol.
|
||||
A real binkd sends a BinkP command frame (first byte >= $80).
|
||||
Read first byte: if < $80, consume the banner line and discard. }
|
||||
Protocol sniff uses MSG_PEEK so the first byte is still in the socket.
|
||||
If the first byte is a BinkP frame (>= $80), save as peek data for
|
||||
the frame reader. If it's a low byte (e.g. Comet banner from a
|
||||
daemon that sends banner first), consume the whole banner line. }
|
||||
if S.IsOriginator then
|
||||
begin
|
||||
if CometTcpWaitData(S.Sock, 5000) then
|
||||
@@ -1177,14 +1276,7 @@ begin
|
||||
{ Set socket non-blocking }
|
||||
CometTcpSetNonBlock(S.Sock, True);
|
||||
|
||||
{ Queue initial NUL info }
|
||||
QueueMsg(S, M_NUL, 'SYS ' + S.Cfg.SysName);
|
||||
QueueMsg(S, M_NUL, 'ZYZ ' + S.Cfg.SysOp);
|
||||
QueueMsg(S, M_NUL, 'LOC ' + S.Cfg.Location);
|
||||
QueueMsg(S, M_NUL, 'VER Comet/' + COMET_VERSION + ' binkp/1.1');
|
||||
QueueMsg(S, M_NUL, 'TIME ' + FormatDateTime('ddd, dd mmm yyyy hh:nn:ss', Now));
|
||||
|
||||
{ Send OPT capabilities.
|
||||
{ Build OPT capabilities string.
|
||||
Only advertise features that are actually enabled. }
|
||||
OptStr := 'NR EXTCMD';
|
||||
|
||||
@@ -1200,7 +1292,8 @@ begin
|
||||
|
||||
if not S.IsOriginator then
|
||||
begin
|
||||
{ Answering: always send CRAM-MD5 challenge }
|
||||
{ Answering: CRAM-MD5 challenge MUST be the first message sent
|
||||
(FTS-1027 requirement). binkd and other mailers expect this. }
|
||||
S.CRAMOpt := CRAMGenerateChallenge;
|
||||
|
||||
{ Only send ED25519 challenge if we have a private key }
|
||||
@@ -1212,8 +1305,16 @@ begin
|
||||
end;
|
||||
|
||||
QueueMsg(S, M_NUL, 'OPT ' + S.CRAMOpt + ' ' + OptStr);
|
||||
end
|
||||
else
|
||||
end;
|
||||
|
||||
{ Queue NUL info (after OPT on answering side per FTS-1027) }
|
||||
QueueMsg(S, M_NUL, 'SYS ' + S.Cfg.SysName);
|
||||
QueueMsg(S, M_NUL, 'ZYZ ' + S.Cfg.SysOp);
|
||||
QueueMsg(S, M_NUL, 'LOC ' + S.Cfg.Location);
|
||||
QueueMsg(S, M_NUL, 'VER Comet/' + COMET_VERSION + ' binkp/1.1');
|
||||
QueueMsg(S, M_NUL, 'TIME ' + FormatDateTime('ddd, dd mmm yyyy hh:nn:ss', Now));
|
||||
|
||||
if S.IsOriginator then
|
||||
QueueMsg(S, M_NUL, 'OPT ' + OptStr);
|
||||
|
||||
{ Queue our addresses }
|
||||
@@ -1291,7 +1392,16 @@ begin
|
||||
{$ENDIF}
|
||||
end;
|
||||
|
||||
{ ---- Session end ---- }
|
||||
{ ---- Session end: flush any remaining queued messages ---- }
|
||||
if (S.MsgCount > 0) and not S.IOError then
|
||||
begin
|
||||
LogDebug('BinkP: flushing %d queued messages after session loop', [S.MsgCount]);
|
||||
while (S.MsgCount > 0) and not S.IOError do
|
||||
begin
|
||||
if not CometTcpWaitSend(S.Sock, 5000) then Break;
|
||||
if SendBlock(S) = 0 then Break;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Determine success }
|
||||
if SessionDone(S) and not S.IOError then
|
||||
@@ -1312,6 +1422,10 @@ begin
|
||||
S.Result.RemoteSysName := S.RemoteSysName;
|
||||
S.Result.RemoteSysOp := S.RemoteSysOp;
|
||||
S.Result.RemoteMailer := S.RemoteMailer;
|
||||
|
||||
{ Fire session end event for host application }
|
||||
BinkpFireSessionEnd(S);
|
||||
|
||||
{ Cleanup }
|
||||
if S.TxFile.Active then
|
||||
begin
|
||||
|
||||
@@ -192,6 +192,52 @@ begin
|
||||
FSlotIndex := ASlotIndex;
|
||||
end;
|
||||
|
||||
{ ---- Session event helpers ---- }
|
||||
|
||||
procedure FireSessionStart(const Protocol, RemoteName, RemoteAddr,
|
||||
RemoteMailer: string);
|
||||
var
|
||||
Ev: TCometEventData;
|
||||
begin
|
||||
FillChar(Ev, SizeOf(Ev), 0);
|
||||
Ev.EventType := cetSessionStart;
|
||||
Ev.Protocol := Protocol;
|
||||
Ev.RemoteName := RemoteName;
|
||||
Ev.RemoteAddr := RemoteAddr;
|
||||
Ev.RemoteMailer := RemoteMailer;
|
||||
CometFireEvent(Ev);
|
||||
end;
|
||||
|
||||
procedure FireSessionAuth(const Protocol: string; AuthMethod: Byte;
|
||||
Encrypted: Boolean);
|
||||
var
|
||||
Ev: TCometEventData;
|
||||
begin
|
||||
FillChar(Ev, SizeOf(Ev), 0);
|
||||
Ev.EventType := cetSessionAuth;
|
||||
Ev.Protocol := Protocol;
|
||||
Ev.AuthMethod := AuthMethod;
|
||||
Ev.Encrypted := Encrypted;
|
||||
CometFireEvent(Ev);
|
||||
end;
|
||||
|
||||
procedure FireSessionEnd(const Protocol: string; FilesSent, FilesRecvd: Integer;
|
||||
BytesSent, BytesRecvd: Int64; Success: Boolean);
|
||||
var
|
||||
Ev: TCometEventData;
|
||||
begin
|
||||
FillChar(Ev, SizeOf(Ev), 0);
|
||||
Ev.EventType := cetSessionEnd;
|
||||
Ev.Protocol := Protocol;
|
||||
Ev.FilesSent := FilesSent;
|
||||
Ev.FilesRecvd := FilesRecvd;
|
||||
Ev.BytesSent := BytesSent;
|
||||
Ev.BytesRecvd := BytesRecvd;
|
||||
Ev.Success := Success;
|
||||
CometFireEvent(Ev);
|
||||
end;
|
||||
|
||||
|
||||
procedure TCometSessionThread.Execute;
|
||||
begin
|
||||
try
|
||||
@@ -489,9 +535,21 @@ begin
|
||||
FDaemon.UpdateSession(FSlotIndex, Info);
|
||||
end;
|
||||
|
||||
{ Fire session events for host application }
|
||||
FireSessionStart('Comet', State.Session.RemoteSysName,
|
||||
CometAddrToStr(State.RemoteInit.Addresses[0]),
|
||||
State.Session.RemoteMailer);
|
||||
FireSessionAuth('Comet', State.Session.AuthMethod,
|
||||
(State.Session.SharedCaps and COPT_CRYPT) <> 0);
|
||||
|
||||
{ Step 3: Run file transfer }
|
||||
RunSession(State);
|
||||
|
||||
{ Fire session end event }
|
||||
Info := FDaemon.GetSessionInfo(FSlotIndex);
|
||||
FireSessionEnd('Comet', Info.FilesSent, Info.FilesRecvd,
|
||||
Info.BytesSent, Info.BytesRecvd, True);
|
||||
|
||||
finally
|
||||
CometSessionDone(State);
|
||||
end;
|
||||
@@ -519,19 +577,45 @@ begin
|
||||
|
||||
if FUseComet then
|
||||
begin
|
||||
{ ---- Comet protocol path ---- }
|
||||
{ ---- 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(FSock, 5);
|
||||
if HSResult = chrBinkP then
|
||||
begin
|
||||
LogInfo('Remote speaks BinkP - proceeding on same connection');
|
||||
FUseComet := False;
|
||||
end
|
||||
else if HSResult = chrDisconnect then
|
||||
begin
|
||||
LogError('Connection lost during protocol detection with %s',
|
||||
[CometAddrToStr(FTargetAddr)]);
|
||||
Exit;
|
||||
end;
|
||||
{ chrOK (Comet banner seen) or chrTimeout (no data) = proceed with Comet }
|
||||
end;
|
||||
|
||||
if FUseComet then
|
||||
begin
|
||||
{ ---- Comet handshake ---- }
|
||||
CometSessionInit(State, FSock, True, FRemoteIP, FRemotePort);
|
||||
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))]);
|
||||
CometSessionDone(State);
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
if FUseComet then
|
||||
begin
|
||||
{ ---- Comet protocol session ---- }
|
||||
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;
|
||||
|
||||
{ Lock all remote AKAs }
|
||||
if Length(State.RemoteInit.Addresses) > 0 then
|
||||
begin
|
||||
@@ -543,7 +627,19 @@ begin
|
||||
Info.Status := cssActive;
|
||||
FDaemon.UpdateSession(FSlotIndex, Info);
|
||||
|
||||
{ Fire session events for host application }
|
||||
FireSessionStart('Comet', State.Session.RemoteSysName,
|
||||
CometAddrToStr(State.RemoteInit.Addresses[0]),
|
||||
State.Session.RemoteMailer);
|
||||
FireSessionAuth('Comet', State.Session.AuthMethod,
|
||||
(State.Session.SharedCaps and COPT_CRYPT) <> 0);
|
||||
|
||||
RunSession(State);
|
||||
|
||||
{ Fire session end event }
|
||||
Info := FDaemon.GetSessionInfo(FSlotIndex);
|
||||
FireSessionEnd('Comet', Info.FilesSent, Info.FilesRecvd,
|
||||
Info.BytesSent, Info.BytesRecvd, True);
|
||||
finally
|
||||
CometSessionDone(State);
|
||||
end;
|
||||
|
||||
@@ -29,7 +29,7 @@ interface
|
||||
const
|
||||
{ Program identification }
|
||||
COMET_NAME = 'Comet';
|
||||
COMET_VERSION = '1.01';
|
||||
COMET_VERSION = '1.1-1';
|
||||
COMET_PRODCODE = $010B; { FTSC product code (Xenia family) }
|
||||
|
||||
{ Network }
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
Multiple nodelist files are supported. The most recent entry for
|
||||
a given address wins (last-loaded takes priority).
|
||||
|
||||
Directory loading parses the first-line date comment to determine
|
||||
the true date (year + day number), so year rollovers are handled
|
||||
correctly even with multiple years of nodelists present.
|
||||
|
||||
Copyright (C) 2026 Ken Johnson
|
||||
License: GPL-2.0
|
||||
}
|
||||
@@ -109,6 +113,71 @@ const
|
||||
NODELIST_GROW = 4096; { Grow entries array by this many at a time }
|
||||
|
||||
|
||||
{ Read the first line of a nodelist file and extract a sortable date value.
|
||||
First line format: ;A Friday, January 3, 2026 -- Day number 003 : ...
|
||||
Returns Year * 1000 + DayNumber, or -1 if unparseable. }
|
||||
function GetNodelistDateValue(const FilePath: string): LongInt;
|
||||
var
|
||||
F: TextFile;
|
||||
Line, Upper: string;
|
||||
P, YearVal, DayVal: Integer;
|
||||
begin
|
||||
Result := -1;
|
||||
if not FileExists(FilePath) then Exit;
|
||||
|
||||
AssignFile(F, FilePath);
|
||||
{$I-} Reset(F); {$I+}
|
||||
if IOResult <> 0 then Exit;
|
||||
|
||||
try
|
||||
if EOF(F) then Exit;
|
||||
ReadLn(F, Line);
|
||||
finally
|
||||
CloseFile(F);
|
||||
end;
|
||||
|
||||
if (Length(Line) = 0) or (Line[1] <> ';') then Exit;
|
||||
|
||||
Upper := UpperCase(Line);
|
||||
|
||||
{ Extract day number from "Day number NNN" }
|
||||
P := Pos('DAY NUMBER', Upper);
|
||||
if P = 0 then Exit;
|
||||
P := P + 10; { skip past 'DAY NUMBER' }
|
||||
while (P <= Length(Line)) and (Line[P] = ' ') do Inc(P);
|
||||
DayVal := 0;
|
||||
while (P <= Length(Line)) and (Line[P] >= '0') and (Line[P] <= '9') do
|
||||
begin
|
||||
DayVal := DayVal * 10 + Ord(Line[P]) - Ord('0');
|
||||
Inc(P);
|
||||
end;
|
||||
if (DayVal < 1) or (DayVal > 366) then Exit;
|
||||
|
||||
{ Extract 4-digit year - scan for first 4-digit number >= 1990 }
|
||||
YearVal := 0;
|
||||
P := 2; { skip leading semicolon }
|
||||
while P <= Length(Line) - 3 do
|
||||
begin
|
||||
if (Line[P] >= '0') and (Line[P] <= '9') and
|
||||
(Line[P+1] >= '0') and (Line[P+1] <= '9') and
|
||||
(Line[P+2] >= '0') and (Line[P+2] <= '9') and
|
||||
(Line[P+3] >= '0') and (Line[P+3] <= '9') then
|
||||
begin
|
||||
YearVal := (Ord(Line[P]) - Ord('0')) * 1000 +
|
||||
(Ord(Line[P+1]) - Ord('0')) * 100 +
|
||||
(Ord(Line[P+2]) - Ord('0')) * 10 +
|
||||
(Ord(Line[P+3]) - Ord('0'));
|
||||
if YearVal >= 1990 then Break;
|
||||
YearVal := 0;
|
||||
end;
|
||||
Inc(P);
|
||||
end;
|
||||
if YearVal = 0 then Exit;
|
||||
|
||||
Result := LongInt(YearVal) * 1000 + DayVal;
|
||||
end;
|
||||
|
||||
|
||||
procedure CometNodelistInit(var NL: TCometNodelist);
|
||||
begin
|
||||
SetLength(NL.Entries, 0);
|
||||
@@ -331,29 +400,35 @@ function CometNodelistLoadDir(var NL: TCometNodelist;
|
||||
var
|
||||
SR: TSearchRec;
|
||||
BestFile: string;
|
||||
BestDay: Integer;
|
||||
DayNum: Integer;
|
||||
BestDate: LongInt;
|
||||
DateVal: LongInt;
|
||||
FilePath: string;
|
||||
NormDir: string;
|
||||
begin
|
||||
Result := -1;
|
||||
NormDir := CometStripSlash(DirPath);
|
||||
if not DirectoryExists(NormDir) then Exit;
|
||||
|
||||
{ Find the most recent nodelist.NNN file }
|
||||
{ Find the most recent nodelist.NNN file by parsing the
|
||||
first-line date comment (year + day number) so that
|
||||
nodelists spanning multiple years sort correctly. }
|
||||
BestFile := '';
|
||||
BestDay := -1;
|
||||
BestDate := -1;
|
||||
|
||||
if FindFirst(NormDir + DirectorySeparator + 'nodelist.*', faAnyFile, SR) = 0 then
|
||||
begin
|
||||
try
|
||||
repeat
|
||||
if (SR.Attr and faDirectory) <> 0 then Continue;
|
||||
{ Extract day number from extension }
|
||||
DayNum := StrToIntDef(Copy(ExtractFileExt(SR.Name), 2, 10), -1);
|
||||
if DayNum > BestDay then
|
||||
{ Skip files without a numeric extension }
|
||||
if StrToIntDef(Copy(ExtractFileExt(SR.Name), 2, 10), -1) < 0 then
|
||||
Continue;
|
||||
FilePath := NormDir + DirectorySeparator + SR.Name;
|
||||
DateVal := GetNodelistDateValue(FilePath);
|
||||
if DateVal > BestDate then
|
||||
begin
|
||||
BestDay := DayNum;
|
||||
BestFile := NormDir + DirectorySeparator + SR.Name;
|
||||
BestDate := DateVal;
|
||||
BestFile := FilePath;
|
||||
end;
|
||||
until FindNext(SR) <> 0;
|
||||
finally
|
||||
|
||||
@@ -111,6 +111,19 @@ function CometSendBanner(Sock: TCometSocket): Boolean;
|
||||
function CometRecvBanner(Sock: TCometSocket; TimeoutSecs: Integer;
|
||||
out PeekBuf: string): TCometHandshakeResult;
|
||||
|
||||
{ Sniff protocol on an outbound connection WITHOUT sending anything.
|
||||
Uses MSG_PEEK so the byte stays in the socket buffer.
|
||||
Waits up to TimeoutSecs for the remote to send data.
|
||||
BinkP answerers send M_NUL immediately - first byte has high bit set.
|
||||
Comet answerers send banner immediately - first byte is 'C' ($43).
|
||||
If no data arrives, remote is Comet waiting for our banner.
|
||||
Returns chrBinkP if BinkP detected.
|
||||
Returns chrOK if Comet detected (remote sent banner first).
|
||||
Returns chrTimeout if no data (assume Comet - send banner next).
|
||||
Returns chrDisconnect if connection lost. }
|
||||
function CometSniffProtocol(Sock: TCometSocket;
|
||||
TimeoutSecs: Integer): TCometHandshakeResult;
|
||||
|
||||
|
||||
{ ---- INIT/INITACK Exchange ---- }
|
||||
|
||||
@@ -321,6 +334,64 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
function CometSniffProtocol(Sock: TCometSocket;
|
||||
TimeoutSecs: Integer): TCometHandshakeResult;
|
||||
var
|
||||
Buf: Byte;
|
||||
Deadline: TDateTime;
|
||||
Got: LongInt;
|
||||
begin
|
||||
Result := chrTimeout;
|
||||
Deadline := Now + (TimeoutSecs / 86400.0);
|
||||
|
||||
while Now < Deadline do
|
||||
begin
|
||||
if not CometTcpWaitData(Sock, 500) then
|
||||
begin
|
||||
if not CometTcpConnected(Sock) then
|
||||
begin
|
||||
Result := chrDisconnect;
|
||||
Exit;
|
||||
end;
|
||||
Continue;
|
||||
end;
|
||||
|
||||
{ MSG_PEEK: read first byte WITHOUT consuming it from socket buffer.
|
||||
Both BinkP and Comet can then read it normally. }
|
||||
Got := CometTcpPeek(Sock, @Buf, 1);
|
||||
if Got < 0 then
|
||||
begin
|
||||
{$IFDEF UNIX}
|
||||
if fpGetErrno = ESysEAGAIN then Continue;
|
||||
{$ENDIF}
|
||||
Result := chrDisconnect;
|
||||
Exit;
|
||||
end;
|
||||
if Got = 0 then
|
||||
begin
|
||||
Result := chrDisconnect;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
{ First byte peeked - check for BinkP (high bit set) }
|
||||
if (Buf and $80) <> 0 then
|
||||
begin
|
||||
Result := chrBinkP;
|
||||
LogInfo('Protocol sniff: first byte $%02X - BinkP detected', [Buf]);
|
||||
end
|
||||
else
|
||||
begin
|
||||
{ Low byte = Comet banner from remote (answerer sends banner first). }
|
||||
Result := chrOK;
|
||||
LogInfo('Protocol sniff: first byte $%02X - Comet detected', [Buf]);
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
LogInfo('Protocol sniff: no data in %d sec - assuming Comet', [TimeoutSecs]);
|
||||
end;
|
||||
|
||||
|
||||
{ ---- INIT/INITACK Building ---- }
|
||||
|
||||
function CometBuildInit(const Info: TCometInitInfo): TBytes;
|
||||
|
||||
@@ -31,7 +31,7 @@ uses
|
||||
{$ELSE}
|
||||
Sockets,
|
||||
{$IFDEF UNIX}
|
||||
BaseUnix, Unix,
|
||||
BaseUnix, Unix, netdb,
|
||||
{$ENDIF}
|
||||
{$IFDEF MSWINDOWS}
|
||||
WinSock2,
|
||||
@@ -117,6 +117,11 @@ function CometTcpSend(Sock: TCometSocket; Buf: Pointer;
|
||||
function CometTcpRecv(Sock: TCometSocket; Buf: Pointer;
|
||||
MaxLen: LongInt): LongInt;
|
||||
|
||||
{ Peek at data without consuming it (MSG_PEEK). Returns bytes peeked,
|
||||
0 if nothing available, or -1 on error. Data stays in socket buffer. }
|
||||
function CometTcpPeek(Sock: TCometSocket; Buf: Pointer;
|
||||
MaxLen: LongInt): LongInt;
|
||||
|
||||
{ Send all data, looping until complete. Returns True on success. }
|
||||
function CometTcpSendAll(Sock: TCometSocket; Buf: Pointer;
|
||||
Len: LongInt): Boolean;
|
||||
@@ -444,6 +449,9 @@ var
|
||||
IA: TInAddr;
|
||||
{$ELSE}
|
||||
IA: in_addr;
|
||||
{$IFDEF UNIX}
|
||||
HEntry: THostEntry;
|
||||
{$ENDIF}
|
||||
{$ENDIF}
|
||||
begin
|
||||
{$IFDEF MSWINDOWS}
|
||||
@@ -470,6 +478,18 @@ begin
|
||||
end;
|
||||
{$ELSE}
|
||||
IA.s_addr := StrToHostAddr(Host).s_addr;
|
||||
{$IFDEF UNIX}
|
||||
{ StrToHostAddr uses FPC's internal resolver which may fail on systems
|
||||
using systemd-resolved or other non-standard DNS setups.
|
||||
Fall back to netdb.ResolveHostByName which uses the C library. }
|
||||
if IA.s_addr = 0 then
|
||||
begin
|
||||
{ ResolveHostByName returns network byte order;
|
||||
HostAddrToStr expects host byte order — swap. }
|
||||
if ResolveHostByName(Host, HEntry) then
|
||||
IA.s_addr := htonl(HEntry.Addr.s_addr);
|
||||
end;
|
||||
{$ENDIF}
|
||||
if IA.s_addr <> 0 then
|
||||
Result := HostAddrToStr(
|
||||
{$IFDEF MSWINDOWS}
|
||||
@@ -688,6 +708,29 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
function CometTcpPeek(Sock: TCometSocket; Buf: Pointer;
|
||||
MaxLen: LongInt): LongInt;
|
||||
begin
|
||||
if (Sock < 0) or (Buf = nil) or (MaxLen <= 0) then
|
||||
begin
|
||||
Result := -1;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
{$IFDEF GO32V2}
|
||||
Result := watt_recv(Sock, Buf, MaxLen, 2); { MSG_PEEK = 2 }
|
||||
{$ELSE}
|
||||
{$IFDEF UNIX}
|
||||
repeat
|
||||
Result := fpRecv(Sock, Buf, MaxLen, MSG_PEEK);
|
||||
until (Result >= 0) or (fpGetErrno <> ESysEINTR);
|
||||
{$ELSE}
|
||||
Result := fpRecv(Sock, Buf, MaxLen, MSG_PEEK);
|
||||
{$ENDIF}
|
||||
{$ENDIF}
|
||||
end;
|
||||
|
||||
|
||||
function CometTcpSendAll(Sock: TCometSocket; Buf: Pointer;
|
||||
Len: LongInt): Boolean;
|
||||
begin
|
||||
|
||||
Reference in New Issue
Block a user