/** * Copyright (c) 2012, University of Konstanz, Distributed Systems Group * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the University of Konstanz nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.jscsi.parser; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SocketChannel; import java.security.DigestException; import java.util.AbstractList; import java.util.ArrayList; import java.util.Iterator; import org.jscsi.exception.InternetSCSIException; import org.jscsi.parser.datasegment.AbstractDataSegment; import org.jscsi.parser.datasegment.IDataSegmentIterator.IDataSegmentChunk; import org.jscsi.parser.digest.IDigest; import org.jscsi.parser.scsi.SCSICommandParser; import org.jscsi.utils.ByteBufferCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <h1>ProtocolDataUnit</h1> * <p> * This class encapsulates a Protocol Data Unit (PDU), which is defined in the iSCSI Standard (RFC 3720). * * @author Volker Wildi */ public final class ProtocolDataUnit { // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** The initial size of the Additional Header Segment. */ private static final int AHS_INITIAL_SIZE = 0; /** The Log interface. */ private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolDataUnit.class); // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** The Basic Header Segment of this PDU. */ private final BasicHeaderSegment basicHeaderSegment; /** The Additional Header Segment 1...n (optional) of this PDU. */ private final AbstractList<AdditionalHeaderSegment> additionalHeaderSegments; /** * The (optional) Data Segment contains PDU associated data. Its payload * effective length is provided in the BHS field - <code>DataSegmentLength</code>. The Data Segment is * also padded to * multiple of a <code>4</code> byte words. */ private ByteBuffer dataSegment; /** * Optional header and data digests protect the integrity of the header and * data, respectively. The digests, if present, are located, respectively, * after the header and PDU-specific data, and cover respectively the header * and the PDU data, each including the padding bytes, if any. * <p> * <b>The existence and type of digests are negotiated during the Login Phase. </b> * <p> * The separation of the header and data digests is useful in iSCSI routing applications, in which only * the header changes when a message is forwarded. In this case, only the header digest should be * recalculated. * <p> * Digests are not included in data or header length fields. * <p> * A zero-length Data Segment also implies a zero-length data-digest. */ /** Digest of the header of this PDU. */ private IDigest headerDigest; /** Digest of the data segment of this PDU. */ private IDigest dataDigest; // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Default constructor, creates a new, empty ProtcolDataUnit object. * * @param initHeaderDigest * The instance of the digest to use for the Basic Header Segment * protection. * @param initDataDigest * The instance of the digest to use for the Data Segment * protection. */ public ProtocolDataUnit(final IDigest initHeaderDigest, final IDigest initDataDigest) { basicHeaderSegment = new BasicHeaderSegment(); headerDigest = initHeaderDigest; additionalHeaderSegments = new ArrayList<AdditionalHeaderSegment>(AHS_INITIAL_SIZE); dataSegment = ByteBufferCache.allocate(0); dataDigest = initDataDigest; } // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Serialize all informations of this PDU object to its byte representation. * * @return The byte representation of this PDU. * @throws InternetSCSIException * If any violation of the iSCSI-Standard emerge. * @throws IOException * if an I/O error occurs. */ public final ByteBuffer serialize() throws InternetSCSIException, IOException { basicHeaderSegment.getParser().checkIntegrity(); final ByteBuffer pdu = ByteBufferCache.allocate(calcSize()); int offset = 0; offset += basicHeaderSegment.serialize(pdu, offset); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Serialized Basic Header Segment:\n" + toString()); } offset += serializeAdditionalHeaderSegments(pdu, offset); // write header digest // TODO: Move CRC calculation in BasicHeaderSegment.serialize? if (basicHeaderSegment.getParser().canHaveDigests()) { offset += serializeDigest(pdu, headerDigest); } // serialize data segment offset += serializeDataSegment(pdu, offset); // write data segment digest // TODO: Move CRC calculation in BasicHeaderSegment.serialize? if (basicHeaderSegment.getParser().canHaveDigests()) { offset += serializeDigest(pdu, dataDigest); } return (ByteBuffer)pdu.rewind(); } /** * Deserializes (parses) a given byte representation of a PDU to an PDU * object. * * @param pdu * The byte representation of an PDU to parse. * @return The number of bytes, which are serialized. * @throws InternetSCSIException * If any violation of the iSCSI-Standard emerge. * @throws IOException * if an I/O error occurs. * @throws DigestException * There is a mismatch of the digest. */ public final int deserialize(final ByteBuffer pdu) throws InternetSCSIException, IOException, DigestException { int offset = deserializeBasicHeaderSegment(pdu); offset += deserializeAdditionalHeaderSegments(pdu, offset); offset += deserializeDataSegment(pdu, offset); basicHeaderSegment.getParser().checkIntegrity(); return offset; } /** * Deserializes a given array starting from offset <code>0</code> and store * the informations in the BasicHeaderSegment object.. * * @param bhs * The array to read from. * @throws InternetSCSIException * If any violation of the iSCSI-Standard emerge. * @throws DigestException * There is a mismatch of the digest. */ private final int deserializeBasicHeaderSegment(final ByteBuffer bhs) throws InternetSCSIException, DigestException { int len = basicHeaderSegment.deserialize(this, bhs); // read header digest and validate if (basicHeaderSegment.getParser().canHaveDigests()) { len += deserializeDigest(bhs, bhs.position() - BasicHeaderSegment.BHS_FIXED_SIZE, BasicHeaderSegment.BHS_FIXED_SIZE, headerDigest); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Deserialized Basic Header Segment:\n" + toString()); } return len; } /** * Deserializes a array (starting from offset <code>0</code>) and store the * informations to the <code>AdditionalHeaderSegment</code> object. * * @param pdu * The array to read from. * @return The length of the read bytes. * @throws InternetSCSIException * If any violation of the iSCSI-Standard emerge. */ private final int deserializeAdditionalHeaderSegments(final ByteBuffer pdu) throws InternetSCSIException { return deserializeAdditionalHeaderSegments(pdu, 0); } /** * Deserializes a array (starting from the given offset) and store the * informations to the <code>AdditionalHeaderSegment</code> object. * * @param pdu * The <code>ByteBuffer</code> to read from. * @param offset * The offset to start from. * @return The length of the written bytes. * @throws InternetSCSIException * If any violation of the iSCSI-Standard emerge. */ private final int deserializeAdditionalHeaderSegments(final ByteBuffer pdu, final int offset) throws InternetSCSIException { // parsing Additional Header Segment int off = offset; int ahsLength = basicHeaderSegment.getTotalAHSLength(); while (ahsLength != 0) { final AdditionalHeaderSegment tmpAHS = new AdditionalHeaderSegment(); tmpAHS.deserialize(pdu, off); additionalHeaderSegments.add(tmpAHS); ahsLength -= tmpAHS.getLength(); off += tmpAHS.getSpecificField().position(); } return off - offset; } /** * Serialize all the contained additional header segments to the destination * array starting from the given offset. * * @param dst * The destination array to write in. * @param offset * The offset to start to write in <code>dst</code>. * @return The written length. * @throws InternetSCSIException * If any violation of the iSCSI-Standard emerge. */ private final int serializeAdditionalHeaderSegments(final ByteBuffer dst, final int offset) throws InternetSCSIException { int off = offset; for (AdditionalHeaderSegment ahs : additionalHeaderSegments) { off += ahs.serialize(dst, off); } return off - offset; } /** * Serializes the data segment (binary or key-value pairs) to a destination * array, staring from offset to write. * * @param dst * The array to write in. * @param offset * The start offset to start from in <code>dst</code>. * @return The written length. * @throws InternetSCSIException * If any violation of the iSCSI-Standard emerge. */ public final int serializeDataSegment(final ByteBuffer dst, final int offset) throws InternetSCSIException { dataSegment.rewind(); dst.position(offset); dst.put(dataSegment); return dataSegment.limit(); } /** * Deserializes a array (starting from the given offset) and store the * informations to the Data Segment. * * @param pdu * The array to read from. * @param offset * The offset to start from. * @return The length of the written bytes. * @throws IOException * if an I/O error occurs. * @throws InternetSCSIException * If any violation of the iSCSI-Standard emerge. * @throws DigestException * There is a mismatch of the digest. */ private final int deserializeDataSegment(final ByteBuffer pdu, final int offset) throws IOException, InternetSCSIException, DigestException { final int length = basicHeaderSegment.getDataSegmentLength(); if (dataSegment == null || dataSegment.limit() < length) { dataSegment = ByteBufferCache.allocate(dataSegment, AbstractDataSegment.getTotalLength(length)); } dataSegment.put(pdu); dataSegment.flip(); // read data segment digest and validate if (basicHeaderSegment.getParser().canHaveDigests()) { deserializeDigest(pdu, offset, length, dataDigest); } if (dataSegment == null) { return 0; } else { return dataSegment.limit(); } } // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Writes this <code>ProtocolDataUnit</code> object to the given <code>SocketChannel</code>. * * @param sChannel * <code>SocketChannel</code> to write to. * @return The number of bytes written, possibly zero. * @throws InternetSCSIException * if any violation of the iSCSI-Standard emerge. * @throws IOException * if an I/O error occurs. */ public final int write(final SocketChannel sChannel) throws InternetSCSIException, IOException { /* // print debug informations if (LOGGER.isTraceEnabled()) { LOGGER.trace(basicHeaderSegment.getParser().getShortInfo()); } final ByteBuffer src = serialize(); int length = 0; while (length < src.limit()) { length += sChannel.write(src); } return length; */ if (LOGGER.isTraceEnabled()) { LOGGER.trace(basicHeaderSegment.getParser().getShortInfo()); } basicHeaderSegment.getParser().checkIntegrity(); // basic header final ByteBuffer pduBegin = ByteBufferCache.allocate(calcSizeWithoutDataSegment()); try { int offset = 0; offset += basicHeaderSegment.serialize(pduBegin, offset); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Serialized Basic Header Segment:\n" + toString()); } offset += serializeAdditionalHeaderSegments(pduBegin, offset); // header digest if (basicHeaderSegment.getParser().canHaveDigests()) { offset += serializeDigest(pduBegin, headerDigest); } pduBegin.rewind(); // data segment dataSegment.rewind(); // data digest + padding final ByteBuffer pduEnd = ByteBufferCache.allocate(dataDigest.getSize() + AbstractDataSegment.getTotalLength(basicHeaderSegment.getDataSegmentLength()) - basicHeaderSegment.getDataSegmentLength()); try { if (basicHeaderSegment.getParser().canHaveDigests()) { serializeDigest(pduBegin, pduEnd, dataDigest); } int n = 0; while (n++ < pduEnd.limit()) { pduEnd.put((byte) 0); } pduEnd.rewind(); int length = 0; final ByteBuffer[] buffers = { pduBegin, dataSegment, pduEnd }; final long expected = pduBegin.limit() + dataSegment.limit() + pduEnd.limit(); while (length < expected) { length += sChannel.write(buffers); } return (int) expected; } finally { ByteBufferCache.release(pduEnd); } } finally { ByteBufferCache.release(pduBegin); } } /** * Reads from the given <code>SocketChannel</code> all the neccassary bytes * to fill this PDU. * * @param sChannel * <code>SocketChannel</code> to read from. * @return The number of bytes, possibly zero,or <code>-1</code> if the * channel has reached end-of-stream * @throws IOException * if an I/O error occurs. * @throws InternetSCSIException * if any violation of the iSCSI-Standard emerge. * @throws DigestException * if a mismatch of the digest exists. */ public final int read(final SocketChannel sChannel, final int emptyReadAttemptCount) throws InternetSCSIException, IOException, DigestException, ClosedChannelException { // read Basic Header Segment first to determine the total length of this // Protocol Data Unit. clear(); int len = 0; int emptyReadCount = 0; final ByteBuffer bhs = ByteBufferCache.allocate(BasicHeaderSegment.BHS_FIXED_SIZE); try{ while (len < BasicHeaderSegment.BHS_FIXED_SIZE) { int lens = sChannel.read(bhs); if (lens == -1) { // The Channel was closed at the Target (e.g. the Target does // not support Multiple Connections) throw new ClosedChannelException(); //return lens; } else if (lens == 0) { emptyReadCount++; if (emptyReadCount >= emptyReadAttemptCount) { return 0; } } else { emptyReadCount = Integer.MIN_VALUE; // Have read something: should be a PDU } len += lens; LOGGER.trace("Receiving through SocketChannel: " + len + " of maximal " + BasicHeaderSegment.BHS_FIXED_SIZE); } bhs.flip(); deserializeBasicHeaderSegment(bhs); } finally {ByteBufferCache.release(bhs);} // check for further reading if (getBasicHeaderSegment().getTotalAHSLength() > 0) { final ByteBuffer ahs = ByteBufferCache.allocate(basicHeaderSegment.getTotalAHSLength()); try { int ahsLength = 0; while (ahsLength < getBasicHeaderSegment().getTotalAHSLength()) { ahsLength += sChannel.read(ahs); } len += ahsLength; ahs.flip(); deserializeAdditionalHeaderSegments(ahs); } finally {ByteBufferCache.release(ahs);} } if (basicHeaderSegment.getDataSegmentLength() > 0) { dataSegment = ByteBufferCache.allocate(dataSegment, AbstractDataSegment.getTotalLength(basicHeaderSegment .getDataSegmentLength())); int dataSegmentLength = 0; while (dataSegmentLength < basicHeaderSegment.getDataSegmentLength()) { dataSegmentLength += sChannel.read(dataSegment); } len += dataSegmentLength; dataSegment.flip(); } // print debug informations if (LOGGER.isTraceEnabled()) { LOGGER.trace(basicHeaderSegment.getParser().getShortInfo()); } return len; } /** * Clears all stored content of this ProtocolDataUnit object. */ public final void clear() { basicHeaderSegment.clear(); headerDigest.reset(); additionalHeaderSegments.clear(); dataSegment.clear(); dataSegment.flip(); dataDigest.reset(); } /** * Release internal resources. */ // OODRIVE public final void release() { ByteBufferCache.release(dataSegment); dataSegment = null; final Iterator<AdditionalHeaderSegment> iterator = this.getAdditionalHeaderSegments(); while (iterator.hasNext()) { final AdditionalHeaderSegment ahs = iterator.next(); ByteBufferCache.release(ahs.getSpecificField()); } // release cdb buffer if scsi command if (getBasicHeaderSegment().getParser() instanceof SCSICommandParser) { final SCSICommandParser parser = (SCSICommandParser) getBasicHeaderSegment().getParser(); ByteBufferCache.release(parser.getCDB()); } } // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Returns an iterator to all contained Additional Header Segment in this * PDU. * * @return The iterator to the contained Additional Header Segment. * @see AdditionalHeaderSegment */ public final Iterator<AdditionalHeaderSegment> getAdditionalHeaderSegments() { return additionalHeaderSegments.iterator(); } /** * Returns the Basic Header Segment contained in this PDU. * * @return The Basic Header Segment. * @see BasicHeaderSegment */ public final BasicHeaderSegment getBasicHeaderSegment() { return basicHeaderSegment; } /** * Gets the data segment in this PDU. * * @return The data segment of this <code>ProtocolDataUnit</code> object. */ public final ByteBuffer getDataSegment() { return dataSegment; } public final void setDataSegment(final ByteBuffer dataSegment) { dataSegment.clear(); ByteBufferCache.release(this.dataSegment); this.dataSegment = dataSegment; basicHeaderSegment.setDataSegmentLength(dataSegment.capacity()); } /** * Sets a new data segment in this PDU. * * @param chunk * The new data segment of this <code>ProtocolDataUnit</code> object. */ public final void setDataSegment(final IDataSegmentChunk chunk) { if (chunk == null) { throw new NullPointerException(); } dataSegment = ByteBufferCache.allocate(dataSegment, chunk.getTotalLength()); dataSegment.put(chunk.getData()); basicHeaderSegment.setDataSegmentLength(chunk.getLength()); } /** * Returns the instance of the used digest algorithm for the header. * * @return The instance of the header digest. */ public final IDigest getHeaderDigest() { return headerDigest; } /** * Sets the digest of the header to use for data integrity. * * @param newHeaderDigest * An instance of the new header digest. */ public final void setHeaderDigest(final IDigest newHeaderDigest) { headerDigest = newHeaderDigest; } /** * Returns the instance of the used digest algorithm for the data segment. * * @return The instance of the data digest. */ public final IDigest getDataDigest() { return dataDigest; } /** * Sets the digest of the data segment to use for data integrity. * * @param newDataDigest * An instance of the new data segment digest. */ public final void setDataDigest(final IDigest newDataDigest) { dataDigest = newDataDigest; } // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** {@inheritDoc} */ @Override public final String toString() { final StringBuilder sb = new StringBuilder(Constants.LOG_INITIAL_SIZE); sb.append(basicHeaderSegment.toString()); for (AdditionalHeaderSegment ahs : additionalHeaderSegments) { sb.append(ahs.toString()); } return sb.toString(); } // -------------------------------------------------------------------------- /** {@inheritDoc} */ @Override public final boolean equals(Object o) { if (o instanceof ProtocolDataUnit == false) return false; ProtocolDataUnit oPdu = (ProtocolDataUnit)o; Iterator<AdditionalHeaderSegment> ahs1 = oPdu.getAdditionalHeaderSegments(); Iterator<AdditionalHeaderSegment> ahs2 = this.getAdditionalHeaderSegments(); while (ahs1.hasNext()) { if (!ahs1.equals(ahs2)) return false; ahs1.next(); ahs2.next(); } if (oPdu.getBasicHeaderSegment().equals(this.getBasicHeaderSegment()) && oPdu.getDataDigest().equals(this.getDataDigest()) && oPdu.getHeaderDigest().equals(this.getHeaderDigest()) && oPdu.getDataSegment().equals(this.getDataSegment())) return true; return false; } // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Calculates the needed size (in bytes) of serializing this object. * * @return The needed size to store this object. */ private final int calcSize() { int size = BasicHeaderSegment.BHS_FIXED_SIZE; size += basicHeaderSegment.getTotalAHSLength() * AdditionalHeaderSegment.AHS_FACTOR; // plus the sizes of the used digests size += headerDigest.getSize(); size += dataDigest.getSize(); size += AbstractDataSegment.getTotalLength(basicHeaderSegment.getDataSegmentLength()); return size; } private final int calcSizeWithoutDataSegment() { int size = BasicHeaderSegment.BHS_FIXED_SIZE; size += basicHeaderSegment.getTotalAHSLength() * AdditionalHeaderSegment.AHS_FACTOR; // plus the sizes of the used digests size += headerDigest.getSize(); return size; } private final int serializeDigest(final ByteBuffer pdu, final IDigest digest) { final int size = digest.getSize(); if (size > 0) { digest.reset(); pdu.mark(); digest.update(pdu, 0, BasicHeaderSegment.BHS_FIXED_SIZE); pdu.putInt((int)digest.getValue()); pdu.reset(); } return size; } // add digest in another buffer private final int serializeDigest(final ByteBuffer pdu, final ByteBuffer dst, final IDigest digest) { final int size = digest.getSize(); if (size > 0) { digest.reset(); pdu.mark(); digest.update(pdu, 0, BasicHeaderSegment.BHS_FIXED_SIZE); dst.putInt((int) digest.getValue()); dst.rewind(); pdu.reset(); } return size; } private final int deserializeDigest(final ByteBuffer pdu, final int offset, final int length, final IDigest digest) throws DigestException { pdu.mark(); digest.update(pdu, offset, length); digest.validate(); pdu.reset(); return digest.getSize(); } // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- }