package com.github.kpavlov.jreactive8583.netty.pipeline; import com.solab.iso8583.IsoMessage; import com.solab.iso8583.IsoValue; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Properties; /** * ChannelHandler responsible for logging messages. * <p> * According to PCI DSS, sensitive cardholder data, like PAN and track data, should not be exposed. When running in secure mode, sensitive cardholder data will be printed masked. </p> */ @ChannelHandler.Sharable public class IsoMessageLoggingHandler extends LoggingHandler { private static final char MASK_CHAR = '*'; private static final int[] DEFAULT_MASKED_FIELDS = { 34,// PAN extended 35,// track 2 36,// track 3 45// track 1 }; private static char[] MASKED_VALUE = "***".toCharArray(); private static String[] FIELD_NAMES = new String[128]; static { try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("com/github/kpavlov/jreactive8583/iso8583fields.properties")) { final Properties properties = new Properties(); properties.load(stream); properties.forEach((key, value) -> { int field = Integer.parseInt((String) key); FIELD_NAMES[field - 1] = (String) value; }); } catch (IOException | NumberFormatException e) { throw new IllegalStateException("Unable to load ISO8583 field descriptions", e); } } private final boolean printSensitiveData; private final boolean printFieldDescriptions; private final int[] maskedFields; public IsoMessageLoggingHandler(LogLevel level, boolean printSensitiveData, boolean printFieldDescriptions, int... maskedFields) { super(level); this.printSensitiveData = printSensitiveData; this.printFieldDescriptions = printFieldDescriptions; this.maskedFields = (maskedFields != null && maskedFields.length > 0) ? maskedFields : DEFAULT_MASKED_FIELDS; } public IsoMessageLoggingHandler(LogLevel level) { this(level, true, true); } private static char[] maskPAN(String fullPan) { char[] maskedPan = fullPan.toCharArray(); for (int i = 6; i < maskedPan.length - 4; i++) { maskedPan[i] = MASK_CHAR; } return maskedPan; } @Override protected String format(ChannelHandlerContext ctx, String eventName, Object arg) { if (arg instanceof IsoMessage) { return super.format(ctx, eventName, formatIsoMessage((IsoMessage) arg)); } else { return super.format(ctx, eventName, arg); } } private String formatIsoMessage(IsoMessage m) { StringBuilder sb = new StringBuilder(); if (printSensitiveData) { sb.append("Message: ").append(m.debugString()).append("\n"); } sb.append("MTI: 0x").append(String.format("%04x", m.getType())); for (int i = 2; i < 128; i++) { if (m.hasField(i)) { final IsoValue<Object> field = m.getField(i); sb.append("\n ").append(i) .append(": ["); if (printFieldDescriptions) { sb.append(FIELD_NAMES[i - 1]).append(':'); } char[] formattedValue; if (printSensitiveData) { formattedValue = field.toString().toCharArray(); } else { if (i == 2) { formattedValue = maskPAN(field.toString()); } else if (Arrays.binarySearch(maskedFields, i) >= 0) { formattedValue = MASKED_VALUE; } else { formattedValue = field.toString().toCharArray(); } } sb.append(field.getType()).append('(').append(field.getLength()) .append(")] = '").append(formattedValue).append('\''); } } return sb.toString(); } }