DownloadFREQ (File REQuest) Support Proposal
DRAFT DOCUMENT
This proposal is a draft document generated by AI and may not have been reviewed for accuracy. The technical details, implementation decisions, and schema described herein should be validated by developers familiar with FidoNet protocols and the BinktermPHP codebase before implementation begins.
Overview
This document proposes adding inbound FREQ (File REQuest) fulfillment to BinktermPHP. FREQs allow a remote FidoNet node to request specific files from your system during a binkp session, without needing a full message-based exchange. The design integrates with the existing file area system and the existing file sharing system, so access control does not need to be reinvented.
Table of Contents
Background: How FREQs Work
During a binkp session the requesting node sends one or more M_GET commands before or during the file exchange phase:
M_GET filename [password] [size_limit] [unix_timestamp]
| Field | Meaning |
|---|---|
| filename | The filename being requested (case-insensitive in practice) |
| password | Optional area or file password |
| size_limit | Max file size the requester will accept (0 = unlimited) |
| unix_timestamp | Only send if file is newer than this date (0 = any) |
The serving node responds by including the matched file(s) in its outbound file list for that session. If no match is found, or access is denied, the server simply does not send the file (no error response in the protocol).
Magic names ? some well-known filenames have special meaning: ALLFILES, FILES, or an area tag used as a filename triggers generation of a dynamic file listing rather than serving a literal file.
Access Control Model
Rather than creating a separate FREQ permission system, FREQ fulfillment integrates with the two existing access control points: file areas and file shares.
Area-level access
A file area with freq_enabled = TRUE exposes all its approved, active files to any FREQ request. An optional freq_password restricts access to callers who supply the correct password in their M_GET command.
This is appropriate for public distribution areas (software releases, nodelist distributions, file echoes).
Per-file access via shares
A file with an active shared_files entry and freq_accessible = TRUE is individually FREQable regardless of whether its area has freq_enabled. This lets you make a specific file requestable without opening the whole area.
Since the share system already supports expiration and revocation, those controls apply automatically to FREQ access too.
Resolution priority
A file is served if either condition is true:
1. Its file area has freq_enabled = TRUE (and the caller supplied the correct freq_password if one is set)
2. It has an active, non-expired shared_files row with freq_accessible = TRUE
Files with status != 'approved' are never served regardless of access settings.
Files in is_private = TRUE areas are never served via FREQ.
Database Schema Changes
file_areas additions
ALTER TABLE file_areas
ADD COLUMN freq_enabled BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN freq_password VARCHAR(255);
COMMENT ON COLUMN file_areas.freq_enabled IS 'If true, all approved files in this area are FREQable';
COMMENT ON COLUMN file_areas.freq_password IS 'Optional password required to FREQ files from this area';
CREATE INDEX idx_file_areas_freq ON file_areas (freq_enabled) WHERE freq_enabled = TRUE;
shared_files addition
ALTER TABLE shared_files
ADD COLUMN freq_accessible BOOLEAN NOT NULL DEFAULT TRUE;
COMMENT ON COLUMN shared_files.freq_accessible IS 'If true, this shared file is also accessible via binkp FREQ';
freq_accessible defaults to TRUE so that sharing a file automatically makes it FREQable. Admins can uncheck this when creating or editing a share if they want web-only access.
freq_log table
Tracks all inbound FREQ requests for auditing and rate-limit analysis.
CREATE TABLE freq_log (
id SERIAL PRIMARY KEY,
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
requesting_node VARCHAR(50) NOT NULL, -- FTN address of requester
filename VARCHAR(255) NOT NULL, -- As requested
resolved_file_id INTEGER REFERENCES files(id) ON DELETE SET NULL,
served BOOLEAN NOT NULL DEFAULT FALSE,
deny_reason VARCHAR(100), -- 'not_found' | 'password' | 'size_limit' | 'timestamp' | 'private'
file_size BIGINT, -- Size served (if served)
session_id VARCHAR(64) -- Binkp session correlation
);
CREATE INDEX idx_freq_log_node ON freq_log (requesting_node);
CREATE INDEX idx_freq_log_time ON freq_log (requested_at DESC);
FREQ Resolution Engine
A new FreqResolver class handles all lookup and access-control logic. It is called by the binkp server when a M_GET command is received.
class FreqResolver {
/
* Resolve a FREQ request to a file path ready for delivery.
*
* @param string $filename Requested filename (case-insensitive)
* @param string|null $password Password supplied by requester
* @param int $sizeLimit Max bytes requester will accept (0 = unlimited)
* @param int $newerThan Unix timestamp (0 = any age)
* @param string $callerAddr FTN address of requesting node
* @return FreqResult
*/
public function resolve(
string $filename,
?string $password,
int $sizeLimit,
int $newerThan,
string $callerAddr
): FreqResult;
/
* Resolve a magic-name request to a dynamically generated file.
* Returns null if $filename is not a recognised magic name.
*/
public function resolveMagic(string $filename, string $callerAddr): ?FreqResult;
}
class FreqResult {
public bool $served;
public ?string $filePath; // Absolute path to serve
public ?string $servedName; // Filename to present to requester
public ?int $fileId; // files.id, null for generated files
public string $denyReason; // '' if served
}
Resolution algorithm
Given filename F, password P, size_limit S, newer_than T, caller C:
1. Check magic names first ? if matched, delegate to resolveMagic()
2. Look up file by filename (case-insensitive) in:
a. file_areas WHERE freq_enabled = TRUE AND is_private = FALSE
? files WHERE LOWER(filename) = LOWER(F) AND status = 'approved'
b. shared_files WHERE freq_accessible = TRUE AND is_active = TRUE
AND (expires_at IS NULL OR expires_at > NOW())
JOIN files WHERE LOWER(filename) = LOWER(F) AND status = 'approved'
AND file_areas.is_private = FALSE
3. If no file found ? log deny_reason='not_found', return not served
4. Check password:
- If file came from a freq_enabled area AND area.freq_password IS NOT NULL:
if P != area.freq_password ? log deny_reason='password', return not served
5. Check size_limit: if S > 0 AND file.filesize > S ? log deny_reason='size_limit', return not served
6. Check newer_than: if T > 0 AND file.created_at <= unix_to_ts(T) ? not served (no log, protocol norm)
7. Log served=TRUE, return file path
Magic Names (File Lists)
When the requested filename matches a magic name, FreqResolver::resolveMagic() generates a temporary text file and returns it as the response.
| Magic name | Response |
|---|---|
| ALLFILES or FILES | Combined listing of all FREQ-enabled areas |
| <AREA_TAG> | Listing for that specific area (if freq_enabled) |
File list format
The generated listing follows the common FILES.BBS-compatible format used by most FTN software:
BINKTERM-PHP File Areas - Generated 2026-03-14
SOFTDIST - Software Distribution
----------------------------------------------------------------------
DOOM11.ZIP 1,234,567 1993-12-10 DOOM shareware v1.1
WOLF3D14.ZIP 987,654 1992-05-05 Wolfenstein 3D v1.4
UTILS - Utilities
----------------------------------------------------------------------
PKZIP204.ZIP 203,948 1993-02-01 PKZIP v2.04g
ARJ241.EXE 187,432 1994-03-15 ARJ archiver v2.41
The file is written to a temp path and cleaned up after the session ends.
Binkp Integration
Inbound FREQ handling in binkp_server.php
The binkp server already handles M_GET at the protocol level (the message type exists). The gap is that no business logic fires when one arrives.
The server needs to:
-
Parse each `M_GET filename [password] [size_limit] [timestamp]` received during session setup
-
Call `FreqResolver::resolve()` for each request
-
Add any served files to the session's outbound file list (alongside any normal outbound mail)
-
Clean up temp files (magic-name generated lists) after the session closes
// In BinkpSession, after M_ADR/M_PWD handshake:
foreach ($session->inboundGetRequests as $req) {
$result = $this->freqResolver->resolve(
$req->filename,
$req->password,
$req->sizeLimit,
$req->newerThan,
$session->remoteAddress
);
if ($result->served) {
$session->addOutboundFile($result->filePath, $result->servedName);
}
$this->freqResolver->logRequest($req, $result, $session->remoteAddress, $session->id);
}
FREQ in outbound poll sessions (binkp_poll.php)
When we initiate a poll to another node, we can also send FREQs (we request files from them). This is the outbound FREQ flow already partially implemented via the is_freq netmail flag. That flow can remain as-is for now; the inbound fulfillment side is the primary focus of this proposal.
Admin Interface
File area edit page additions
The existing file area create/edit form gains two new fields in the "Access" section:
-
Allow FREQ ? checkbox for `freq_enabled`
-
FREQ Password ? text field for `freq_password` (shown only when Allow FREQ is checked)
File share create/edit additions
The share creation dialog gains:
-
Accessible via FREQ ? checkbox for `freq_accessible` (default checked)
FREQ log viewer (/admin/freq-log)
A new admin page showing the freq_log table:
-
Columns: timestamp, requesting node, filename, served (yes/no), deny reason, file size
-
Filterable by node, filename, served/denied, date range
-
Useful for auditing and spotting abuse
Security Considerations
-
Private areas are never served ? `is_private = TRUE` areas are explicitly excluded from all FREQ resolution regardless of `freq_enabled`.
-
Only approved files ? files with `status != 'approved'` (pending, quarantined, rejected) are never served.
-
Password enforcement ? area passwords are compared server-side; no timing attack mitigation is needed given the low-frequency nature of binkp sessions.
-
Rate limiting ? the `freq_log` table enables rate limit analysis. A per-node rate limit (max N FREQs per session, max M files per day) can be added in a later phase.
-
Share expiration respected ? expired shares are not served even if the `is_active` flag is still true (the resolver checks `expires_at`).
-
Temp file cleanup ? magic-name generated files are written to a temp directory and deleted after the session ends (or after a configurable TTL for crash safety).
-
Path traversal ? `FreqResolver` resolves filenames only from database records; it never constructs filesystem paths from user-supplied input directly.
Implementation Plan
Phase 1 ? Schema and resolver
-
Migration: add `freq_enabled`, `freq_password` to `file_areas`
-
Migration: add `freq_accessible` to `shared_files`
-
Migration: create `freq_log` table
-
`FreqResolver` class with resolution algorithm and logging
-
Magic name handlers (ALLFILES, FILES, per-area tag)
Phase 2 ? Binkp integration
-
Parse `M_GET` commands in `BinkpSession`
-
Wire `FreqResolver` into server session flow
-
Temp file management for magic-name responses
Phase 3 ? Admin interface
-
File area edit form: FREQ fields
-
Share create/edit: `freq_accessible` checkbox
-
FREQ log viewer page
Phase 4 ? Outbound FREQ status tracking
-
Add `freq_status` column to the `netmail` table (`pending` | `fulfilled` | `denied` | `partial`)
-
When a FREQ netmail is sent, status starts as `pending`
-
When a binkp session with the target node completes and the expected file(s) were received, mark `fulfilled`
-
Expose status in the netmail reader (e.g. small badge: "FREQ Pending", "FREQ Fulfilled")
New and Modified Files
New
| File | Purpose |
|---|---|
| src/Freq/FreqResolver.php | FREQ resolution engine |
| src/Freq/FreqResult.php | Result value object |
| src/Freq/MagicFileListGenerator.php | Generates ALLFILES/per-area file listings |
| database/migrations/v1.11.0.21_freq_support.sql | Schema additions (freq_enabled/freq_password on file_areas, freq_accessible on shared_files, freq_log table, freq_status on netmail) |
| templates/admin/freq_log.twig | FREQ log admin page |
| routes/admin-freq-routes.php | Admin route for FREQ log viewer |
Modified
| File | Change |
|---|---|
| src/BinkP/BinkpSession.php | Parse and queue inbound M_GET commands; add resolved files to outbound list |
| scripts/binkp_server.php | Wire FreqResolver into inbound session handler |
| templates/admin/file_area_edit.twig | Add FREQ enable/password fields |
| templates/files.twig | Add FREQ accessible checkbox to share create dialog |
| routes/admin-routes.php | Register FREQ log route |
| src/FileAreaManager.php | Pass freq_enabled/freq_password/freq_accessible through CRUD methods |
Decisions
-
Open FREQ ? any FidoNet node may FREQ from us. No allowlist or downlinks-table restriction. This is standard FidoNet behaviour; access control is handled at the area level via `freq_enabled` and `freq_password`.
-
No rate limiting ? not implemented. The `freq_log` table provides data if this needs revisiting later.
-
No NODELIST serving ? nodelist FREQ support is not included.
-
Outbound FREQ status tracking ? yes. A `freq_status` column will be added to the `netmail` table to track whether a sent FREQ was fulfilled (see Phase 4 of the implementation plan).
Document Status: Draft Proposal
Last Updated: 2026-03-14
Author: AI-Generated (Requires Review)
|