Class RemoteFileOperations
- All Implemented Interfaces:
CloudRequestHandler
files.* requests for one connection; internal, not
for module use.
Every request resolves its path through FileOperationManager.resolveTarget(java.lang.String, com.ultikits.ultitools.manager.FileOperationManager.ResolveMode) unchanged,
so the whole 6.3.0 guard set applies to protocol v2 exactly as it applied to v1: the editable
roots, the protected server files and extensions, the credential basenames and glob patterns,
the remote action log's directory and (since plan 22-07) the framework's own
.ultikits/ state directory. This class never builds a File from a request path
on its own; it acts only on the operand the guard returns.
Every file decision is recorded. Protocol v1 recorded one RemoteActionLog
entry per remote file operation, reads and listings included (phase 6 D-22); this handler keeps
that property for all four requests: one entry per request that reached a decision, ALLOWED when
the request was carried out (the file read, the directory listed, the change made) and DENIED
when it was answered with an error code, with the path as the target (empty for the root
listing), the literal "panel" as the actor and — for a denial — exactly the text the
panel received. A malformed payload is answered by the router before it reaches a decision and
records nothing. Content is never recorded. The log itself is injected, so the handler stays
testable and the wiring (plan 22-21) binds it to the live RemoteActionLog. Reads keep
being answered while the server disables, but from the first step of the disable they are no
longer recorded: the disable closes the action log, and a read changes nothing.
It is package-private on purpose: resolveTarget and ResolvedTarget are
package-private, and keeping the handler in this package is what makes calling them — rather
than reimplementing a second, weaker path walk — the only possible way to answer a request.
Results are the contract's *.result payloads: status: ok with the typed data,
or status: error whose code is one of the codes the vendored schema lists for
that result type. Payload validation failures are not answered here; they leave
PayloadReader as an unchecked invalid_message that
AgentMessageRouter turns into a correlated protocol error
before any filesystem access happens.
Why this path cannot overwrite operator content. files.write replaces a file
only while the baseVersion the panel sent still equals the SHA-256 of the bytes on disk,
and that comparison and the replacement happen under the one JVM-wide lock every configuration
load and commit also holds (AtomicConfigWriter.underWriteLock(java.util.function.Supplier<T>)): a config.update or
a module's save() of the same file can therefore never commit between this comparison and
this replacement, and this replacement never lands between their own last-moment re-read and
their rename; one of the two sees the other's bytes and refuses. A files.delete removes
its entry under the same lock. A version that matches
means the panel is writing back exactly the bytes it read, so the operator's content is changed
only by an explicit panel edit of that content. Disabling the write path leaves every file
untouched. A writer outside this framework that changes the file in the window between the
comparison and the atomic move is a niche case (the operator editing server files while the
panel writes the same file) and is not defended further: the next read reports the version the
file then has, and the panel's next write is compared against it again.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate static final classCollects whyPosixAttributePreserver.replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener)declined the replacement.private static final classOne refusal from the locked write section: a contract error code and its message. -
Field Summary
FieldsModifier and TypeFieldDescriptionprivate final Consumer<RemoteActionLog.Entry> private static final Comparator<File> private static final StringThe refusal text everyfile_changedanswer carries.private final FileOperationManagerprivate static final Stringprivate static final Stringprivate static final Stringprivate static final Stringprivate static final Stringprivate static final Stringprivate static final Stringprivate static final Stringprivate static final Patternfiles.listaccepts the empty root path in addition to aServerPath.private static final Loggerprivate static final intThe contract's upper bound on acontentfield, in code points.private static final intThe contract's cap on one directory listing; above it the reply is truncated.private static final intThe contract's upper bound on a result message, in code points.private static final intThe contract's upper bound on apathfield, in code points.private static final StringThe action-log actor for this surface: protocol v2 carries no per-operator identity.private final BooleanSupplierprivate final Fileprivate RunnableTest seam; seesetWriteGate(Runnable). -
Constructor Summary
ConstructorsConstructorDescriptionRemoteFileOperations(FileOperationManager files, File serverRoot, Consumer<RemoteActionLog.Entry> actionLog) Creates the handler for one server.RemoteFileOperations(FileOperationManager files, File serverRoot, Consumer<RemoteActionLog.Entry> actionLog, BooleanSupplier readsAudited) Creates the handler with an explicit answer to "are reads recorded now". -
Method Summary
Modifier and TypeMethodDescriptionallowedChildren(String basePath, File[] children) private booleanappendEditableRoots(com.google.gson.JsonArray entries) Lists the configured editable roots as directory entries named by their server-root-relative path — the very spelling a subsequentfiles.listof that name needs.private voidauditAllowed(Capability capability, String action, String target) private voidauditDenied(Capability capability, String action, String target, String reason) private voidauditReadAllowed(String action, String target) Records a served read or listing, unless the server is disabling: the disable closes the action log, and a read changes nothing, so leaving it out loses no change (the shutdown window records no remote action).private voidauditReadDenied(String action, String target, String reason) Records a refused read or listing, unless the server is disabling, asauditReadAllowed(java.lang.String, java.lang.String)does.private static StringThe refusal text as the panel receives it: never empty and never above the contract's cap.compareAndReplace(File file, byte[] content, String baseVersion, int maxBytes) (package private) static StringcontentRefusal(byte[] bytes, int maxBytes) The contract'sfile-contentrule over raw bytes.private static StringdecodeUtf8(byte[] bytes) private voiddelete(CloudRequest request, CloudChannel channel) Answersfiles.delete: removes one file, or one symbolic link as the link itself.private static com.google.gson.JsonObjectprivate static voidSends a correlated error for one result type.voidhandle(CloudRequest request, CloudChannel channel) Validates the payload and handles it on the dispatching thread.private static booleanisForbiddenControl(char character) private voidlist(CloudRequest request, CloudChannel channel) Answersfiles.list: one directory's entries, with the size and modification time of files (nullfor directories), or — for the empty path — the configured editable roots, which is what the contract means by the server root: the panel may browse no further up than the operator's grant.private voidread(CloudRequest request, CloudChannel channel) Answersfiles.read: the file's content decoded as strict UTF-8, its size in bytes and the SHA-256 of exactly those bytes — the version a laterfiles.writemust send back as itsbaseVersion.private static byte[]readAtMost(File file, int maxBytes) Reads a file through a stream capped atmaxBytes + 1bytes.private voidrecord(RemoteActionLog.Entry entry) Hands one entry to the action log.private voidrefuseDelete(CloudChannel channel, String replyTo, String path, String code, String message) Answers a refusedfiles.deleteand records it, asrefuseWrite(com.ultikits.ultitools.websocket.CloudChannel, java.lang.String, java.lang.String, java.lang.String, java.lang.String)does.private voidrefuseList(CloudChannel channel, String replyTo, String path, String code, String message) Answers a refusedfiles.listand records it, asrefuseRead(com.ultikits.ultitools.websocket.CloudChannel, java.lang.String, java.lang.String, java.lang.String, java.lang.String)does.private voidrefuseRead(CloudChannel channel, String replyTo, String path, String code, String message) Answers a refusedfiles.readand records it, asrefuseWrite(com.ultikits.ultitools.websocket.CloudChannel, java.lang.String, java.lang.String, java.lang.String, java.lang.String)does.private voidrefuseWrite(CloudChannel channel, String replyTo, String path, String code, String message) Answers a refusedfiles.writeand records the same text in the action log, so the log says what the operator was told.private StringrelativeToServerRoot(File root) (package private) voidsetWriteGate(Runnable gate) Installs a hook that runs inside the write lock after the version comparison passed and immediately before the replacement.(package private) static Stringsha256Hex(byte[] bytes) private voidwrite(CloudRequest request, CloudChannel channel) Answersfiles.write: replaces an existing file's content when, and only when, the panel'sbaseVersionstill equals the SHA-256 of the bytes on disk.writeLocked(File file, byte[] content, String baseVersion, int maxBytes) The critical section offiles.write: compare the version against the bytes on disk and replace the file, both underAtomicConfigWriter.underWriteLock(java.util.function.Supplier<T>), the lock every configuration load, last-moment check and commit of this JVM also holds.
-
Field Details
-
LOGGER
-
FILES_LIST
- See Also:
-
FILES_LIST_RESULT
- See Also:
-
FILES_READ
- See Also:
-
FILES_READ_RESULT
- See Also:
-
FILES_WRITE
- See Also:
-
FILES_WRITE_RESULT
- See Also:
-
FILES_DELETE
- See Also:
-
FILES_DELETE_RESULT
- See Also:
-
MAX_MESSAGE_LENGTH
private static final int MAX_MESSAGE_LENGTHThe contract's upper bound on a result message, in code points.- See Also:
-
MAX_PATH_LENGTH
private static final int MAX_PATH_LENGTHThe contract's upper bound on apathfield, in code points.- See Also:
-
MAX_CONTENT_LENGTH
private static final int MAX_CONTENT_LENGTHThe contract's upper bound on acontentfield, in code points.- See Also:
-
MAX_LIST_ENTRIES
private static final int MAX_LIST_ENTRIESThe contract's cap on one directory listing; above it the reply is truncated.- See Also:
-
LIST_PATH
files.listaccepts the empty root path in addition to aServerPath. -
PANEL_ACTOR
The action-log actor for this surface: protocol v2 carries no per-operator identity.- See Also:
-
CHANGED_MESSAGE
The refusal text everyfile_changedanswer carries.- See Also:
-
BY_NAME
-
files
-
serverRoot
-
actionLog
-
readsAudited
-
writeGate
Test seam; seesetWriteGate(Runnable).
-
-
Constructor Details
-
RemoteFileOperations
RemoteFileOperations(FileOperationManager files, File serverRoot, Consumer<RemoteActionLog.Entry> actionLog) Creates the handler for one server.- Parameters:
files- the v1 file manager whose guard and editable-root configuration this handler reuses rather than duplicatingserverRoot- the server's root directory, used to name the editable rootsactionLog- receives one entry per decided request, ornullwhen the caller has no action log to write to (tests and headless harnesses)
-
RemoteFileOperations
RemoteFileOperations(FileOperationManager files, File serverRoot, Consumer<RemoteActionLog.Entry> actionLog, BooleanSupplier readsAudited) Creates the handler with an explicit answer to "are reads recorded now".- Parameters:
files- as forRemoteFileOperations(FileOperationManager, File, Consumer)serverRoot- as forRemoteFileOperations(FileOperationManager, File, Consumer)actionLog- as forRemoteFileOperations(FileOperationManager, File, Consumer)readsAudited- whether afiles.readorfiles.listdecision is recorded; production asksPluginInitiationUtils.isAcceptingPanelMessages(), so from the first step of the disable a read is still answered but writes no line to the closing action log
-
-
Method Details
-
setWriteGate
Installs a hook that runs inside the write lock after the version comparison passed and immediately before the replacement. It exists so a test can hold one writer between its check and its write while a second writer (anotherfiles.writeor a configuration commit) waits for the lock; production leaves it null, which costs one null check per write.- Parameters:
gate- the hook to run, or null to remove it
-
handle
Description copied from interface:CloudRequestHandlerValidates the payload and handles it on the dispatching thread. No Bukkit main-thread scheduling is implied; handlers must schedule such work themselves. Synchronous payload validation failures become correlated invalid_message replies; other runtime failures become internal_error without exposing exception text. Asynchronous failures are the handler's responsibility.- Specified by:
handlein interfaceCloudRequestHandler- Parameters:
request- the parsed envelope with a defensively copied payloadchannel- the reply channel, which may close during processing
-
read
Answersfiles.read: the file's content decoded as strict UTF-8, its size in bytes and the SHA-256 of exactly those bytes — the version a laterfiles.writemust send back as itsbaseVersion.The contract's RUNTIME_CHECKS
file-contentrule is applied to the raw bytes read from disk, before decoding into a reply: a file above the negotiatedfileMaxBytes, bytes that are not valid UTF-8, or a C0 control character other than TAB, LF and CR (and DEL) answerfile_not_editable. The rule exists so the panel never receives content it cannot render and write back unchanged. -
list
Answersfiles.list: one directory's entries, with the size and modification time of files (nullfor directories), or — for the empty path — the configured editable roots, which is what the contract means by the server root: the panel may browse no further up than the operator's grant.A child whose own path the guard refuses (a credential file, a
.jar, a link whose real target lies outside the roots) is left out of the listing rather than marked, because a v2 listing carries no per-entry refusal marker; the panel therefore never learns that such a name exists. Above 2000 entries the reply keeps the first 2000 by name and setstruncated, so one directory cannot produce an oversized frame. -
appendEditableRoots
private boolean appendEditableRoots(com.google.gson.JsonArray entries) Lists the configured editable roots as directory entries named by their server-root-relative path — the very spelling a subsequentfiles.listof that name needs. A root that is outside the server root, or whose name the guard refuses, cannot be addressed by any validServerPathand is skipped: listing it would offer the panel a name that every later request refuses.- Parameters:
entries- receives one entry per addressable root- Returns:
- whether the contract's 2000-entry cap was exceeded
-
relativeToServerRoot
-
allowedChildren
-
entryOf
-
write
Answersfiles.write: replaces an existing file's content when, and only when, the panel'sbaseVersionstill equals the SHA-256 of the bytes on disk.The comparison and the atomic replacement happen under the JVM-wide configuration write lock (
AtomicConfigWriter.underWriteLock(java.util.function.Supplier<T>)), so concurrent writers cannot both succeed — neither twofiles.writerequests nor afiles.writeand a configuration commit (config.update, a module'ssave()) of the same file. The replacement goes throughPosixAttributePreserver.replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener), which keeps the file's POSIX permissions, owner and group and refuses to write through a symbolic link or over an operator-pinned read-only file (write_denied). The write never creates a file. -
writeLocked
private RemoteFileOperations.WriteOutcome writeLocked(File file, byte[] content, String baseVersion, int maxBytes) The critical section offiles.write: compare the version against the bytes on disk and replace the file, both underAtomicConfigWriter.underWriteLock(java.util.function.Supplier<T>), the lock every configuration load, last-moment check and commit of this JVM also holds.A current file above the connection's
fileMaxBytesis answeredfile_changedwithout being read: the panel can never have received such a file's content, so nobaseVersionit holds can equal that file's bytes. Refusing it here also keeps a large log or plugin database from being loaded into the heap merely to compare a hash.- Parameters:
file- the resolved target the guard allowedcontent- the exact bytes to writebaseVersion- the version the panel readmaxBytes- the connection'sfileMaxBytes- Returns:
- an empty outcome when the file now holds
content, otherwise the refusal
-
compareAndReplace
private RemoteFileOperations.WriteOutcome compareAndReplace(File file, byte[] content, String baseVersion, int maxBytes) -
delete
Answersfiles.delete: removes one file, or one symbolic link as the link itself. A directory is refused withis_directory— the contract has no recursive delete, and a panel delete must never remove a tree an operator built. The entry is resolved in entry mode (FileOperationManager.ResolveMode.ENTRY), so a link to a directory is judged and removed as the link, never followed. -
readAtMost
Reads a file through a stream capped atmaxBytes + 1bytes.The extra byte is how a file that grew between the caller's size check and this read is still detected: the result is then one byte longer than the limit and the caller's existing content rule refuses it. Without the cap a file of any size would be loaded into the heap before the limit could be applied — the reason this helper exists at all.
- Parameters:
file- the file to readmaxBytes- the connection'sfileMaxBytes- Returns:
- the file's bytes, or
maxBytes + 1of them when the file is larger - Throws:
IOException- if the file cannot be opened or read
-
contentRefusal
The contract'sfile-contentrule over raw bytes.- Parameters:
bytes- the bytes that would be sent, or written, as file contentmaxBytes- the negotiatedfileMaxBytes- Returns:
- why the bytes are not editable content, or
nullwhen they are
-
isForbiddenControl
private static boolean isForbiddenControl(char character) -
decodeUtf8
-
sha256Hex
-
error
private static void error(CloudChannel channel, String replyTo, String type, String code, String message) Sends a correlated error for one result type. The message is the guard's own refusal text where one exists; it is capped at the contract's 500 code points because a schema-violating frame would be dropped by the gateway instead of explaining the refusal to the operator. -
refuseWrite
private void refuseWrite(CloudChannel channel, String replyTo, String path, String code, String message) Answers a refusedfiles.writeand records the same text in the action log, so the log says what the operator was told. -
refuseRead
private void refuseRead(CloudChannel channel, String replyTo, String path, String code, String message) Answers a refusedfiles.readand records it, asrefuseWrite(com.ultikits.ultitools.websocket.CloudChannel, java.lang.String, java.lang.String, java.lang.String, java.lang.String)does. The entry is written before the reply, so whoever sees the answer also finds the line. -
refuseList
private void refuseList(CloudChannel channel, String replyTo, String path, String code, String message) Answers a refusedfiles.listand records it, asrefuseRead(com.ultikits.ultitools.websocket.CloudChannel, java.lang.String, java.lang.String, java.lang.String, java.lang.String)does. -
refuseDelete
private void refuseDelete(CloudChannel channel, String replyTo, String path, String code, String message) Answers a refusedfiles.deleteand records it, asrefuseWrite(com.ultikits.ultitools.websocket.CloudChannel, java.lang.String, java.lang.String, java.lang.String, java.lang.String)does. -
auditAllowed
-
auditDenied
-
auditReadAllowed
Records a served read or listing, unless the server is disabling: the disable closes the action log, and a read changes nothing, so leaving it out loses no change (the shutdown window records no remote action). -
auditReadDenied
Records a refused read or listing, unless the server is disabling, asauditReadAllowed(java.lang.String, java.lang.String)does. -
record
Hands one entry to the action log. A missing sink is a silent no-op (tests, headless harnesses), and a sink that throws must not cost the panel its reply — the request itself has already been decided. -
capped
The refusal text as the panel receives it: never empty and never above the contract's cap.
-