Class ServerPropertiesManager

java.lang.Object
com.ultikits.ultitools.manager.ServerPropertiesManager

@Internal public class ServerPropertiesManager extends Object
Safe server.properties manager for remote editing via WebSocket. Only exposes a curated whitelist of non-sensitive keys.

Why a panel edit cannot overwrite operator content (maintainer decisions of 2026-10-04, "operator-written configuration is never overwritten" and "what code may write, by file type": a panel edit writes only the named key; UltiKits/UltiTools-Reborn#607). The file is never re-emitted: an edit replaces the value text of the one physical line that defines the key and nothing else - the header and operator comments, key order, the key text and separator as written, line terminators and every other value keep their bytes. The file is decoded the way the running server reads it (measured from the Paper 1.21.11 jar, Settings#loadFromFile: strict UTF-8, falling back to ISO-8859-1 when the file is not valid UTF-8), the new value is escaped with the Properties value grammar in a form that reader returns exactly, and the result is re-encoded in the charset it was decoded with. Before publishing, every other line is checked byte for byte and a fresh parse must return the new value and every other key unchanged; a key defined on more than one line, or over continuation lines, is refused with a reason. Publication is atomic through AtomicConfigWriter, and only while the file still holds the bytes the edit was made against. A refusal writes nothing and logs one WARNING naming the file, the key and the reason, never a value.

  • Field Details

    • LOGGER

      private static final Logger LOGGER
    • WRITE_LOCK

      private static final Object WRITE_LOCK
      Serializes panel edits of the file within this server run.
    • FILE_NAME

      private static final String FILE_NAME
      See Also:
    • serverRoot

      private final File serverRoot
    • webSocketClient

      private UltiPanelWebSocketClient webSocketClient
    • SAFE_KEYS

      private static final Set<String> SAFE_KEYS
    • V2_READABLE_KEYS

      private static final List<String> V2_READABLE_KEYS
      The keys the frozen protocol 2.0.0 contract lets a panel read, in contract order. SAFE_KEYS stays its own set: this list decides what readVersioned(int) reports, not what the v1 write path accepts.
    • V2_WRITABLE_KEYS

      private static final List<String> V2_WRITABLE_KEYS
      The keys a remote update may change: the contract's twelve. It is deliberately independent of SAFE_KEYS, which still contains the v1-only spawn-animals and spawn-npcs. online-mode and white-list must never join it: turning server authentication or the whitelist off from the panel is an elevation of privilege the operator has not granted (maintainer decision, user decision 15), so those two keys are readable and never writable.
  • Constructor Details

    • ServerPropertiesManager

      public ServerPropertiesManager(File serverRoot)
  • Method Details

    • setWebSocketClient

      @Deprecated(since="6.3.1", forRemoval=true) public void setWebSocketClient(UltiPanelWebSocketClient client)
      Deprecated, for removal: This API element is subject to removal in a future version.
      There is no replacement: the framework never calls it, and its own UltiCloud session (protocol 2.0.0) carries the panel requests. Removed in 6.4.0.
      Sets the protocol v1 client the v1 senders of this manager reply over.
      Scheduled removal version:
      6.4.0
    • getSafeProperties

      public Map<String,String> getSafeProperties()
      The whitelisted keys the file defines, decoded the way the server reads them (see the class description).
      Returns:
      key to value; empty when the file is absent, unreadable or unparseable
    • setProperty

      public boolean setProperty(String key, String value)
    • writableKeys

      public static List<String> writableKeys()
      The keys a remote update may change: the contract's twelve.
      Returns:
      an unmodifiable list of the writable keys, in contract order
    • readVersioned

      public ServerPropertiesManager.PropertiesSnapshot readVersioned(int maxContentBytes)
      Reads server.properties for the protocol 2.0.0 panel: the fourteen readable keys that are present and parse to their contract type, the writable key set, and the version a later update must send back as its baseVersion.

      The version is the SHA-256 of exactly the bytes this call read, so the values and the version always describe one file state; a key whose value does not parse to its type is left out rather than reported with a wrong type. The contract's RUNTIME_CHECKS file-content rule applies here too — the file must be valid UTF-8, within the connection's limit and free of C0 control characters other than TAB, LF and CR — because the panel reads this file to write it back.

      Parameters:
      maxContentBytes - the connection's negotiated fileMaxBytes
      Returns:
      the snapshot, or which refusal prevented one
    • updateWithBaseVersion

      public ServerPropertiesManager.PropertiesUpdateResult updateWithBaseVersion(String baseVersion, Map<String,String> changes)
      Applies a protocol 2.0.0 properties update: for each requested key, replaces the value text of the physical line that defines it and nothing else, and only while server.properties still hashes to baseVersion.

      Why this path cannot overwrite operator content (configuration-layer rule): the version is computed from the bytes read inside WRITE_LOCK, the comparison happens there too, and publication goes through AtomicConfigWriter, which re-reads the file under its own lock and replaces it only while it still holds those bytes. A panel edit therefore lands only when the operator is writing back the very content the panel read; any other state answers ServerPropertiesManager.PropertiesUpdateResult.Outcome.FILE_CHANGED and leaves every byte in place. Keys outside the contract's twelve are refused per key even when a caller reaches this method directly — that refusal is the defence in depth behind the strict payload schema, so online-mode and white-list cannot be changed remotely by any route.

      Parameters:
      baseVersion - the SHA-256 the panel read, or null to have the update refused as changed
      changes - the requested keys and their new values, already in their text form
      Returns:
      the per-key outcome, the version now on disk and, for a refusal, its reason
    • isEditableContent

      private static boolean isEditableContent(byte[] bytes, ServerPropertiesManager.PropertiesText text, int maxContentBytes)
      The contract's RUNTIME_CHECKS file-content rule over server.properties: within the connection's limit, valid UTF-8 (the manager falls back to ISO-8859-1 for the server's own reading, but the panel only ever writes UTF-8) and no C0 control character other than TAB, LF and CR, and no DEL.
    • versionOf

      private static String versionOf(byte[] bytes)
    • writeProperty

      private ServerPropertiesManager.WriteResult writeProperty(String key, String value)
      Why this exists next to setProperty(String, String): the boolean is lossy. A false could mean "the key is not on the whitelist", "the running server server's server.properties does not have the key", "there is no server.properties to write to", or "the write itself failed" — four situations with four different fixes, collapsed into one value. The batch path has to tell the caller which one happened, so the real outcome is produced here and setProperty keeps its original contract by narrowing it.

      The write changes only the line defining key (see the class description for why nothing else can change); a refusal carries its reason.

    • refuse

      private static ServerPropertiesManager.WriteResult refuse(Path path, String key, String reason)
    • lineNumbers

      private static String lineNumbers(List<ServerPropertiesManager.Definition> definitions)
    • isWhitespace

      private static boolean isWhitespace(char c)
    • endsWithOddBackslashes

      private static boolean endsWithOddBackslashes(CharSequence line)
    • splitKey

      private static int[] splitKey(CharSequence line)
      Where the key ends and the value starts in one logical line, exactly as Properties#load0 decides: the key ends at the first unescaped =, : or whitespace; then whitespace, at most one = or :, and whitespace are skipped.
      Returns:
      [keyLength, valueStart]
    • unescape

      private static String unescape(String escaped)
      Properties#loadConvert: \uXXXX, \t \n \r \f, and \x for any other x.
    • escape

      private static String escape(String value, boolean utf8)
      Properties#saveConvert for a value: a leading space, \, = : # ! and the four control escapes are escaped; any other control character, a lone surrogate, and - in a file the server reads as ISO-8859-1 - every character above ~ is written as \uXXXX, which the reader decodes in both charsets. In a UTF-8 file other characters are written as they are, as the server itself writes them.
    • byteLines

      private static List<byte[]> byteLines(byte[] bytes)
      Splits bytes into lines, each with its terminator (LF, CR or CRLF).
    • describeOutcome

      private static String describeOutcome(ServerPropertiesManager.WriteOutcome outcome)
      One line naming why a single-key write did not happen, or null when it did. Exposed through handleSet(JsonObject)'s response so a panel operator is told the reason, not just success: false.
    • handleServerProperties

      @Deprecated(since="6.3.1", forRemoval=true) public void handleServerProperties(com.google.gson.JsonObject data)
      Deprecated, for removal: This API element is subject to removal in a future version.
      Use readVersioned(int) and updateWithBaseVersion(String, Map), which the protocol 2.0.0 session calls; this v1 handler replies only over the client handed to setWebSocketClient. Removed in 6.4.0.
      Handle WebSocket message for server properties operations.
      Parameters:
      data - the message data
      Scheduled removal version:
      6.4.0
    • handleGet

      private void handleGet()
    • handleSet

      private void handleSet(com.google.gson.JsonObject data)
    • handleSetAll

      private void handleSetAll(com.google.gson.JsonObject data)
    • applySetAll

      public ServerPropertiesManager.SetAllResult applySetAll(com.google.gson.JsonObject values)
      Apply a batch of properties, report it over the socket, and hand the outcome back.

      The return value is the point. handleSetAll does not need it — it only ever builds a message — but update_config with fileName: server_properties routes through here too, and its config_update_response is supposed to distinguish "delivered" from "took effect". Before this returned anything, that path had no way to learn the answer and reported success unconditionally.

      Parameters:
      values - key → value, an explicit JSON null meaning "leave this key alone"
      Returns:
      what happened to each key
    • buildSetAllResponse

      private com.google.gson.JsonObject buildSetAllResponse(ServerPropertiesManager.SetAllResult result)
    • toJsonArray

      private static com.google.gson.JsonArray toJsonArray(List<String> keys)
    • warnIfIncomplete

      private void warnIfIncomplete(ServerPropertiesManager.SetAllResult result)
      The response above is what the panel reads; this is what a server operator reads. Both are needed: someone changing a setting that silently does not apply otherwise finds nothing on either side.
    • sendResponse

      private void sendResponse(com.google.gson.JsonObject response)