Class SensitiveCommandRegistry

java.lang.Object
com.ultikits.ultitools.websocket.SensitiveCommandRegistry

public final class SensitiveCommandRegistry extends Object
The table of commands whose arguments must never reach UltiCloud, and the redaction rule that enforces it.

A command such as /login <password> carries a secret in its arguments. Anything the framework sends to UltiCloud (the panel's console log stream, including the start-up lines replayed once the stream opens; error reports; chat events) is passed through redactText(String) on its way out, which keeps a registered command's name and replaces everything after it with ***. A player-command event never carries arguments at all, registered or not: it carries commandName(String) only.

Data sent to UltiCloud is redacted, and so are the framework's own remote-command records. As of 6.4.0, when the panel's remote console runs a registered command, the framework's own lines about it (the [Remote command] > ... echo and the refusal and failure lines, which reach the server console, latest.log and the panel's log stream) and its entry in the remote-action log (plugins/UltiTools/security/action.log.*) show the command followed by *** instead of its arguments, for example [Remote command] > logadmin ***. Before 6.4.0 those local records carried the full command line. Everything else written locally keeps the original text: lines other plugins log, a player's own issued server command line in the local log files, and the LogRecord objects other handlers see. The command itself is always dispatched with its arguments.

Matching. A command line matches when its first word, with one leading / and a namespace: prefix removed, equals a registered name, compared case-insensitively (Locale.ROOT, so a Turkish or other default locale does not change the result). With login registered, all of /login, /LOGIN, ultilogin:login and /ultilogin:Login match. Names are stored in that normalised form, so registering ultilogin:login registers login.

The table starts empty when constructed. The framework's own registry (UltiTools#getSensitiveCommandRegistry()) is built by withFrameworkCommands() and, as of 6.4.0, holds the framework's one secret-carrying command, ulticloud (/ulticloud login <code> carries a single-use re-pair code). A module that owns a secret-carrying command registers it, and every alias, from its enable hook:


 public boolean registerSelf() {
     UltiTools.getInstance().getSensitiveCommandRegistry()
             .register("login", "l", "register", "reg", "changepassword");
     return true;
 }
 
Registration is safe from any thread, takes effect immediately whether or not the server is linked to UltiCloud, and is idempotent, so registering again after a module reload is harmless. There is deliberately no way to unregister: a name stays redacted until the server restarts, which errs on the side of redacting too much rather than too little, and means one module cannot weaken another module's redaction.

Unlike the rest of this package, this class is a stable, public extension point for modules.

Since:
6.3.1
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    (package private) static final String[]
    The framework's own secret-carrying commands: ulticloud, whose login <code> form carries the single-use re-pair code of the panel's "Revoke and re-pair" action (issue #869).
    private final Set<String>
    The registered names, normalised by normalize(String).
    private static final String
    What replaces a registered command's arguments.
  • Constructor Summary

    Constructors
    Constructor
    Description
    Creates an empty registry.
  • Method Summary

    Modifier and Type
    Method
    Description
    static String
    commandName(String commandLine)
    The normalised name of a command line's command: its first whitespace-delimited token, passed through normalize(String).
    private int
    findRedactionPoint(String text, int lineStart, int lineEnd)
    Finds where one line must be cut: the end of the first registered command that is followed by an argument, or -1 when the line needs no redaction.
    private static boolean
    hasArgument(String text, int from, int to)
    Whether an argument lies in [from, to): any character other than whitespace and quote characters, so a closing quote alone is not an argument.
    private static boolean
    isBoundary(char c)
    Whitespace or a quote character: what may precede a command in text and ends its token.
    boolean
    isSensitive(String commandLine)
    Whether a command line starts with a registered command.
    static String
    normalize(String commandToken)
    Normalises one command token: strips one leading / and a namespace: prefix, and lower-cases the rest with Locale.ROOT.
    Redacts one command line: when its command is registered and arguments follow, keeps the command as typed and replaces the arguments with ***.
    Redacts free text, such as a log line, an exception message or a stack trace, before it is sent to UltiCloud.
    void
    register(String... commandNames)
    Registers command names whose arguments must not reach UltiCloud.
    A snapshot of the registered names, in their normalised form.
    private static int
    skipWhitespace(String text, int from, int to)
     
    Creates the framework's registry: a new registry with FRAMEWORK_COMMANDS registered, so a player who types /ulticloud login <code> by mistake (the command is console-only and refuses, but the server still logs the line) does not stream the code to UltiCloud.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • REDACTED

      private static final String REDACTED
      What replaces a registered command's arguments.
      See Also:
    • FRAMEWORK_COMMANDS

      static final String[] FRAMEWORK_COMMANDS
      The framework's own secret-carrying commands: ulticloud, whose login <code> form carries the single-use re-pair code of the panel's "Revoke and re-pair" action (issue #869). Its other subcommands carry no arguments, so redacting the whole command loses nothing.
    • names

      private final Set<String> names
      The registered names, normalised by normalize(String).
  • Constructor Details

    • SensitiveCommandRegistry

      public SensitiveCommandRegistry()
      Creates an empty registry.
  • Method Details

    • withFrameworkCommands

      public static SensitiveCommandRegistry withFrameworkCommands()
      Creates the framework's registry: a new registry with FRAMEWORK_COMMANDS registered, so a player who types /ulticloud login <code> by mistake (the command is console-only and refuses, but the server still logs the line) does not stream the code to UltiCloud.
      Returns:
      a new registry holding the framework's own commands
      Since:
      6.4.0
    • register

      public void register(String... commandNames)
      Registers command names whose arguments must not reach UltiCloud.

      Each name is normalised first (normalize(String)), so "/Login", "login" and "ultilogin:login" all register login. A null array, and null or blank names, are ignored. Registering a name twice is harmless.

      Parameters:
      commandNames - the command names and aliases to register
    • isSensitive

      public boolean isSensitive(String commandLine)
      Whether a command line starts with a registered command.
      Parameters:
      commandLine - a command line as typed, with or without the leading /
      Returns:
      true if the line's command name is registered
    • normalize

      public static String normalize(String commandToken)
      Normalises one command token: strips one leading / and a namespace: prefix, and lower-cases the rest with Locale.ROOT.

      "/login" and "/UltiLogin:LOGIN" give "login"; "//login" gives "/login" (only one slash is stripped, so a // command stays distinct); "login:" gives an empty string.

      Parameters:
      commandToken - one command token, possibly null
      Returns:
      the normalised name, or an empty string for a null or blank token
    • commandName

      public static String commandName(String commandLine)
      The normalised name of a command line's command: its first whitespace-delimited token, passed through normalize(String).

      "/ultilogin:Login pw" gives "login", " /spawn" gives "spawn", "/MSG Alex hi" gives "msg". This is the only part of a player's command that the framework sends to UltiCloud in a command event.

      Parameters:
      commandLine - a command line as typed, possibly null
      Returns:
      the normalised command name, or an empty string for a null or blank line
    • redactCommandLine

      public String redactCommandLine(String commandLine)
      Redacts one command line: when its command is registered and arguments follow, keeps the command as typed and replaces the arguments with ***.

      With login registered, "/login hunter2" gives "/login ***", while "/login" and "/login " (no arguments) are returned unchanged, as is any line whose command is not registered.

      Parameters:
      commandLine - a command line as typed, possibly null
      Returns:
      the redacted line, or the line itself when nothing needs redacting
    • redactText

      public String redactText(String text)
      Redacts free text, such as a log line, an exception message or a stack trace, before it is sent to UltiCloud.

      Each line (\n, \r\n or \r separated; the separators are kept) is handled on its own. A command starts at a / that opens the line or follows whitespace or a quote character (", ' or `), and runs to the next whitespace or quote character. At the first such command on the line whose normalised form is registered, and that is followed by an argument (anything other than whitespace and quote characters), the command is kept and the rest of the line is replaced with ***. For example, with login and register registered:

      • Steve issued server command: /register a b becomes Steve issued server command: /register ***;
      • said "/login pw" becomes said "/login ***;
      • see http://x/login/y, //login pw (a different command) and typed "/login" are unchanged.
      The redaction is deliberately greedy: everything after the command to the end of the line goes, not just "the arguments", because where a command's arguments end cannot be known for arbitrary plugins' log formats. Losing the tail of a line is acceptable; leaking a password is not. Text with no registered command is returned unchanged (the same instance), and redacting already-redacted text changes nothing. The cost is one pass over the text; no pattern is built from the registered names.
      Parameters:
      text - the text, possibly null
      Returns:
      the redacted text, or the text itself when nothing needs redacting
    • registeredNames

      public Set<String> registeredNames()
      A snapshot of the registered names, in their normalised form.
      Returns:
      an unmodifiable copy; later registrations do not change it
    • findRedactionPoint

      private int findRedactionPoint(String text, int lineStart, int lineEnd)
      Finds where one line must be cut: the end of the first registered command that is followed by an argument, or -1 when the line needs no redaction.
    • hasArgument

      private static boolean hasArgument(String text, int from, int to)
      Whether an argument lies in [from, to): any character other than whitespace and quote characters, so a closing quote alone is not an argument.
    • isBoundary

      private static boolean isBoundary(char c)
      Whitespace or a quote character: what may precede a command in text and ends its token.
    • skipWhitespace

      private static int skipWhitespace(String text, int from, int to)