/* * Copyright (c) 2018, 2026, 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 sun.security.ssl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.nio.ByteBuffer; import java.security.cert.Certificate; import java.security.cert.Extension; import java.security.cert.X509Certificate; import java.text.MessageFormat; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.*; import jdk.internal.vm.annotation.ForceInline; import sun.security.util.HexDumpEncoder; import sun.security.util.Debug; import sun.security.x509.*; import static java.nio.charset.StandardCharsets.UTF_8; import static sun.security.ssl.Utilities.LINE_SEP; /** * Implementation of SSL logger. *
* If the system property "javax.net.debug" is not defined, the debug logging * is turned off. If the system property "javax.net.debug" is defined as * empty, the debug logger is specified by System.getLogger("javax.net.ssl"), * and applications can customize and configure the logger or use external * logging mechanisms. If the system property "javax.net.debug" is defined * and non-empty, a private debug logger which logs to System.err is used. */ public final class SSLLogger implements System.Logger { private static final System.Logger logger; // High level boolean to track whether logging is active (i.e. all/ssl). // Further checks may be necessary to determine if data is logged. private static final boolean logging; private final String loggerName; private final boolean useCompactFormat; static { String p = System.getProperty("javax.net.debug"); if (p != null) { if (p.isEmpty()) { logger = System.getLogger("javax.net.ssl"); Opt.ALL.on = true; } else { p = p.toLowerCase(Locale.ENGLISH); if (p.contains("help")) { // help option calls exit(0) help(); } // configure expanded logging mode in constructor logger = new SSLLogger("javax.net.ssl", p); if (p.contains("all")) { Opt.ALL.on = true; } else { for (Opt o : Opt.values()) { // deal with special "_" options later if (o.component.contains("_")) { continue; } if (p.contains(o.component)) { o.on = true; // remove pattern to avoid it being reused // e.g. "ssl,sslctx" parsing p = p.replaceFirst(o.component, ""); } } // "record" and "handshake" subcomponents allow // extra configuration options if (Opt.HANDSHAKE.on && p.contains("verbose")) { Opt.HANDSHAKE_VERBOSE.on = true; } if (Opt.RECORD.on) { if (p.contains("packet")) { Opt.RECORD_PACKET.on = true; } if (p.contains("plaintext")) { Opt.RECORD_PLAINTEXT.on = true; } } // finally, if only "ssl" component is declared, then // enable all subcomponents. "ssl" logs all activity // except for the "data" and "packet" categories if (Opt.SSL.on && EnumSet.allOf(Opt.class) .stream() .noneMatch(o -> o.on && o.isSubComponent)) { for (Opt opt : Opt.values()) { if (opt.isSubComponent) { opt.on = true; } } } } } // javax.net.debug would be misconfigured property with respect // to logging if value didn't contain "all" or "ssl" logging = Opt.ALL.on || Opt.SSL.on; } else { logger = null; logging = false; } } private SSLLogger(String loggerName, String options) { this.loggerName = loggerName; options = options.toLowerCase(Locale.ENGLISH); this.useCompactFormat = !options.contains("expand"); } @ForceInline public static boolean isOn() { return logging; } /** * Return true if the specific DebugOption is enabled or ALL is enabled */ public static boolean isOn(Opt option) { return Opt.ALL.on || option.on; } public static void severe(String msg, Object... params) { SSLLogger.log0(Level.ERROR, msg, params); } public static void warning(String msg, Object... params) { SSLLogger.log0(Level.WARNING, msg, params); } public static void info(String msg, Object... params) { SSLLogger.log0(Level.INFO, msg, params); } public static void fine(String msg, Object... params) { SSLLogger.log0(Level.DEBUG, msg, params); } public static void finer(String msg, Object... params) { SSLLogger.log0(Level.TRACE, msg, params); } public static void finest(String msg, Object... params) { SSLLogger.log0(Level.TRACE, msg, params); } private static void log0(Level level, String msg, Object... params) { if (logger != null && logger.isLoggable(level)) { if (params == null || params.length == 0) { logger.log(level, msg); } else { try { String formatted = SSLSimpleFormatter.formatParameters(params); // use the customized log method for SSLLogger if (logger instanceof SSLLogger) { logger.log(level, msg, formatted); } else { logger.log(level, msg + ":" + LINE_SEP + formatted); } } catch (Exception exp) { // ignore it, just for debugging. } } } } private static void help() { System.err.printf("%n%-16s %s%n", "help", "print this help message and exit"); System.err.printf("%-16s %s%n%n", "expand", "expanded (less compact) output format"); System.err.printf("%-16s %s%n", "all", "turn on all debugging"); System.err.printf("%-16s %s%n%n", "ssl", "turn on ssl debugging"); System.err.printf("The following filters can be used with ssl:%n%n"); System.err.printf(" %-14s %s%n", "defaultctx", "print default SSL initialization"); System.err.printf(" %-14s %s%n", "handshake", "print each handshake message"); System.err.printf(" %-12s %s%n", "verbose", "verbose handshake message printing (widens handshake)"); System.err.printf(" %-14s %s%n", "keymanager", "print key manager tracing"); System.err.printf(" %-14s %s%n", "record", "enable per-record tracing"); System.err.printf(" %-12s %s%n", "packet", "print raw SSL/TLS packets (widens record)"); System.err.printf(" %-12s %s%n", "plaintext", "hex dump of record plaintext (widens record)"); System.err.printf(" %-14s %s%n", "respmgr", "print OCSP response tracing"); System.err.printf(" %-14s %s%n", "session", "print session activity"); System.err.printf(" %-14s %s%n", "sessioncache", "print session cache tracing"); System.err.printf(" %-14s %s%n", "sslctx", "print SSLContext tracing"); System.err.printf(" %-14s %s%n", "trustmanager", "print trust manager tracing"); System.err.printf("%nIf \"ssl\" is specified by itself," + " all non-widening filters are enabled.%n"); System.err.printf("%nSpecifying filter options with \"ssl\"" + " includes messages for the selected categories, as well" + " as all general SSL debug messages.%n"); System.exit(0); } static String toString(Object... params) { try { return SSLSimpleFormatter.formatParameters(params); } catch (Exception exp) { return "unexpected exception thrown: " + exp.getMessage(); } } // Logs a warning message and always returns false. This method // can be used as an OR Predicate to add a log in a stream filter. static boolean logWarning(Opt option, String s) { if (SSLLogger.isOn() && option.on) { SSLLogger.warning(s); } return false; } @Override public String getName() { return loggerName; } @Override public void log(Level level, ResourceBundle rb, String message, Throwable thrwbl) { if (isLoggable(level)) { try { String formatted = SSLSimpleFormatter.format(this, level, message, thrwbl); System.err.write(formatted.getBytes(UTF_8)); } catch (Exception exp) { // ignore it, just for debugging. } } } @Override public void log(Level level, ResourceBundle rb, String message, Object... params) { if (isLoggable(level)) { try { String formatted = SSLSimpleFormatter.format(this, level, message, params); System.err.write(formatted.getBytes(UTF_8)); } catch (Exception exp) { // ignore it, just for debugging. } } } @Override public boolean isLoggable(Level level) { return level != Level.OFF; } /** * Enum representing possible debug options for JSSE debugging. *
* ALL and SSL are considered master components. Entries without an * underscore ("_"), and not ALL or SSL, are subcomponents. Entries * with an underscore ("_") denote options specific to subcomponents. *
* Fields: * - 'component': Lowercase name of the option. * - 'isSubComponent': True for subcomponents. * - 'on': Indicates whether the option is enabled. Some rule based logic * is used to determine value of this field. *
* Enabling subcomponents fine-tunes (filters) debug output.
*/
public enum Opt {
ALL,
DEFAULTCTX,
HANDSHAKE,
HANDSHAKE_VERBOSE,
KEYMANAGER,
RECORD,
RECORD_PACKET,
RECORD_PLAINTEXT,
RESPMGR,
SESSION,
SESSIONCACHE, // placeholder for 8344685
SSLCTX,
TRUSTMANAGER,
SSL; // define ssl last, helps with sslctx matching later.
final String component;
final boolean isSubComponent;
boolean on;
Opt() {
this.component = this.toString().toLowerCase(Locale.ENGLISH);
this.isSubComponent = !(component.contains("_") ||
component.equals("all") ||
component.equals("ssl"));
}
}
private static class SSLSimpleFormatter {
private static final String PATTERN = "yyyy-MM-dd kk:mm:ss.SSS z";
private static final DateTimeFormatter dateTimeFormat =
DateTimeFormatter.ofPattern(PATTERN, Locale.ENGLISH)
.withZone(ZoneId.systemDefault());
private static final MessageFormat basicCertFormat = new MessageFormat(
"""
"version" : "v{0}",
"serial number" : "{1}",
"signature algorithm": "{2}",
"issuer" : "{3}",
"not before" : "{4}",
"not after" : "{5}",
"subject" : "{6}",
"subject public key" : "{7}"
""",
Locale.ENGLISH);
private static final MessageFormat extendedCertFormat =
new MessageFormat(
"""
"version" : "v{0}",
"serial number" : "{1}",
"signature algorithm": "{2}",
"issuer" : "{3}",
"not before" : "{4}",
"not after" : "{5}",
"subject" : "{6}",
"subject public key" : "{7}",
"extensions" : [
{8}
]
""",
Locale.ENGLISH);
private static final MessageFormat messageFormatNoParas =
new MessageFormat(
"""
'{'
"logger" : "{0}",
"level" : "{1}",
"thread id" : "{2}",
"thread name" : "{3}",
"time" : "{4}",
"caller" : "{5}",
"message" : "{6}"
'}'
""",
Locale.ENGLISH);
private static final MessageFormat messageCompactFormatNoParas =
new MessageFormat(
"{0}|{1}|{2}|{3}|{4}|{5}|{6}" + LINE_SEP,
Locale.ENGLISH);
private static final MessageFormat messageFormatWithParas =
new MessageFormat(
"""
'{'
"logger" : "{0}",
"level" : "{1}",
"thread id" : "{2}",
"thread name" : "{3}",
"time" : "{4}",
"caller" : "{5}",
"message" : "{6}",
"specifics" : [
{7}
]
'}'
""",
Locale.ENGLISH);
private static final MessageFormat messageCompactFormatWithParas =
new MessageFormat(
"""
{0}|{1}|{2}|{3}|{4}|{5}|{6} (
{7}
)
""",
Locale.ENGLISH);
private static final MessageFormat keyObjectFormat = new MessageFormat(
"""
"{0}" : '{'
{1}'}'
""",
Locale.ENGLISH);
// INFO: [TH: 123450] 2011-08-20 23:12:32.3225 PDT
// log message
// log message
// ...
private static String format(SSLLogger logger, Level level,
String message, Object ... parameters) {
if (parameters == null || parameters.length == 0) {
Object[] messageFields = {
logger.loggerName,
level.getName(),
Utilities.toHexString(Thread.currentThread().threadId()),
Thread.currentThread().getName(),
dateTimeFormat.format(Instant.now()),
formatCaller(),
message
};
if (logger.useCompactFormat) {
return messageCompactFormatNoParas.format(messageFields);
} else {
return messageFormatNoParas.format(messageFields);
}
}
Object[] messageFields = {
logger.loggerName,
level.getName(),
Utilities.toHexString(Thread.currentThread().threadId()),
Thread.currentThread().getName(),
dateTimeFormat.format(Instant.now()),
formatCaller(),
message,
(logger.useCompactFormat ?
formatParameters(parameters) :
Utilities.indent(formatParameters(parameters)))
};
if (logger.useCompactFormat) {
return messageCompactFormatWithParas.format(messageFields);
} else {
return messageFormatWithParas.format(messageFields);
}
}
private static String formatCaller() {
return StackWalker.getInstance().walk(s ->
s.dropWhile(f ->
f.getClassName().startsWith("sun.security.ssl.SSLLogger") ||
f.getClassName().startsWith("java.lang.System"))
.map(f -> f.getFileName() + ":" + f.getLineNumber())
.findFirst().orElse("unknown caller"));
}
private static String formatParameters(Object ... parameters) {
StringBuilder builder = new StringBuilder(512);
boolean isFirst = true;
for (Object parameter : parameters) {
if (isFirst) {
isFirst = false;
} else {
builder.append("," + LINE_SEP);
}
if (parameter instanceof Throwable t) {
builder.append(formatThrowable(t));
} else if (parameter instanceof Certificate c) {
builder.append(formatCertificate(c));
} else if (parameter instanceof ByteArrayInputStream bis) {
builder.append(formatByteArrayInputStream(bis));
} else if (parameter instanceof ByteBuffer bb) {
builder.append(formatByteBuffer((bb)));
} else if (parameter instanceof byte[] bytes) {
builder.append(formatByteArrayInputStream(
new ByteArrayInputStream(bytes)));
} else if (parameter instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry