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.

Only data sent to UltiCloud is redacted. The server's own console, its log files and the LogRecord objects other handlers see keep the original text: the operator who can read the local console can already read what the players typed.

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. The framework registers nothing on its own; 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
    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)
     

    Methods inherited from class java.lang.Object

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

  • Constructor Details

    • SensitiveCommandRegistry

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

    • 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)