1 /*
  2  * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package sun.security.ssl;
 27 
 28 import java.io.ByteArrayInputStream;
 29 import java.io.ByteArrayOutputStream;
 30 import java.io.IOException;
 31 import java.io.PrintStream;
 32 import java.lang.System.Logger;
 33 import java.lang.System.Logger.Level;
 34 import java.nio.ByteBuffer;
 35 import java.security.cert.Certificate;
 36 import java.security.cert.Extension;
 37 import java.security.cert.X509Certificate;
 38 import java.text.MessageFormat;
 39 import java.time.Instant;
 40 import java.time.ZoneId;
 41 import java.time.format.DateTimeFormatter;
 42 import java.util.Locale;
 43 import java.util.Map;
 44 import java.util.ResourceBundle;
 45 
 46 import sun.security.action.GetPropertyAction;
 47 import sun.security.util.HexDumpEncoder;
 48 import sun.security.x509.*;
 49 
 50 import static java.nio.charset.StandardCharsets.UTF_8;
 51 
 52 /**
 53  * Implementation of SSL logger.
 54  *
 55  * If the system property "javax.net.debug" is not defined, the debug logging
 56  * is turned off.  If the system property "javax.net.debug" is defined as
 57  * empty, the debug logger is specified by System.getLogger("javax.net.ssl"),
 58  * and applications can customize and configure the logger or use external
 59  * logging mechanisms.  If the system property "javax.net.debug" is defined
 60  * and non-empty, a private debug logger implemented in this class is used.
 61  */
 62 public final class SSLLogger {
 63     private static final System.Logger logger;
 64     private static final String property;
 65     public static final boolean isOn;
 66 
 67     static {
 68         String p = GetPropertyAction.privilegedGetProperty("javax.net.debug");
 69         if (p != null) {
 70             if (p.isEmpty()) {
 71                 property = "";
 72                 logger = System.getLogger("javax.net.ssl");
 73             } else {
 74                 property = p.toLowerCase(Locale.ENGLISH);
 75                 if (property.equals("help")) {
 76                     help();
 77                 }
 78 
 79                 logger = new SSLConsoleLogger("javax.net.ssl", p);
 80             }
 81             isOn = true;
 82         } else {
 83             property = null;
 84             logger = null;
 85             isOn = false;
 86         }
 87     }
 88 
 89     private static void help() {
 90         System.err.println();
 91         System.err.println("help           print the help messages");
 92         System.err.println("expand         expand debugging information");
 93         System.err.println();
 94         System.err.println("all            turn on all debugging");
 95         System.err.println("ssl            turn on ssl debugging");
 96         System.err.println();
 97         System.err.println("The following can be used with ssl:");
 98         System.err.println("\trecord       enable per-record tracing");
 99         System.err.println("\thandshake    print each handshake message");
100         System.err.println("\tkeygen       print key generation data");
101         System.err.println("\tsession      print session activity");
102         System.err.println("\tdefaultctx   print default SSL initialization");
103         System.err.println("\tsslctx       print SSLContext tracing");
104         System.err.println("\tsessioncache print session cache tracing");
105         System.err.println("\tkeymanager   print key manager tracing");
106         System.err.println("\ttrustmanager print trust manager tracing");
107         System.err.println("\tpluggability print pluggability tracing");
108         System.err.println();
109         System.err.println("\thandshake debugging can be widened with:");
110         System.err.println("\tdata         hex dump of each handshake message");
111         System.err.println("\tverbose      verbose handshake message printing");
112         System.err.println();
113         System.err.println("\trecord debugging can be widened with:");
114         System.err.println("\tplaintext    hex dump of record plaintext");
115         System.err.println("\tpacket       print raw SSL/TLS packets");
116         System.err.println();
117         System.exit(0);
118     }
119 
120     /**
121      * Return true if the "javax.net.debug" property contains the
122      * debug check points, or System.Logger is used.
123      */
124     public static boolean isOn(String checkPoints) {
125         if (property == null) {              // debugging is turned off
126             return false;
127         } else if (property.isEmpty()) {     // use System.Logger
128             return true;
129         }                                   // use provider logger
130 
131         String[] options = checkPoints.split(",");
132         for (String option : options) {
133             option = option.trim();
134             if (!SSLLogger.hasOption(option)) {
135                 return false;
136             }
137         }
138 
139         return true;
140     }
141 
142     private static boolean hasOption(String option) {
143         option = option.toLowerCase(Locale.ENGLISH);
144         if (property.contains("all")) {
145             return true;
146         } else {
147             int offset = property.indexOf("ssl");
148             if (offset != -1 && property.indexOf("sslctx", offset) != -1) {
149                 // don't enable data and plaintext options by default
150                 if (!(option.equals("data")
151                         || option.equals("packet")
152                         || option.equals("plaintext"))) {
153                     return true;
154                 }
155             }
156         }
157 
158         return property.contains(option);
159     }
160 
161     public static void severe(String msg, Object... params) {
162         SSLLogger.log(Level.ERROR, msg, params);
163     }
164 
165     public static void warning(String msg, Object... params) {
166         SSLLogger.log(Level.WARNING, msg, params);
167     }
168 
169     public static void info(String msg, Object... params) {
170         SSLLogger.log(Level.INFO, msg, params);
171     }
172 
173     public static void fine(String msg, Object... params) {
174         SSLLogger.log(Level.DEBUG, msg, params);
175     }
176 
177     public static void finer(String msg, Object... params) {
178         SSLLogger.log(Level.TRACE, msg, params);
179     }
180 
181     public static void finest(String msg, Object... params) {
182         SSLLogger.log(Level.ALL, msg, params);
183     }
184 
185     private static void log(Level level, String msg, Object... params) {
186         if (logger.isLoggable(level)) {
187             if (params == null || params.length == 0) {
188                 logger.log(level, msg);
189             } else {
190                 try {
191                     String formatted =
192                             SSLSimpleFormatter.formatParameters(params);
193                     logger.log(level, msg, formatted);
194                 } catch (Exception exp) {
195                     // ignore it, just for debugging.
196                 }
197             }
198         }
199     }
200 
201     static String toString(Object... params) {
202         try {
203             return SSLSimpleFormatter.formatParameters(params);
204         } catch (Exception exp) {
205             return "unexpected exception thrown: " + exp.getMessage();
206         }
207     }
208 
209     private static class SSLConsoleLogger implements Logger {
210         private final String loggerName;
211         private final boolean useCompactFormat;
212 
213         SSLConsoleLogger(String loggerName, String options) {
214             this.loggerName = loggerName;
215             options = options.toLowerCase(Locale.ENGLISH);
216             this.useCompactFormat = !options.contains("expand");
217         }
218 
219         @Override
220         public String getName() {
221             return loggerName;
222         }
223 
224         @Override
225         public boolean isLoggable(Level level) {
226             return (level != Level.OFF);
227         }
228 
229         @Override
230         public void log(Level level,
231                 ResourceBundle rb, String message, Throwable thrwbl) {
232             if (isLoggable(level)) {
233                 try {
234                     String formatted =
235                         SSLSimpleFormatter.format(this, level, message, thrwbl);
236                     System.err.write(formatted.getBytes(UTF_8));
237                 } catch (Exception exp) {
238                     // ignore it, just for debugging.
239                 }
240             }
241         }
242 
243         @Override
244         public void log(Level level,
245                 ResourceBundle rb, String message, Object... params) {
246             if (isLoggable(level)) {
247                 try {
248                     String formatted =
249                         SSLSimpleFormatter.format(this, level, message, params);
250                     System.err.write(formatted.getBytes(UTF_8));
251                 } catch (Exception exp) {
252                     // ignore it, just for debugging.
253                 }
254             }
255         }
256     }
257 
258     private static class SSLSimpleFormatter {
259         private static final String PATTERN = "yyyy-MM-dd kk:mm:ss.SSS z";
260         private static final DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern(PATTERN, Locale.ENGLISH)
261                                                                                  .withZone(ZoneId.systemDefault());
262 
263         private static final MessageFormat basicCertFormat = new MessageFormat(
264                 "\"version\"            : \"v{0}\",\n" +
265                 "\"serial number\"      : \"{1}\",\n" +
266                 "\"signature algorithm\": \"{2}\",\n" +
267                 "\"issuer\"             : \"{3}\",\n" +
268                 "\"not before\"         : \"{4}\",\n" +
269                 "\"not  after\"         : \"{5}\",\n" +
270                 "\"subject\"            : \"{6}\",\n" +
271                 "\"subject public key\" : \"{7}\"\n",
272                 Locale.ENGLISH);
273 
274         private static final MessageFormat extendedCertFormart =
275             new MessageFormat(
276                 "\"version\"            : \"v{0}\",\n" +
277                 "\"serial number\"      : \"{1}\",\n" +
278                 "\"signature algorithm\": \"{2}\",\n" +
279                 "\"issuer\"             : \"{3}\",\n" +
280                 "\"not before\"         : \"{4}\",\n" +
281                 "\"not  after\"         : \"{5}\",\n" +
282                 "\"subject\"            : \"{6}\",\n" +
283                 "\"subject public key\" : \"{7}\",\n" +
284                 "\"extensions\"         : [\n" +
285                 "{8}\n" +
286                 "]\n",
287                 Locale.ENGLISH);
288 
289         //
290         // private static MessageFormat certExtFormat = new MessageFormat(
291         //         "{0} [{1}] '{'\n" +
292         //         "  critical: {2}\n" +
293         //         "  value: {3}\n" +
294         //         "'}'",
295         //         Locale.ENGLISH);
296         //
297 
298         private static final MessageFormat messageFormatNoParas =
299             new MessageFormat(
300                 "'{'\n" +
301                 "  \"logger\"      : \"{0}\",\n" +
302                 "  \"level\"       : \"{1}\",\n" +
303                 "  \"thread id\"   : \"{2}\",\n" +
304                 "  \"thread name\" : \"{3}\",\n" +
305                 "  \"time\"        : \"{4}\",\n" +
306                 "  \"caller\"      : \"{5}\",\n" +
307                 "  \"message\"     : \"{6}\"\n" +
308                 "'}'\n",
309                 Locale.ENGLISH);
310 
311         private static final MessageFormat messageCompactFormatNoParas =
312             new MessageFormat(
313                 "{0}|{1}|{2}|{3}|{4}|{5}|{6}\n",
314                 Locale.ENGLISH);
315 
316         private static final MessageFormat messageFormatWithParas =
317             new MessageFormat(
318                 "'{'\n" +
319                 "  \"logger\"      : \"{0}\",\n" +
320                 "  \"level\"       : \"{1}\",\n" +
321                 "  \"thread id\"   : \"{2}\",\n" +
322                 "  \"thread name\" : \"{3}\",\n" +
323                 "  \"time\"        : \"{4}\",\n" +
324                 "  \"caller\"      : \"{5}\",\n" +
325                 "  \"message\"     : \"{6}\",\n" +
326                 "  \"specifics\"   : [\n" +
327                 "{7}\n" +
328                 "  ]\n" +
329                 "'}'\n",
330                 Locale.ENGLISH);
331 
332         private static final MessageFormat messageCompactFormatWithParas =
333             new MessageFormat(
334                 "{0}|{1}|{2}|{3}|{4}|{5}|{6} (\n" +
335                 "{7}\n" +
336                 ")\n",
337                 Locale.ENGLISH);
338 
339         private static final MessageFormat keyObjectFormat = new MessageFormat(
340                 "\"{0}\" : '{'\n" +
341                 "{1}" +
342                 "'}'\n",
343                 Locale.ENGLISH);
344 
345         // INFO: [TH: 123450] 2011-08-20 23:12:32.3225 PDT
346         //     log message
347         //     log message
348         //     ...
349         private static String format(SSLConsoleLogger logger, Level level,
350                     String message, Object ... parameters) {
351 
352             if (parameters == null || parameters.length == 0) {
353                 Object[] messageFields = {
354                     logger.loggerName,
355                     level.getName(),
356                     Utilities.toHexString(Thread.currentThread().getId()),
357                     Thread.currentThread().getName(),
358                     dateTimeFormat.format(Instant.now()),
359                     formatCaller(),
360                     message
361                 };
362 
363                 if (logger.useCompactFormat) {
364                     return messageCompactFormatNoParas.format(messageFields);
365                 } else {
366                     return messageFormatNoParas.format(messageFields);
367                 }
368             }
369 
370             Object[] messageFields = {
371                     logger.loggerName,
372                     level.getName(),
373                     Utilities.toHexString(Thread.currentThread().getId()),
374                     Thread.currentThread().getName(),
375                     dateTimeFormat.format(Instant.now()),
376                     formatCaller(),
377                     message,
378                     (logger.useCompactFormat ?
379                         formatParameters(parameters) :
380                         Utilities.indent(formatParameters(parameters)))
381                 };
382 
383             if (logger.useCompactFormat) {
384                 return messageCompactFormatWithParas.format(messageFields);
385             } else {
386                 return messageFormatWithParas.format(messageFields);
387             }
388         }
389 
390         private static String formatCaller() {
391             return StackWalker.getInstance().walk(s ->
392                 s.dropWhile(f ->
393                     f.getClassName().startsWith("sun.security.ssl.SSLLogger") ||
394                     f.getClassName().startsWith("java.lang.System"))
395                 .map(f -> f.getFileName() + ":" + f.getLineNumber())
396                 .findFirst().orElse("unknown caller"));
397         }
398 
399         private static String formatParameters(Object ... parameters) {
400             StringBuilder builder = new StringBuilder(512);
401             boolean isFirst = true;
402             for (Object parameter : parameters) {
403                 if (isFirst) {
404                     isFirst = false;
405                 } else {
406                     builder.append(",\n");
407                 }
408 
409                 if (parameter instanceof Throwable) {
410                     builder.append(formatThrowable((Throwable)parameter));
411                 } else if (parameter instanceof Certificate) {
412                     builder.append(formatCertificate((Certificate)parameter));
413                 } else if (parameter instanceof ByteArrayInputStream) {
414                     builder.append(formatByteArrayInputStream(
415                         (ByteArrayInputStream)parameter));
416                 } else if (parameter instanceof ByteBuffer) {
417                     builder.append(formatByteBuffer((ByteBuffer)parameter));
418                 } else if (parameter instanceof byte[]) {
419                     builder.append(formatByteArrayInputStream(
420                         new ByteArrayInputStream((byte[])parameter)));
421                 } else if (parameter instanceof Map.Entry) {
422                     @SuppressWarnings("unchecked")
423                     Map.Entry<String, ?> mapParameter =
424                         (Map.Entry<String, ?>)parameter;
425                     builder.append(formatMapEntry(mapParameter));
426                 } else {
427                     builder.append(formatObject(parameter));
428                 }
429             }
430 
431             return builder.toString();
432         }
433 
434         // "throwable": {
435         //   ...
436         // }
437         private static String formatThrowable(Throwable throwable) {
438             StringBuilder builder = new StringBuilder(512);
439             ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
440             try (PrintStream out = new PrintStream(bytesOut)) {
441                 throwable.printStackTrace(out);
442                 builder.append(Utilities.indent(bytesOut.toString()));
443             }
444             Object[] fields = {
445                     "throwable",
446                     builder.toString()
447                 };
448 
449             return keyObjectFormat.format(fields);
450         }
451 
452         // "certificate": {
453         //   ...
454         // }
455         private static String formatCertificate(Certificate certificate) {
456 
457             if (!(certificate instanceof X509Certificate)) {
458                 return Utilities.indent(certificate.toString());
459             }
460 
461             StringBuilder builder = new StringBuilder(512);
462             try {
463                 X509CertImpl x509 =
464                     X509CertImpl.toImpl((X509Certificate)certificate);
465                 X509CertInfo certInfo =
466                         (X509CertInfo)x509.get(X509CertImpl.NAME + "." +
467                                                        X509CertImpl.INFO);
468                 CertificateExtensions certExts = (CertificateExtensions)
469                         certInfo.get(X509CertInfo.EXTENSIONS);
470                 if (certExts == null) {
471                     Object[] certFields = {
472                         x509.getVersion(),
473                         Utilities.toHexString(
474                                 x509.getSerialNumber().toByteArray()),
475                         x509.getSigAlgName(),
476                         x509.getIssuerX500Principal().toString(),
477                         dateTimeFormat.format(x509.getNotBefore().toInstant()),
478                         dateTimeFormat.format(x509.getNotAfter().toInstant()),
479                         x509.getSubjectX500Principal().toString(),
480                         x509.getPublicKey().getAlgorithm()
481                         };
482                     builder.append(Utilities.indent(
483                             basicCertFormat.format(certFields)));
484                 } else {
485                     StringBuilder extBuilder = new StringBuilder(512);
486                     boolean isFirst = true;
487                     for (Extension certExt : certExts.getAllExtensions()) {
488                         if (isFirst) {
489                             isFirst = false;
490                         } else {
491                             extBuilder.append(",\n");
492                         }
493                         extBuilder.append("{\n" +
494                             Utilities.indent(certExt.toString()) + "\n}");
495                     }
496                     Object[] certFields = {
497                         x509.getVersion(),
498                         Utilities.toHexString(
499                                 x509.getSerialNumber().toByteArray()),
500                         x509.getSigAlgName(),
501                         x509.getIssuerX500Principal().toString(),
502                         dateTimeFormat.format(x509.getNotBefore().toInstant()),
503                         dateTimeFormat.format(x509.getNotAfter().toInstant()),
504                         x509.getSubjectX500Principal().toString(),
505                         x509.getPublicKey().getAlgorithm(),
506                         Utilities.indent(extBuilder.toString())
507                         };
508                     builder.append(Utilities.indent(
509                             extendedCertFormart.format(certFields)));
510                 }
511             } catch (Exception ce) {
512                 // ignore the exception
513             }
514 
515             Object[] fields = {
516                     "certificate",
517                     builder.toString()
518                 };
519 
520             return Utilities.indent(keyObjectFormat.format(fields));
521         }
522 
523         private static String formatByteArrayInputStream(
524                 ByteArrayInputStream bytes) {
525             StringBuilder builder = new StringBuilder(512);
526 
527             try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream()) {
528                 HexDumpEncoder hexEncoder = new HexDumpEncoder();
529                 hexEncoder.encodeBuffer(bytes, bytesOut);
530 
531                 builder.append(Utilities.indent(bytesOut.toString()));
532             } catch (IOException ioe) {
533                 // ignore it, just for debugging.
534             }
535 
536             return builder.toString();
537         }
538 
539         private static String formatByteBuffer(ByteBuffer byteBuffer) {
540             StringBuilder builder = new StringBuilder(512);
541             try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream()) {
542                 HexDumpEncoder hexEncoder = new HexDumpEncoder();
543                 hexEncoder.encodeBuffer(byteBuffer.duplicate(), bytesOut);
544                 builder.append(Utilities.indent(bytesOut.toString()));
545             } catch (IOException ioe) {
546                 // ignore it, just for debugging.
547             }
548 
549             return builder.toString();
550         }
551 
552         private static String formatMapEntry(Map.Entry<String, ?> entry) {
553             String key = entry.getKey();
554             Object value = entry.getValue();
555 
556             String formatted;
557             if (value instanceof String) {
558                 // "key": "value"
559                 formatted = "\"" + key + "\": \"" + (String)value + "\"";
560             } else if (value instanceof String[]) {
561                 // "key": [ "string a",
562                 //          "string b",
563                 //          "string c"
564                 //        ]
565                 StringBuilder builder = new StringBuilder(512);
566                 String[] strings = (String[])value;
567                 builder.append("\"" + key + "\": [\n");
568                 for (String string : strings) {
569                     builder.append("      \"" + string + "\"");
570                     if (string != strings[strings.length - 1]) {
571                         builder.append(",");
572                     }
573                     builder.append("\n");
574                 }
575                 builder.append("      ]");
576 
577                 formatted = builder.toString();
578             } else if (value instanceof byte[]) {
579                 formatted = "\"" + key + "\": \"" +
580                     Utilities.toHexString((byte[])value) + "\"";
581             } else if (value instanceof Byte) {
582                 formatted = "\"" + key + "\": \"" +
583                     Utilities.toHexString((byte)value) + "\"";
584             } else {
585                 formatted = "\"" + key + "\": " +
586                     "\"" + value.toString() + "\"";
587             }
588 
589             return Utilities.indent(formatted);
590         }
591 
592         private static String formatObject(Object obj) {
593             return obj.toString();
594         }
595     }
596 }