blob: 70ee7026a7b6119f4f1bde98caa57e2f85750c0e [file]
/*
* Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.util;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnixDomainSocketAddress;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.function.Consumer;
import java.util.stream.Stream;
import sun.security.util.SecurityProperties;
import jdk.internal.misc.VM;
/**
* Contains static utility methods which can filter exception
* message strings for sensitive information.
*
* Code using this mechanism should use formatMsg()
* to generate a formatted (enhanced or restricted) string for exception
* messages.
*
* The methods above take variable numbers of SensitiveInfo objects
* as parameters which contain the text that may have to be filtered.
*
* The SensitiveInfo objects should be generated with one of the following:
* public static SensitiveInfo filterSocketInfo(String s)
* public static SensitiveInfo filterNonSocketInfo(String s)
* public static SensitiveInfo filterJarName(String name)
* public static SensitiveInfo filterUserName(String name)
*/
public final class Exceptions {
private Exceptions() {}
private static volatile boolean enhancedSocketExceptionText;
private static volatile boolean enhancedNonSocketExceptionText;
private static volatile boolean enhancedUserExceptionText;
private static volatile boolean enhancedJarExceptionText;
private static volatile boolean initialized = false;
/**
* Base class for generating exception messages that may
* contain sensitive information which in certain contexts
* needs to be filtered out, in case it gets revealed in
* unexpected places. Exception messages are either enhanced
* or restricted. Enhanced messages include sensitive information.
* Restricted messages don't.
*
* Sub-class for any new category that needs to be independently
* controlled. Consider using a unique value for the
* SecurityProperties.includedInExceptions(String value) mechanism
* Current values defined are "jar", "userInfo"
* "hostInfo", "hostInfoExclSocket".
*
* New code can also piggy back on existing categories
*
* A SensitiveInfo contains the following components
* all of which default to empty strings.
*
* prefix, the sensitive info itself, a suffix
* and a replacement string.
*
* The composeFilteredText(boolean enhance) method generates
* an enhanced string when enhance is true.
* This comprises (enhance == true)
* prefix + info + suffix
* When (enhance == false), then by default the output is:
* "" empty string
* However, if a replacement is set, then when enhance == false
* the output is the replacement string.
*/
public abstract static class SensitiveInfo {
String info, suffix, prefix, replacement;
boolean enhanced;
SensitiveInfo(String info) {
this.info = info;
prefix = suffix = replacement = "";
}
public SensitiveInfo prefixWith(String prefix) {
this.prefix = prefix;
return this;
}
public SensitiveInfo suffixWith(String suffix) {
this.suffix = suffix;
return this;
}
public SensitiveInfo replaceWith(String replacement) {
this.replacement = replacement;
return this;
}
public boolean enhanced() {
return enhanced;
}
/**
* Implementation should call composeFilteredText(boolean flag)
* where flag contains the boolean value of whether
* the category is enabled or not.
*/
public abstract String output();
protected String composeFilteredText(boolean enhance) {
if (enhance) {
this.enhanced = true;
return prefix + info + suffix;
} else {
return replacement;
}
}
}
static final class SocketInfo extends SensitiveInfo {
public SocketInfo(String host) {
super(host);
}
@Override
public String output() {
setup();
return super.composeFilteredText(enhancedSocketExceptionText);
}
}
static final class NonSocketInfo extends SensitiveInfo {
public NonSocketInfo(String host) {
super(host);
}
@Override
public String output() {
setup();
return super.composeFilteredText(enhancedNonSocketExceptionText);
}
}
static final class JarInfo extends SensitiveInfo {
public JarInfo(String name) {
super(name);
}
@Override
public String output() {
setup();
return super.composeFilteredText(enhancedJarExceptionText);
}
}
static final class UserInfo extends SensitiveInfo {
public UserInfo(String host) {
super(host);
}
@Override
public String output() {
setup();
return super.composeFilteredText(enhancedUserExceptionText);
}
}
// remove leading, trailing and duplicated space characters
static String trim(String s) {
int len = s.length();
if (len == 0) return s;
StringBuilder sb = new StringBuilder();
// initial value deals with leading spaces
boolean inSpace = true;
for (int i=0; i<len; i++) {
char c = s.charAt(i);
if (c == ' ') {
if (inSpace)
continue;
inSpace = true;
} else
inSpace = false;
sb.append(c);
}
int sblen = sb.length();
// last char could be a space
if (sblen > 0 && sb.charAt(sblen - 1) == ' ')
sb.deleteCharAt(sblen - 1);
return sb.toString();
}
public static SensitiveInfo filterSocketInfo(String host) {
return new SocketInfo(host);
}
public static SensitiveInfo filterNonSocketInfo(String host) {
return new NonSocketInfo(host);
}
public static SensitiveInfo filterJarName(String name) {
return new JarInfo(name);
}
public static SensitiveInfo filterUserName(String name) {
return new UserInfo(name);
}
/**
* Transform each SensitiveInfo into a String argument which is passed
* to String.format(). This string is then trimmed.
*/
public static String formatMsg(String format, SensitiveInfo... infos) {
String[] args = new String[infos.length];
int i = 0;
for (SensitiveInfo info : infos) {
args[i++] = info.output();
}
return trim(String.format(format, (Object[])args));
}
/**
* Simplification of above. Equivalent to:
* formatMsg("%s", SensitiveInfo[1]); // ie with one arg
*/
public static String formatMsg(SensitiveInfo info) {
return trim(info.output());
}
public static void setup() {
if (initialized || !VM.isBooted())
return;
enhancedSocketExceptionText = SecurityProperties.includedInExceptions("hostInfo");
enhancedNonSocketExceptionText = SecurityProperties.includedInExceptions("hostInfoExclSocket")
| enhancedSocketExceptionText;
enhancedUserExceptionText = SecurityProperties.includedInExceptions("userInfo");
enhancedJarExceptionText = SecurityProperties.INCLUDE_JAR_NAME_IN_EXCEPTIONS;
initialized = true;
}
public static boolean enhancedNonSocketExceptions() {
setup();
return enhancedNonSocketExceptionText;
}
public static boolean enhancedSocketExceptions() {
setup();
return enhancedSocketExceptionText;
}
/**
* The enhanced message text is the socket address appended to
* the original IOException message
*/
public static IOException ioException(IOException e, SocketAddress addr) {
setup();
if (!enhancedSocketExceptionText || addr == null) {
return e;
}
if (addr instanceof UnixDomainSocketAddress) {
return ofUnixDomain(e, (UnixDomainSocketAddress)addr);
} else if (addr instanceof InetSocketAddress) {
return ofInet(e, (InetSocketAddress)addr);
} else {
return e;
}
}
private static IOException ofInet(IOException e, InetSocketAddress addr) {
return create(e, String.join(": ", e.getMessage(), addr.toString()));
}
private static IOException ofUnixDomain(IOException e, UnixDomainSocketAddress addr) {
String path = addr.getPath().toString();
StringBuilder sb = new StringBuilder();
sb.append(e.getMessage());
sb.append(": ");
sb.append(path);
String enhancedMsg = sb.toString();
return create(e, enhancedMsg);
}
// return a new instance of the same type with the given detail
// msg, or if the type doesn't support detail msgs, return given
// instance.
private static <T extends Exception> T create(T e, String msg) {
try {
Class<? extends Exception> clazz = e.getClass();
@SuppressWarnings("unchecked")
Constructor<T> ctor = (Constructor<T>)clazz.getConstructor(String.class);
T e1 = (ctor.newInstance(msg));
e1.setStackTrace(e.getStackTrace());
return e1;
} catch (Exception e0) {
// Some eg AsynchronousCloseException have no detail msg
return e;
}
}
}