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.iso.channel; 020 021import org.jpos.core.Configuration; 022import org.jpos.core.ConfigurationException; 023import org.jpos.iso.*; 024 025import java.io.IOException; 026import java.net.ServerSocket; 027 028/** 029 * {@link ISOChannel} implementation for the jPOS-CMF framing. 030 * This channel uses a 3-byte big-endian length prefix (24-bit unsigned) and no 031 * additional message header. 032 * 033 * <h3>Keep-alive handling</h3> 034 * A zero-length frame is treated as a keep-alive. When a keep-alive is received and either 035 * {@code replyKeepAlive} is enabled or {@link #isExpectKeepAlive()} returns {@code true}, the channel 036 * echoes the keep-alive back to the peer. 037 * 038 * <h3>Configuration</h3> 039 * <ul> 040 * <li>{@code reply-keepalive} (boolean, default {@code true}): Whether to echo a received 041 * keep-alive (zero-length frame) back to the peer.</li> 042 * <li>{@code max-keepalives-in-a-row} (integer, default {@code 0}): Maximum number of consecutive 043 * keep-alives allowed while waiting for a non-zero length frame. A value of {@code 0} disables 044 * the limit (unlimited), preserving legacy behavior.</li> 045 * </ul> 046 * 047 * @author apr@jpos.org 048 * @since 3.0.1 049 * 050 * @see ISOMsg 051 * @see ISOException 052 * @see ISOChannel 053 * @see ISOPackager 054 */ 055public class CMFChannel extends BaseChannel { 056 /** 057 * Number of bytes used to encode the length prefix. 058 */ 059 private static final int LENGTH_BYTES = 3; 060 061 /** 062 * Maximum payload length representable by a 24-bit unsigned length field (0xFFFFFF). 063 */ 064 private static final int MAX_PACKET_LENGTH = 0x00FF_FFFF; 065 066 private boolean replyKeepAlive = true; 067 068 /** 069 * Maximum number of consecutive keep-alives allowed while waiting for a non-zero length frame. 070 * A value of {@code 0} means unlimited. 071 */ 072 private int maxKeepAlivesInARow = 0; 073 074 /** 075 * Creates an unconfigured {@code CMFChannel}. 076 * 077 * The caller is expected to set the packager/connection parameters through the usual 078 * {@link BaseChannel} configuration and/or setters before use. 079 */ 080 public CMFChannel () { 081 super(); 082 } 083 084 /** 085 * Constructs a client {@code CMFChannel}. 086 * 087 * @param host server TCP address or hostname. 088 * @param port server TCP port number. 089 * @param p the {@link ISOPackager} used to pack/unpack {@link ISOMsg}s. 090 * 091 * @see ISOPackager 092 */ 093 public CMFChannel (String host, int port, ISOPackager p) { 094 super(host, port, p); 095 } 096 097 /** 098 * Constructs a server {@code CMFChannel}. 099 * 100 * This constructor creates a {@link ServerSocket} internally (as per {@link BaseChannel}) 101 * and waits for inbound connections when {@link #accept()} is invoked. 102 * 103 * @param p the {@link ISOPackager} used to pack/unpack {@link ISOMsg}s. 104 * @throws IOException if the underlying server socket cannot be created. 105 * 106 * @see ISOPackager 107 */ 108 public CMFChannel (ISOPackager p) throws IOException { 109 super(p); 110 } 111 112 /** 113 * Constructs a server {@code CMFChannel} associated with an existing {@link ServerSocket}. 114 * 115 * @param p the {@link ISOPackager} used to pack/unpack {@link ISOMsg}s. 116 * @param serverSocket the server socket used to accept a connection. 117 * @throws IOException if an I/O error occurs while initializing the channel. 118 * 119 * @see ISOPackager 120 */ 121 public CMFChannel (ISOPackager p, ServerSocket serverSocket) throws IOException { 122 super(p, serverSocket); 123 } 124 125 /** 126 * Sends the CMF message length prefix. 127 * 128 * The CMF framing uses a 3-byte big-endian length prefix (24-bit unsigned). 129 * 130 * @param len the packed message length in bytes. 131 * @throws IOException if an I/O error occurs writing to the socket output stream or if 132 * {@code len} is outside the valid range ({@code 0..0xFFFFFF}). 133 */ 134 @Override 135 protected void sendMessageLength(int len) throws IOException { 136 if (len < 0 || len > MAX_PACKET_LENGTH) { 137 throw new IOException( 138 "Invalid CMF packet length " + len + " (valid range: 0.." + MAX_PACKET_LENGTH + ")" 139 ); 140 } 141 serverOut.write((len >>> 16) & 0xFF); 142 serverOut.write((len >>> 8) & 0xFF); 143 serverOut.write(len & 0xFF); 144 } 145 146 /** 147 * Reads the CMF message length prefix. 148 * 149 * Reads a 3-byte big-endian length value. A zero-length frame is treated as a keep-alive. 150 * When a keep-alive is received and either {@code replyKeepAlive} is enabled or 151 * {@link #isExpectKeepAlive()} is {@code true}, the keep-alive is echoed back to the peer. 152 * The method continues reading until a non-zero length is obtained. 153 * 154 * <p>If {@code max-keepalives-in-a-row} is configured to a value greater than {@code 0}, 155 * the method will fail after receiving more than that number of consecutive keep-alives 156 * without a subsequent non-zero length frame.</p> 157 * 158 * @return the packed message length in bytes (always {@code > 0}). 159 * @throws IOException if an I/O error occurs reading from the socket input stream, or if too 160 * many consecutive keep-alives are received (when limited). 161 * @throws ISOException if the channel detects an ISO-level framing/processing error. 162 */ 163 @Override 164 protected int getMessageLength() throws IOException, ISOException { 165 int keepAlives = 0; 166 167 for (;;) { 168 int b0 = serverIn.readUnsignedByte(); 169 int b1 = serverIn.readUnsignedByte(); 170 int b2 = serverIn.readUnsignedByte(); 171 172 int len = (b0 << 16) | (b1 << 8) | b2; 173 174 if (len != 0) { 175 // Defensive check; with 3 bytes, len cannot exceed MAX_PACKET_LENGTH, 176 // but this documents intent and protects future refactors. 177 if (len > MAX_PACKET_LENGTH) { 178 throw new ISOException( 179 "Invalid CMF packet length " + len + " (max " + MAX_PACKET_LENGTH + ")" 180 ); 181 } 182 return len; 183 } 184 185 // Keep-alive (0 length). 186 if (maxKeepAlivesInARow > 0 && ++keepAlives > maxKeepAlivesInARow) { 187 throw new IOException("Too many consecutive keep-alives (" + keepAlives + ")"); 188 } 189 190 if (replyKeepAlive || isExpectKeepAlive()) { 191 serverOutLock.lock(); 192 try { 193 // Echo the same 3-byte zero-length frame. 194 serverOut.write(0); 195 serverOut.write(0); 196 serverOut.write(0); 197 serverOut.flush(); 198 } finally { 199 serverOutLock.unlock(); 200 } 201 } 202 } 203 } 204 205 /** 206 * Returns the channel header length. 207 * 208 * CMF framing does not include a separate message header beyond the 3-byte length prefix. 209 * 210 * @return {@code 0}. 211 */ 212 @Override 213 protected int getHeaderLength() { 214 return 0; 215 } 216 217 /** 218 * Sends the message header (no-op). 219 * 220 * CMF framing does not define a message header; only the length prefix is used. 221 * 222 * @param m the message being sent. 223 * @param len the packed message length in bytes. 224 */ 225 @Override 226 protected void sendMessageHeader(ISOMsg m, int len) { 227 // CMF channel does not use a header. 228 } 229 230 /** 231 * Applies configuration to this channel. 232 * 233 * In addition to the base {@link BaseChannel} configuration, this method reads: 234 * <ul> 235 * <li>{@code reply-keepalive} (boolean, default {@code true}).</li> 236 * <li>{@code max-keepalives-in-a-row} (integer, default {@code 0}).</li> 237 * </ul> 238 * 239 * @param cfg the configuration source. 240 * @throws ConfigurationException if configuration cannot be applied. 241 */ 242 @Override 243 public void setConfiguration (Configuration cfg) throws ConfigurationException { 244 super.setConfiguration(cfg); 245 replyKeepAlive = cfg.getBoolean("reply-keepalive", true); 246 maxKeepAlivesInARow = cfg.getInt("max-keepalives-in-a-row", 0); 247 } 248 249 /** 250 * Returns the maximum packet length supported by this channel. 251 * 252 * CMF framing uses a 3-byte unsigned length prefix (24-bit). Therefore, the maximum 253 * representable non-zero payload length is {@code 0xFFFFFF} (16,777,215 bytes). 254 * 255 * @return the maximum packet length, in bytes. 256 */ 257 @Override 258 public int getMaxPacketLength() { 259 return MAX_PACKET_LENGTH; 260 } 261}