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