com.netflix.msl.msg.MessageOutputStream Maven / Gradle / Ivy
/**
* Copyright (c) 2012-2017 Netflix, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.msl.msg;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.netflix.msl.MslConstants.CompressionAlgorithm;
import com.netflix.msl.MslCryptoException;
import com.netflix.msl.MslException;
import com.netflix.msl.MslInternalException;
import com.netflix.msl.crypto.ICryptoContext;
import com.netflix.msl.io.MslEncoderException;
import com.netflix.msl.io.MslEncoderFactory;
import com.netflix.msl.io.MslEncoderFormat;
import com.netflix.msl.util.MslContext;
/**
* A MSL message consists of a single MSL header followed by one or more
* payload chunks carrying application data. Each payload chunk is individually
* packaged but sequentially ordered. The end of the message is indicated by a
* payload with no data.
*
* No payload chunks may be included in an error message.
*
* Data is buffered until {@link #flush()} or {@link #close()} is called.
* At that point a new payload chunk is created and written out. Closing a
* {@code MessageOutputStream} does not close the destination output stream in
* case additional MSL messages will be written.
*
* A copy of the payload chunks is kept in-memory and can be retrieved by a
* a call to {@code getPayloads()} until {@code stopCaching()} is called. This
* is used to facilitate automatic re-sending of messages.
*
* @author Wesley Miaw
*/
public class MessageOutputStream extends OutputStream {
/**
* Construct a new error message output stream. The header is output
* immediately by calling {@code #flush()} on the destination output
* stream.
*
* @param ctx the MSL context.
* @param destination MSL output stream.
* @param header error header.
* @param format the MSL encoder format.
* @throws IOException if there is an error writing the header.
*/
public MessageOutputStream(final MslContext ctx, final OutputStream destination, final ErrorHeader header, final MslEncoderFormat format) throws IOException {
// Encode the header.
final byte[] encoding;
try {
final MslEncoderFactory encoder = ctx.getMslEncoderFactory();
encoding = header.toMslEncoding(encoder, format);
} catch (final MslEncoderException e) {
throw new IOException("Error encoding the error header.", e);
}
this.ctx = ctx;
this.destination = destination;
this.encoderFormat = format;
this.capabilities = ctx.getMessageCapabilities();
this.header = header;
this.compressionAlgo = null;
this.cryptoContext = null;
this.destination.write(encoding);
this.destination.flush();
}
/**
* Construct a new message output stream. The header is output
* immediately by calling {@code #flush()} on the destination output
* stream.
*
* The most preferred compression algorithm and encoder format supported
* by the message header will be used. If this is a response, the message
* header capabilities will already consist of the intersection of the
* local and remote entity capabilities.
*
* @param ctx the MSL context.
* @param destination MSL output stream.
* @param header message header.
* @param cryptoContext payload data crypto context.
* @throws IOException if there is an error writing the header.
*/
public MessageOutputStream(final MslContext ctx, final OutputStream destination, final MessageHeader header, final ICryptoContext cryptoContext) throws IOException {
final MslEncoderFactory encoder = ctx.getMslEncoderFactory();
// Identify the compression algorithm and encoder format.
final MessageCapabilities capabilities = header.getMessageCapabilities();
final CompressionAlgorithm compressionAlgo;
final MslEncoderFormat encoderFormat;
if (capabilities != null) {
final Set compressionAlgos = capabilities.getCompressionAlgorithms();
compressionAlgo = CompressionAlgorithm.getPreferredAlgorithm(compressionAlgos);
final Set encoderFormats = capabilities.getEncoderFormats();
encoderFormat = encoder.getPreferredFormat(encoderFormats);
} else {
compressionAlgo = null;
encoderFormat = encoder.getPreferredFormat(null);
}
// Encode the header.
final byte[] encoding;
try {
encoding = header.toMslEncoding(encoder, encoderFormat);
} catch (final MslEncoderException e) {
throw new IOException("Error encoding the message header.", e);
}
this.ctx = ctx;
this.destination = destination;
this.encoderFormat = encoderFormat;
this.capabilities = capabilities;
this.header = header;
this.compressionAlgo = compressionAlgo;
this.cryptoContext = cryptoContext;
this.destination.write(encoding);
this.destination.flush();
}
/* (non-Javadoc)
* @see java.lang.Object#finalize()
*/
@Override
protected void finalize() throws Throwable {
close();
super.finalize();
}
/**
* Set the payload chunk compression algorithm that will be used for all
* future payload chunks. This function will flush any buffered data iff
* the compression algorithm is being changed.
*
* @param compressionAlgo payload chunk compression algorithm. Null for no
* compression.
* @return true if the compression algorithm is supported by the message,
* false if it is not.
* @throws IOException if buffered data could not be flushed. The
* compression algorithm will be unchanged.
* @throws MslInternalException if writing an error message.
* @see #flush()
*/
public boolean setCompressionAlgorithm(final CompressionAlgorithm compressionAlgo) throws IOException {
// Make sure this is not an error message,
final MessageHeader messageHeader = getMessageHeader();
if (messageHeader == null)
throw new MslInternalException("Cannot write payload data for an error message.");
// Make sure the message is capable of using the compression algorithm.
if (compressionAlgo != null) {
if (capabilities == null)
return false;
final Set compressionAlgos = capabilities.getCompressionAlgorithms();
if (!compressionAlgos.contains(compressionAlgo))
return false;
}
if (this.compressionAlgo != compressionAlgo)
flush();
this.compressionAlgo = compressionAlgo;
return true;
}
/**
* @return the message header. Will be null for error messages.
*/
public MessageHeader getMessageHeader() {
if (header instanceof MessageHeader)
return (MessageHeader)header;
return null;
}
/**
* @return the error header. Will be null except for error messages.
*/
public ErrorHeader getErrorHeader() {
if (header instanceof ErrorHeader)
return (ErrorHeader)header;
return null;
}
/**
* Returns the payloads sent so far. Once payload caching is turned off
* this list will always be empty.
*
* @return an immutable ordered list of the payloads sent so far.
*/
List getPayloads() {
return Collections.unmodifiableList(payloads);
}
/**
* Turns off caching of any message data (e.g. payloads).
*/
void stopCaching() {
caching = false;
payloads.clear();
}
/**
* By default the destination output stream is not closed when this message
* output stream is closed. If it should be closed then this method can be
* used to dictate the desired behavior.
*
* @param close true if the destination output stream should be closed,
* false if it should not.
*/
public void closeDestination(final boolean close) {
this.closeDestination = close;
}
/* (non-Javadoc)
* @see java.io.OutputStream#close()
*/
@Override
public void close() throws IOException {
if (closed) return;
// Send a final payload that can be used to identify the end of data.
// This is done by setting closed equal to true while the current
// payload not null.
closed = true;
flush();
currentPayload = null;
// Only close the destination if instructed to do so because we might
// want to reuse the connection.
if (closeDestination)
destination.close();
}
/**
* Flush any buffered data out to the destination. This creates a payload
* chunk. If there is no buffered data or this is an error message this
* function does nothing.
*
* @throws IOException if buffered data could not be flushed.
* @throws MslInternalException if writing an error message.
* @see java.io.OutputStream#flush()
*/
@Override
public void flush() throws IOException {
// If the current payload is null, we are already closed.
if (currentPayload == null) return;
// If we are not closed, and there is no data then we have nothing to
// send.
if (!closed && currentPayload.size() == 0) return;
// This is a no-op for error messages and handshake messages.
final MessageHeader messageHeader = getMessageHeader();
if (messageHeader == null || messageHeader.isHandshake()) return;
// Otherwise we are closed and need to send any buffered data as the
// last payload. If there is no buffered data, we still need to send a
// payload with the end of message flag set.
try {
final byte[] data = currentPayload.toByteArray();
final PayloadChunk chunk = new PayloadChunk(ctx, payloadSequenceNumber, messageHeader.getMessageId(), closed, compressionAlgo, data, this.cryptoContext);
if (caching) payloads.add(chunk);
final MslEncoderFactory encoder = ctx.getMslEncoderFactory();
final byte[] encoding = chunk.toMslEncoding(encoder, encoderFormat);
destination.write(encoding);
destination.flush();
++payloadSequenceNumber;
// If we are closed, get rid of the current payload. This prevents
// us from sending any more payloads. Otherwise reset it for reuse.
if (closed)
currentPayload = null;
else
currentPayload.reset();
} catch (final MslEncoderException e) {
throw new IOException("Error encoding payload chunk [sequence number " + payloadSequenceNumber + "].", e);
} catch (final MslCryptoException e) {
throw new IOException("Error encrypting payload chunk [sequence number " + payloadSequenceNumber + "].", e);
} catch (final MslException e) {
throw new IOException("Error compressing payload chunk [sequence number " + payloadSequenceNumber + "].", e);
}
}
/* (non-Javadoc)
* @see java.io.OutputStream#write(byte[], int, int)
*/
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
// Fail if closed.
if (closed)
throw new IOException("Message output stream already closed.");
// Make sure this is not an error message or handshake message.
final MessageHeader messageHeader = getMessageHeader();
if (messageHeader == null)
throw new MslInternalException("Cannot write payload data for an error message.");
if (messageHeader.isHandshake())
throw new MslInternalException("Cannot write payload data for a handshake message.");
// Append data.
currentPayload.write(b, off, len);
}
/* (non-Javadoc)
* @see java.io.OutputStream#write(byte[])
*/
@Override
public void write(final byte[] b) throws IOException {
write(b, 0, b.length);
}
/* (non-Javadoc)
* @see java.io.OutputStream#write(int)
*/
@Override
public void write(final int b) throws IOException {
final byte[] ba = new byte[1];
ba[0] = (byte)(b & 0xFF);
write(ba);
}
/** MSL context. */
private final MslContext ctx;
/** Destination output stream. */
private final OutputStream destination;
/** MSL encoder format. */
private final MslEncoderFormat encoderFormat;
/** Message output stream capabilities. */
private final MessageCapabilities capabilities;
/** Header. */
private final Header header;
/** Payload crypto context. */
private final ICryptoContext cryptoContext;
/** Paload chunk compression algorithm. */
private CompressionAlgorithm compressionAlgo;
/** Current payload sequence number. */
private long payloadSequenceNumber = 1;
/** Current payload chunk data. */
private ByteArrayOutputStream currentPayload = new ByteArrayOutputStream();
/** Stream is closed. */
private boolean closed = false;
/** True if the destination output stream should be closed. */
private boolean closeDestination = false;
/** True if caching data. */
private boolean caching = true;
/** Ordered list of sent payloads. */
private final List payloads = new ArrayList();
}