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}