Add a universal command hook (FtpResponse facade) + custom cooperative transfers#97
Open
srgg wants to merge 1 commit into
Open
Add a universal command hook (FtpResponse facade) + custom cooperative transfers#97srgg wants to merge 1 commit into
srgg wants to merge 1 commit into
Conversation
Add a single extension point, setCommandHandler(FtpResponse&, command, parameter), invoked before the built-in dispatch for every command. The hook is void; its disposition is inferred from what it does with the FtpResponse capability facade: reply() -> command handled, built-in skipped rewriteCommand() -> library runs the rewritten command instead beginCustomTransfer -> caller-driven data transfer, built-in skipped (nothing) -> the original command runs FtpResponse is a narrow facade (friend of FtpServer) exposing only the hook-safe ops, so a hook cannot reach begin()/end()/handleFTP() or rebind itself. reply() being the handled-signal makes "reply then also pass" a non-issue by construction. rewriteCommand copies are bounded to the internal command[]/cmdLine[] buffers (strncpy/strnlen + alias-safe memmove). CustomTransfer streams cooperatively, one chunk per handleFTP() tick, so the control channel never blocks. onEnd() fires exactly once on every termination (done, ABOR, disconnect) through the abortTransfer() funnel, so resources never leak; it returns a custom final line or nullptr for the library default (226/426).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Command hook extension
Problem
Applications currently have no way to influence how the server handles individual FTP commands.
The existing
setCallbackandsetTransferCallbackAPIs are invoked only after a command has already been processed and returnvoid. They are useful for observing server activity, but they cannot modify, reject, or replace command handling.As a result, several common extension scenarios are currently impossible or require patching the server implementation:
Proposal
This PR introduces an optional command hook that gives applications control over the handling of individual FTP commands.
The hook is fully backward-compatible. If no hook is registered, the server behaves exactly as it does today.
API
The hook runs for every command, immediately before the server's built-in handler. It does not return a value. Instead, its interaction with
FtpResponsedetermines what happens next:res.reply("550 …")res.rewriteCommand(cmd, param)res.beginCustomTransfer(&xfer, ctx)Because handling is expressed through
FtpResponse, there is no return value to interpret incorrectly. Callingreply()marks the command as handled, so the hook cannot both send a reply and allow the built-in handler to send another one.FtpResponseintentionally exposes only these operations. Internal server methods such asbegin(),end(),handleFTP(), and callback registration are not reachable from the hook.Custom transfers
beginCustomTransfer()allows a hook to stream data that no standard FTP command can produce, such as generated reports, multi-file bundles, or on-the-fly compression.Transfers are driven incrementally from
handleFTP(), one chunk per call, so the control connection remains responsive and never blocks waiting for the producer.onEnd()is guaranteed to run exactly once regardless of how the transfer finishes:ABORend()This provides a single place to close files, release buffers, and perform cleanup.
Transfer byte counts continue flowing through the existing
setTransferCallback, so progress reporting and stall watchdogs continue to work for custom transfers.Safety
rewriteCommandcopies into the server's fixedcommand[]andcmdLine[]buffers using bounded operations (strncpy/strnlen). It also handles cases whereparampoints into the same buffer usingmemmove.Examples
Read-only server
Reject write operations while allowing all normal read commands to continue through the built-in handler:
Command aliasing
Rewrite a command while continuing to use the built-in handler.
For example,
RETR LATESTcan be translated into a request for the newest file while preserving existing RETR behavior such as resume support and access checks:Custom authentication
The hook runs before the built-in authentication flow, allowing applications to provide their own authentication mechanism.
For example, the library can still handle
USER, while the hook handlesPASSusing a token, hash, rotating one-time code, or external identity service:setAuthenticated(true)marks the session as authenticated, allowing subsequent commands to pass the built-in authentication check.isAuthenticated()can be used by hooks that need to inspect the current session state.Implement a command
Handle a custom or extension command directly without involving a data connection:
Custom generated transfer
Serve generated data that does not exist as a file on disk:
Compatibility
This change is purely additive.
No existing function signatures change, and when no command hook is installed the server behavior is unchanged.