001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.transaction.participant; 020 021import java.util.Arrays; 022import java.util.Set; 023import java.util.HashSet; 024import java.util.StringTokenizer; 025import java.io.Serializable; 026import java.util.regex.Pattern; 027 028import org.jpos.core.*; 029import org.jpos.iso.*; 030import org.jpos.rc.CMF; 031import org.jpos.rc.Result; 032import org.jpos.transaction.Context; 033import org.jpos.transaction.ContextConstants; 034import org.jpos.transaction.TransactionParticipant; 035import org.jpos.util.Caller; 036 037import static org.jpos.transaction.ContextConstants.*; 038 039/** 040 * Transaction participant that validates the presence and shape of selected 041 * ISOMsg fields against the configured rules, marking the transaction with 042 * {@link CMF}-coded errors when validation fails. 043 */ 044public class CheckFields implements TransactionParticipant, Configurable { 045 /** Default constructor; no instance state to initialise. */ 046 public CheckFields() {} 047 private Configuration cfg; 048 private String request; 049 private Pattern PCODE_PATTERN = Pattern.compile("^[\\d|\\w]{6}$"); 050 private Pattern TID_PATTERN = Pattern.compile("^[\\w\\s]{1,16}"); 051 private Pattern MID_PATTERN = Pattern.compile("^[\\w\\s]{1,15}"); 052 private Pattern TRANSMISSION_TIMESTAMP_PATTERN = Pattern.compile("^\\d{10}"); 053 private Pattern LOCAL_TIMESTAMP_PATTERN = Pattern.compile("^\\d{14}"); 054 private Pattern CAPTUREDATE_PATTERN = Pattern.compile("^\\d{4}"); 055 private Pattern ORIGINAL_DATA_ELEMENTS_PATTERN = Pattern.compile("^\\d{30,41}$"); 056 private boolean ignoreCardValidation = false; 057 private boolean allowExtraFields = false; 058 private Pattern track1Pattern = null; 059 private Pattern track2Pattern = null; 060 061 public int prepare (long id, Serializable context) { 062 Context ctx = (Context) context; 063 Result rc = ctx.getResult(); 064 try { 065 ISOMsg m = ctx.get (request); 066 if (m == null) { 067 ctx.getResult().fail(CMF.INVALID_TRANSACTION, Caller.info(), "'%s' not available in Context", request); 068 return ABORTED | NO_JOIN | READONLY; 069 } 070 Set<String> validFields = new HashSet<>(); 071 assertFields (ctx, m, cfg.get ("mandatory", ""), true, validFields, rc); 072 assertFields (ctx, m, cfg.get ("optional", ""), false, validFields, rc); 073 if (!allowExtraFields) assertNoExtraFields (m, validFields, rc); 074 } catch (Throwable t) { 075 rc.fail(CMF.SYSTEM_ERROR, Caller.info(), t.getMessage()); 076 ctx.log(t); 077 } 078 return (rc.hasFailures() ? ABORTED : PREPARED) | NO_JOIN | READONLY; 079 } 080 081 public void setConfiguration (Configuration cfg) { 082 this.cfg = cfg; 083 request = cfg.get ("request", ContextConstants.REQUEST.toString()); 084 ignoreCardValidation = cfg.getBoolean("ignore-card-validation", false); 085 allowExtraFields = cfg.getBoolean("allow-extra-fields", false); 086 String t1 = cfg.get("track1-pattern", null); 087 if (t1 != null) { 088 track1Pattern = Pattern.compile(t1); 089 } 090 String t2 = cfg.get("track2-pattern", null); 091 if (t2 != null) { 092 track2Pattern = Pattern.compile(t2); 093 } 094 } 095 096 private void assertFields(Context ctx, ISOMsg m, String fields, boolean mandatory, Set<String> validFields, Result rc) { 097 StringTokenizer st = new StringTokenizer (fields, ", "); 098 while (st.hasMoreTokens()) { 099 String s = st.nextToken(); 100 ContextConstants k = null; 101 try { 102 k = ContextConstants.valueOf(s); 103 } catch (IllegalArgumentException ignored) { } 104 if (k != null) { 105 switch (k) { 106 case PCODE: 107 putPCode(ctx, m, mandatory, validFields, rc); 108 break; 109 case CARD: 110 putCard(ctx, m, mandatory, validFields, rc); 111 break; 112 case TID: 113 putTid(ctx, m, mandatory, validFields, rc); 114 break; 115 case MID: 116 putMid(ctx, m, mandatory, validFields, rc); 117 break; 118 case TRANSMISSION_TIMESTAMP: 119 putTimestamp(ctx, m, TRANSMISSION_TIMESTAMP.toString(), 7, TRANSMISSION_TIMESTAMP_PATTERN, mandatory, validFields, rc); 120 break; 121 case TRANSACTION_TIMESTAMP: 122 putTimestamp(ctx, m, TRANSACTION_TIMESTAMP.toString(), 12, LOCAL_TIMESTAMP_PATTERN, mandatory, validFields, rc); 123 break; 124 case POS_DATA_CODE: 125 putPDC(ctx, m, mandatory, validFields, rc); 126 break; 127 case CAPTURE_DATE: 128 putCaptureDate(ctx, m, mandatory, validFields, rc); 129 break; 130 case AMOUNT: 131 putAmount(ctx, m, mandatory, validFields, rc); 132 break; 133 case ORIGINAL_DATA_ELEMENTS: 134 putOriginalDataElements(ctx, m, mandatory, validFields, rc); 135 break; 136 default: 137 k = null; 138 } 139 } 140 if (k == null) { 141 if (mandatory && !m.hasField(s)) 142 rc.fail(CMF.MISSING_FIELD, Caller.info(), s); 143 else 144 validFields.add(s); 145 } 146 } 147 } 148 private void assertNoExtraFields (ISOMsg m, Set validFields, Result rc) { 149 StringBuilder sb = new StringBuilder(); 150 for (int i=1; i<=m.getMaxField(); i++) { // we start at 1, MTI is always valid 151 String s = Integer.toString (i); 152 if (m.hasField(i) && !validFields.contains (s)) { 153 if (sb.length() > 0) 154 sb.append (' '); 155 sb.append (s); 156 } 157 } 158 if (sb.length() > 0) 159 rc.fail(CMF.EXTRA_FIELD, Caller.info(), sb.toString()); 160 } 161 162 private void putCard (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 163 boolean hasCard = m.hasAny("2", "14", "35", "45"); 164 if (!mandatory && !hasCard) 165 return; // nothing to do, card is optional 166 167 try { 168 Card.Builder cb = Card.builder(); 169 if (track1Pattern != null) 170 cb.withTrack1Builder(Track1.builder().pattern(track1Pattern)); 171 if (track2Pattern != null) 172 cb.withTrack2Builder(Track2.builder().pattern(track2Pattern)); 173 cb.isomsg(m); 174 if (ignoreCardValidation) 175 cb.validator(null); 176 Card card = cb.build(); 177 ctx.put (ContextConstants.CARD.toString(), card); 178 if (card.hasTrack1()) 179 validFields.add("45"); 180 if (card.hasTrack2()) 181 validFields.add("35"); 182 if (card.getPan() != null && m.hasField(2)) 183 validFields.add("2"); 184 if (card.getExp() != null && m.hasField(14)) 185 validFields.add("14"); 186 } catch (InvalidCardException e) { 187 validFields.addAll(Arrays.asList("2", "14", "35", "45")); 188 if (hasCard) { 189 rc.fail(CMF.INVALID_CARD_NUMBER, Caller.info(), e.getMessage()); 190 } else if (mandatory) { 191 rc.fail(CMF.MISSING_FIELD, Caller.info(), e.getMessage()); 192 } 193 } 194 } 195 196 private void putPCode (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 197 if (m.hasField(3)) { 198 String s = m.getString(3); 199 200 validFields.add("3"); 201 if (PCODE_PATTERN.matcher(s).matches()) { 202 ctx.put(ContextConstants.PCODE.toString(), m.getString(3)); 203 } else 204 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid PCODE '%s'", s); 205 } else if (mandatory) { 206 rc.fail(CMF.MISSING_FIELD, Caller.info(), "PCODE"); 207 } 208 } 209 210 private void putTid (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 211 if (m.hasField(41)) { 212 String s = m.getString(41); 213 validFields.add("41"); 214 if (TID_PATTERN.matcher(s).matches()) { 215 ctx.put(ContextConstants.TID.toString(), m.getString(41)); 216 } else 217 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid TID '%s'", s); 218 } else if (mandatory) { 219 rc.fail(CMF.MISSING_FIELD, Caller.info(), "TID"); 220 } 221 } 222 223 private void putMid (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 224 if (m.hasField(42)) { 225 String s = m.getString(42); 226 validFields.add("42"); 227 if (MID_PATTERN.matcher(s).matches()) { 228 ctx.put(ContextConstants.MID.toString(), m.getString(42)); 229 } else 230 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid MID '%s'", s); 231 } else if (mandatory) { 232 rc.fail(CMF.MISSING_FIELD, Caller.info(), "MID"); 233 } 234 } 235 private void putTimestamp (Context ctx, ISOMsg m, String key, int fieldNumber, Pattern ptrn, boolean mandatory, Set<String> validFields, Result rc) { 236 if (m.hasField(fieldNumber)) { 237 String s = m.getString(fieldNumber); 238 validFields.add(Integer.toString(fieldNumber)); 239 if (ptrn.matcher(s).matches()) 240 ctx.put (key, ISODate.parseISODate(s)); 241 else 242 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", key, s); 243 } else if (mandatory) { 244 rc.fail(CMF.MISSING_FIELD, Caller.info(), TRANSMISSION_TIMESTAMP.toString()); 245 } 246 } 247 248 private void putCaptureDate (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 249 if (m.hasField(17)) { 250 String s = m.getString(17); 251 validFields.add("17"); 252 if (CAPTUREDATE_PATTERN.matcher(s).matches()) 253 ctx.put (CAPTURE_DATE.toString(), ISODate.parseISODate(s + "120000")); 254 else 255 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", CAPTURE_DATE, s); 256 } else if (mandatory) { 257 rc.fail(CMF.MISSING_FIELD, Caller.info(), CAPTURE_DATE.toString()); 258 } 259 } 260 261 private void putPDC (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 262 if (m.hasField(22)) { 263 byte[] b = m.getBytes(22); 264 validFields.add("22"); 265 if (b.length != 16) { 266 rc.fail( 267 CMF.INVALID_FIELD, 268 Caller.info(), "Invalid %s '%s'", 269 ContextConstants.POS_DATA_CODE.toString(), 270 ISOUtil.hexString(b) 271 ); 272 } 273 else { 274 ctx.put(ContextConstants.POS_DATA_CODE.toString(), PosDataCode.valueOf(m.getBytes(22))); 275 } 276 } else if (mandatory) { 277 rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.POS_DATA_CODE.toString()); 278 } 279 } 280 281 private void putAmount (Context ctx, ISOMsg m, boolean mandatory,Set<String> validFields, Result rc) { 282 Object o4 = m.getComponent(4); 283 Object o5 = m.getComponent(5); 284 ISOAmount a4 = null; 285 ISOAmount a5 = null; 286 if (o4 instanceof ISOAmount) { 287 a4 = (ISOAmount) o4; 288 validFields.add("4"); 289 } 290 if (o5 instanceof ISOAmount) { 291 a5 = (ISOAmount) o5; 292 validFields.add("5"); 293 } 294 if (a5 != null) { 295 ctx.put (AMOUNT.toString(), a5); 296 if (a4 != null) { 297 ctx.put (LOCAL_AMOUNT.toString(), a4); 298 } 299 } else if (a4 != null) { 300 ctx.put (AMOUNT.toString(), a4); 301 } 302 if (mandatory && (a4 == null && a5 == null)) 303 rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.AMOUNT.toString()); 304 } 305 306 private void putOriginalDataElements (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 307 String s = m.getString(56); 308 if (s != null) { 309 validFields.add("56"); 310 if (ORIGINAL_DATA_ELEMENTS_PATTERN.matcher(s).matches()) { 311 ctx.put (ORIGINAL_MTI.toString(), s.substring(0,4)); 312 ctx.put (ORIGINAL_STAN.toString(), s.substring(4,16)); 313 ctx.put (ORIGINAL_TIMESTAMP.toString(), ISODate.parseISODate (s.substring(16,30))); 314 } else { 315 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", ORIGINAL_DATA_ELEMENTS, s); 316 } 317 } else if (mandatory) { 318 rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.ORIGINAL_DATA_ELEMENTS.toString()); 319 } 320 } 321}