com.unboundid.asn1.ASN1StreamReader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of unboundid-ldapsdk Show documentation
Show all versions of unboundid-ldapsdk Show documentation
The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use
Java API for communicating with LDAP directory servers and performing
related tasks like reading and writing LDIF, encoding and decoding data
using base64 and ASN.1 BER, and performing secure communication. This
package contains the Standard Edition of the LDAP SDK, which is a
complete, general-purpose library for communicating with LDAPv3 directory
servers.
The newest version!
/*
* Copyright 2009-2024 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright 2009-2024 Ping Identity Corporation
*
* 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.
*/
/*
* Copyright (C) 2009-2024 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.asn1;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.InputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.SocketTimeoutException;
import java.util.Date;
import java.util.logging.Level;
import javax.security.sasl.SaslClient;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.util.Debug;
import com.unboundid.util.Mutable;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import static com.unboundid.asn1.ASN1Messages.*;
/**
* This class provides a mechanism for ASN.1 elements (including sequences and
* sets) from an input stream in a manner that allows the data to be decoded on
* the fly without constructing {@link ASN1Element} objects if they are not
* needed. If any method in this class throws an {@code IOException}, then the
* caller must close this reader and must not attempt to use it any more.
* {@code ASN1StreamReader} instances are not threadsafe and must not be
* accessed concurrently by multiple threads.
*/
@Mutable()
@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
public final class ASN1StreamReader
implements Closeable
{
/**
* The default maximum element size that will be used for the constructor that
* does not specify a size.
*/
private static final int DEFAULT_MAX_ELEMENT_SIZE_BYTES =
new LDAPConnectionOptions().getMaxMessageSize();
// Indicates whether socket timeout exceptions should be ignored for the
// initial read of an element.
private boolean ignoreInitialSocketTimeout;
// Indicates whether socket timeout exceptions should be ignored for
// subsequent reads of an element.
private boolean ignoreSubsequentSocketTimeout;
// The input stream that will be used for reading data after it has been
// unwrapped by SASL processing.
@Nullable private volatile ByteArrayInputStream saslInputStream;
// The input stream from which data will be read.
@NotNull private final InputStream inputStream;
// The maximum element size that will be allowed.
private final int maxElementSize;
// The total number of bytes read from the underlying input stream.
private long totalBytesRead;
// The SASL client that will be used to unwrap any data read over this
// stream reader.
@Nullable private volatile SaslClient saslClient;
/**
* Creates a new ASN.1 stream reader that will read data from the provided
* input stream. It will use a maximum element size equal to the default
* value returned by the {@link LDAPConnectionOptions#getMaxMessageSize()}
* method.
*
* @param inputStream The input stream from which data should be read. If
* the provided input stream does not support the use of
* the {@code mark} and {@code reset} methods, then it
* will be wrapped with a {@code BufferedInputStream}.
*/
public ASN1StreamReader(@NotNull final InputStream inputStream)
{
this(inputStream, DEFAULT_MAX_ELEMENT_SIZE_BYTES);
}
/**
* Creates a new ASN.1 stream reader that will read data from the provided
* input stream. It will use a maximum element size of
* {@code Integer.MAX_VALUE}.
*
* @param inputStream The input stream from which data should be read.
* If the provided input stream does not support the
* use of the {@code mark} and {@code reset} methods,
* then it will be wrapped with a
* {@code BufferedInputStream}.
* @param maxElementSize The maximum size in bytes of an ASN.1 element that
* may be read. A value less than or equal to zero
* will be interpreted as {@code Integer.MAX_VALUE}.
*/
public ASN1StreamReader(@NotNull final InputStream inputStream,
final int maxElementSize)
{
if (inputStream.markSupported())
{
this.inputStream = inputStream;
}
else
{
this.inputStream = new BufferedInputStream(inputStream);
}
if (maxElementSize > 0)
{
this.maxElementSize = maxElementSize;
}
else
{
this.maxElementSize = Integer.MAX_VALUE;
}
totalBytesRead = 0L;
ignoreInitialSocketTimeout = false;
ignoreSubsequentSocketTimeout = false;
saslClient = null;
saslInputStream = null;
}
/**
* Closes this ASN.1 stream reader and the underlying input stream. This
* reader must not be used after it has been closed.
*
* @throws IOException If a problem occurs while closing the underlying
* input stream.
*/
@Override()
public void close()
throws IOException
{
inputStream.close();
}
/**
* Retrieves the total number of bytes read so far from the underlying input
* stream.
*
* @return The total number of bytes read so far from the underlying input
* stream.
*/
long getTotalBytesRead()
{
return totalBytesRead;
}
/**
* Indicates whether to ignore {@code java.net.SocketTimeoutException}
* exceptions that may be caught during processing.
*
* @return {@code true} if {@code SocketTimeoutException} exceptions should
* be ignored, or {@code false} if they should not be ignored and
* should be propagated to the caller.
*
* @deprecated Use the {@link #ignoreInitialSocketTimeoutException()} and
* {@link #ignoreSubsequentSocketTimeoutException()} methods
* instead.
*/
@Deprecated()
public boolean ignoreSocketTimeoutException()
{
return ignoreInitialSocketTimeout;
}
/**
* Indicates whether to ignore {@code java.net.SocketTimeoutException}
* exceptions that may be caught while trying to read the first byte of an
* element.
*
* @return {@code true} if {@code SocketTimeoutException} exceptions should
* be ignored while trying to read the first byte of an element, or
* {@code false} if they should not be ignored and should be
* propagated to the caller.
*/
public boolean ignoreInitialSocketTimeoutException()
{
return ignoreInitialSocketTimeout;
}
/**
* Indicates whether to ignore {@code java.net.SocketTimeoutException}
* exceptions that may be caught while trying to read subsequent bytes of an
* element (after one or more bytes have already been read for that element).
*
* @return {@code true} if {@code SocketTimeoutException} exceptions should
* be ignored while trying to read subsequent bytes of an element, or
* {@code false} if they should not be ignored and should be
* propagated to the caller.
*/
public boolean ignoreSubsequentSocketTimeoutException()
{
return ignoreSubsequentSocketTimeout;
}
/**
* Indicates whether to ignore {@code java.net.SocketTimeoutException}
* exceptions that may be caught during processing.
*
* @param ignoreSocketTimeout Indicates whether to ignore
* {@code SocketTimeoutException} exceptions that
* may be caught during processing.
*
* @deprecated Use the {@link #setIgnoreSocketTimeout(boolean,boolean)}
* method instead.
*/
@Deprecated()
public void setIgnoreSocketTimeout(final boolean ignoreSocketTimeout)
{
ignoreInitialSocketTimeout = ignoreSocketTimeout;
ignoreSubsequentSocketTimeout = ignoreSocketTimeout;
}
/**
* Indicates whether to ignore {@code java.net.SocketTimeoutException}
* exceptions that may be caught during processing.
*
* @param ignoreInitialSocketTimeout Indicates whether to ignore
* {@code SocketTimeoutException}
* exceptions that may be caught while
* trying to read the first byte of an
* element.
* @param ignoreSubsequentSocketTimeout Indicates whether to ignore
* {@code SocketTimeoutException}
* exceptions that may be caught while
* reading beyond the first byte of an
* element.
*/
public void setIgnoreSocketTimeout(final boolean ignoreInitialSocketTimeout,
final boolean ignoreSubsequentSocketTimeout)
{
this.ignoreInitialSocketTimeout = ignoreInitialSocketTimeout;
this.ignoreSubsequentSocketTimeout = ignoreSubsequentSocketTimeout;
}
/**
* Peeks at the next byte to be read from the input stream without actually
* consuming it.
*
* @return An integer value encapsulating the BER type of the next element in
* the input stream, or -1 if the end of the input stream has been
* reached and there is no data to be read. If a value of -1 is
* returned, then the input stream will not have been closed since
* this method is not intended to have any impact on the underlying
* input stream.
*
* @throws IOException If a problem occurs while reading from the input
* stream.
*/
public int peek()
throws IOException
{
final InputStream is;
if (saslClient == null)
{
is = inputStream;
}
else
{
if ((saslInputStream == null) || (saslInputStream.available() <= 0))
{
readAndDecodeSASLData(-1);
}
is = saslInputStream;
}
is.mark(1);
final int byteRead = read(true);
is.reset();
return byteRead;
}
/**
* Reads the BER type of the next element from the input stream. This may not
* be called if a previous element has been started but not yet completed.
*
* @return An integer value encapsulating the BER type of the next element in
* the input stream, or -1 if the end of the input stream has been
* reached and there is no data to be read. If a value of -1 is
* returned, then the input stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream.
*/
private int readType()
throws IOException
{
final int typeInt = read(true);
if (typeInt < 0)
{
close();
}
else
{
totalBytesRead++;
}
return typeInt;
}
/**
* Reads the length of the next element from the input stream. This may only
* be called after reading the BER type.
*
* @return The length of the next element from the input stream.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the stream has been reached, or
* if the decoded length is greater than the maximum
* allowed length.
*/
private int readLength()
throws IOException
{
int length = read(false);
if (length < 0)
{
throw new IOException(ERR_READ_END_BEFORE_FIRST_LENGTH.get());
}
totalBytesRead++;
if (length > 127)
{
final int numLengthBytes = length & 0x7F;
length = 0;
if ((numLengthBytes < 1) || (numLengthBytes > 4))
{
throw new IOException(ERR_READ_LENGTH_TOO_LONG.get(numLengthBytes));
}
for (int i=0; i < numLengthBytes; i++)
{
final int lengthInt = read(false);
if (lengthInt < 0)
{
throw new IOException(ERR_READ_END_BEFORE_LENGTH_END.get());
}
length <<= 8;
length |= (lengthInt & 0xFF);
}
totalBytesRead += numLengthBytes;
}
if ((length < 0) || ((maxElementSize > 0) && (length > maxElementSize)))
{
throw new IOException(ERR_READ_LENGTH_EXCEEDS_MAX.get(length,
maxElementSize));
}
return length;
}
/**
* Skips over the specified number of bytes.
*
* @param numBytes The number of bytes to skip.
*
* @throws IOException If a problem occurs while reading from the input
* stream, or if the end of the stream is reached before
* having skipped the specified number of bytes.
*/
private void skip(final int numBytes)
throws IOException
{
if (numBytes <= 0)
{
return;
}
if (saslClient != null)
{
int skippedSoFar = 0;
final byte[] skipBuffer = new byte[numBytes];
while (true)
{
final int bytesRead = read(skipBuffer, skippedSoFar,
(numBytes - skippedSoFar));
if (bytesRead < 0)
{
// We unexpectedly hit the end of the stream. We'll just return since
// we clearly can't skip any more, and subsequent read attempts will
// fail.
return;
}
skippedSoFar += bytesRead;
totalBytesRead += bytesRead;
if (skippedSoFar >= numBytes)
{
return;
}
}
}
long totalBytesSkipped = inputStream.skip(numBytes);
while (totalBytesSkipped < numBytes)
{
final long bytesSkipped = inputStream.skip(numBytes - totalBytesSkipped);
if (bytesSkipped <= 0)
{
while (totalBytesSkipped < numBytes)
{
final int byteRead = read(false);
if (byteRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
totalBytesSkipped++;
}
}
else
{
totalBytesSkipped += bytesSkipped;
}
}
totalBytesRead += numBytes;
}
/**
* Reads a complete ASN.1 element from the input stream.
*
* @return The ASN.1 element read from the input stream, or {@code null} if
* the end of the input stream was reached before any data could be
* read. If {@code null} is returned, then the input stream will
* have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*/
@Nullable()
public ASN1Element readElement()
throws IOException
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
int valueBytesRead = 0;
int bytesRemaining = length;
final byte[] value = new byte[length];
while (valueBytesRead < length)
{
final int bytesRead = read(value, valueBytesRead, bytesRemaining);
if (bytesRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
valueBytesRead += bytesRead;
bytesRemaining -= bytesRead;
}
totalBytesRead += length;
final ASN1Element e = new ASN1Element((byte) type, value);
Debug.debugASN1Read(e);
return e;
}
/**
* Reads an ASN.1 Boolean element from the input stream and returns the value
* as a {@code Boolean}.
*
* @return The {@code Boolean} value of the ASN.1 Boolean element read, or
* {@code null} if the end of the input stream was reached before any
* data could be read. If {@code null} is returned, then the input
* stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*
* @throws ASN1Exception If the data read cannot be parsed as an ASN.1
* Boolean element.
*/
@Nullable()
public Boolean readBoolean()
throws IOException, ASN1Exception
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
if (length == 1)
{
final int value = read(false);
if (value < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
totalBytesRead++;
final Boolean booleanValue = (value != 0x00);
Debug.debugASN1Read(Level.INFO, "Boolean", type, 1, booleanValue);
return booleanValue;
}
else
{
skip(length);
throw new ASN1Exception(ERR_BOOLEAN_INVALID_LENGTH.get());
}
}
/**
* Reads an ASN.1 enumerated element from the input stream and returns the
* value as an {@code Integer}.
*
* @return The {@code Integer} value of the ASN.1 enumerated element read, or
* {@code null} if the end of the input stream was reached before any
* data could be read. If {@code null} is returned, then the input
* stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*
* @throws ASN1Exception If the data read cannot be parsed as an ASN.1
* enumerated element.
*/
@Nullable()
public Integer readEnumerated()
throws IOException, ASN1Exception
{
return readInteger();
}
/**
* Reads an ASN.1 generalized time element from the input stream and returns
* the value as a {@code Date}.
*
* @return The {@code Date} value of the ASN.1 generalized time element read,
* or {@code null} if the end of the input stream was reached before
* any data could be read. If {@code null} is returned, then the
* input stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*
* @throws ASN1Exception If the data read cannot be parsed as an ASN.1
* generalized time element.
*/
@Nullable()
public Date readGeneralizedTime()
throws IOException, ASN1Exception
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
int valueBytesRead = 0;
int bytesRemaining = length;
final byte[] value = new byte[length];
while (valueBytesRead < length)
{
final int bytesRead = read(value, valueBytesRead, bytesRemaining);
if (bytesRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
valueBytesRead += bytesRead;
bytesRemaining -= bytesRead;
}
totalBytesRead += length;
final String timestamp = StaticUtils.toUTF8String(value);
final Date date =
new Date(ASN1GeneralizedTime.decodeTimestamp(timestamp));
Debug.debugASN1Read(Level.INFO, "GeneralizedTime", type, length, timestamp);
return date;
}
/**
* Reads an ASN.1 integer element from the input stream and returns the value
* as an {@code Integer}.
*
* @return The {@code Integer} value of the ASN.1 integer element read, or
* {@code null} if the end of the input stream was reached before any
* data could be read. If {@code null} is returned, then the input
* stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*
* @throws ASN1Exception If the data read cannot be parsed as an ASN.1
* integer element.
*/
@Nullable()
public Integer readInteger()
throws IOException, ASN1Exception
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
if ((length == 0) || (length > 4))
{
skip(length);
throw new ASN1Exception(ERR_INTEGER_INVALID_LENGTH.get(length));
}
boolean negative = false;
int intValue = 0;
for (int i=0; i < length; i++)
{
final int byteRead = read(false);
if (byteRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
if (i == 0)
{
negative = ((byteRead & 0x80) != 0x00);
}
intValue <<= 8;
intValue |= (byteRead & 0xFF);
}
if (negative)
{
switch (length)
{
case 1:
intValue |= 0xFFFF_FF00;
break;
case 2:
intValue |= 0xFFFF_0000;
break;
case 3:
intValue |= 0xFF00_0000;
break;
}
}
totalBytesRead += length;
Debug.debugASN1Read(Level.INFO, "Integer", type, length, intValue);
return intValue;
}
/**
* Reads an ASN.1 integer element from the input stream and returns the value
* as a {@code Long}.
*
* @return The {@code Long} value of the ASN.1 integer element read, or
* {@code null} if the end of the input stream was reached before any
* data could be read. If {@code null} is returned, then the input
* stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*
* @throws ASN1Exception If the data read cannot be parsed as an ASN.1
* integer element.
*/
@Nullable()
public Long readLong()
throws IOException, ASN1Exception
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
if ((length == 0) || (length > 8))
{
skip(length);
throw new ASN1Exception(ERR_LONG_INVALID_LENGTH.get(length));
}
boolean negative = false;
long longValue = 0;
for (int i=0; i < length; i++)
{
final int byteRead = read(false);
if (byteRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
if (i == 0)
{
negative = ((byteRead & 0x80) != 0x00);
}
longValue <<= 8;
longValue |= (byteRead & 0xFFL);
}
if (negative)
{
switch (length)
{
case 1:
longValue |= 0xFFFF_FFFF_FFFF_FF00L;
break;
case 2:
longValue |= 0xFFFF_FFFF_FFFF_0000L;
break;
case 3:
longValue |= 0xFFFF_FFFF_FF00_0000L;
break;
case 4:
longValue |= 0xFFFF_FFFF_0000_0000L;
break;
case 5:
longValue |= 0xFFFF_FF00_0000_0000L;
break;
case 6:
longValue |= 0xFFFF_0000_0000_0000L;
break;
case 7:
longValue |= 0xFF00_0000_0000_0000L;
break;
}
}
totalBytesRead += length;
Debug.debugASN1Read(Level.INFO, "Long", type, length, longValue);
return longValue;
}
/**
* Reads an ASN.1 integer element from the input stream and returns the value
* as a {@code BigInteger}.
*
* @return The {@code BigInteger} value of the ASN.1 integer element read, or
* {@code null} if the end of the input stream was reached before any
* data could be read. If {@code null} is returned, then the input
* stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*
* @throws ASN1Exception If the data read cannot be parsed as an ASN.1
* integer element.
*/
@Nullable()
public BigInteger readBigInteger()
throws IOException, ASN1Exception
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
if (length == 0)
{
throw new ASN1Exception(ERR_BIG_INTEGER_DECODE_EMPTY_VALUE.get());
}
final byte[] valueBytes = new byte[length];
for (int i=0; i < length; i++)
{
final int byteRead = read(false);
if (byteRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
valueBytes[i] = (byte) byteRead;
}
final BigInteger bigIntegerValue = new BigInteger(valueBytes);
totalBytesRead += length;
Debug.debugASN1Read(Level.INFO, "BigInteger", type, length,
bigIntegerValue);
return bigIntegerValue;
}
/**
* Reads an ASN.1 null element from the input stream. No value will be
* returned but the null element will be consumed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*
* @throws ASN1Exception If the data read cannot be parsed as an ASN.1 null
* element.
*/
public void readNull()
throws IOException, ASN1Exception
{
final int type = readType();
if (type < 0)
{
return;
}
final int length = readLength();
if (length != 0)
{
skip(length);
throw new ASN1Exception(ERR_NULL_HAS_VALUE.get());
}
Debug.debugASN1Read(Level.INFO, "Null", type, 0, null);
}
/**
* Reads an ASN.1 octet string element from the input stream and returns the
* value as a byte array.
*
* @return The byte array value of the ASN.1 octet string element read, or
* {@code null} if the end of the input stream was reached before any
* data could be read. If {@code null} is returned, then the input
* stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*/
@Nullable()
public byte[] readBytes()
throws IOException
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
int valueBytesRead = 0;
int bytesRemaining = length;
final byte[] value = new byte[length];
while (valueBytesRead < length)
{
final int bytesRead = read(value, valueBytesRead, bytesRemaining);
if (bytesRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
valueBytesRead += bytesRead;
bytesRemaining -= bytesRead;
}
totalBytesRead += length;
Debug.debugASN1Read(Level.INFO, "byte[]", type, length, value);
return value;
}
/**
* Reads an ASN.1 octet string element from the input stream and returns the
* value as a {@code String} using the UTF-8 encoding.
*
* @return The {@code String} value of the ASN.1 octet string element read,
* or {@code null} if the end of the input stream was reached before
* any data could be read. If {@code null} is returned, then the
* input stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*/
@Nullable()
public String readString()
throws IOException
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
int valueBytesRead = 0;
int bytesRemaining = length;
final byte[] value = new byte[length];
while (valueBytesRead < length)
{
final int bytesRead = read(value, valueBytesRead, bytesRemaining);
if (bytesRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
valueBytesRead += bytesRead;
bytesRemaining -= bytesRead;
}
totalBytesRead += length;
final String s = StaticUtils.toUTF8String(value);
Debug.debugASN1Read(Level.INFO, "String", type, length, s);
return s;
}
/**
* Reads an ASN.1 UTC time element from the input stream and returns the value
* as a {@code Date}.
*
* @return The {@code Date} value of the ASN.1 UTC time element read, or
* {@code null} if the end of the input stream was reached before any
* data could be read. If {@code null} is returned, then the input
* stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*
* @throws ASN1Exception If the data read cannot be parsed as an ASN.1 UTC
* time element.
*/
@Nullable()
public Date readUTCTime()
throws IOException, ASN1Exception
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
int valueBytesRead = 0;
int bytesRemaining = length;
final byte[] value = new byte[length];
while (valueBytesRead < length)
{
final int bytesRead = read(value, valueBytesRead, bytesRemaining);
if (bytesRead < 0)
{
throw new IOException(ERR_READ_END_BEFORE_VALUE_END.get());
}
valueBytesRead += bytesRead;
bytesRemaining -= bytesRead;
}
totalBytesRead += length;
final String timestamp = StaticUtils.toUTF8String(value);
final Date date = new Date(ASN1UTCTime.decodeTimestamp(timestamp));
Debug.debugASN1Read(Level.INFO, "UTCTime", type, length, timestamp);
return date;
}
/**
* Reads the beginning of an ASN.1 sequence from the input stream and
* returns a value that can be used to determine when the end of the sequence
* has been reached. Elements which are part of the sequence may be read from
* this ASN.1 stream reader until the
* {@link ASN1StreamReaderSequence#hasMoreElements} method returns
* {@code false}.
*
* @return An object which may be used to determine when the end of the
* sequence has been reached, or {@code null} if the end of the input
* stream was reached before any data could be read. If {@code null}
* is returned, then the input stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*/
@Nullable()
public ASN1StreamReaderSequence beginSequence()
throws IOException
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
Debug.debugASN1Read(Level.INFO, "Sequence Header", type, length, null);
return new ASN1StreamReaderSequence(this, (byte) type, length);
}
/**
* Reads the beginning of an ASN.1 set from the input stream and returns a
* value that can be used to determine when the end of the set has been
* reached. Elements which are part of the set may be read from this ASN.1
* stream reader until the {@link ASN1StreamReaderSet#hasMoreElements} method
* returns {@code false}.
*
* @return An object which may be used to determine when the end of the set
* has been reached, or {@code null} if the end of the input stream
* was reached before any data could be read. If {@code null} is
* returned, then the input stream will have been closed.
*
* @throws IOException If a problem occurs while reading from the input
* stream, if the end of the input stream is reached in
* the middle of the element, or or if an attempt is
* made to read an element larger than the maximum
* allowed size.
*/
@Nullable()
public ASN1StreamReaderSet beginSet()
throws IOException
{
final int type = readType();
if (type < 0)
{
return null;
}
final int length = readLength();
Debug.debugASN1Read(Level.INFO, "Set Header", type, length, null);
return new ASN1StreamReaderSet(this, (byte) type, length);
}
/**
* Reads a byte of data from the underlying input stream, optionally ignoring
* socket timeout exceptions.
*
* @param initial Indicates whether this is the initial read for an element.
*
* @return The byte read from the input stream, or -1 if the end of the
* input stream was reached.
*
* @throws IOException If a problem occurs while reading data.
*/
private int read(final boolean initial)
throws IOException
{
if (saslClient != null)
{
if (saslInputStream != null)
{
final int b = saslInputStream.read();
if (b >= 0)
{
return b;
}
}
readAndDecodeSASLData(-1);
return saslInputStream.read();
}
try
{
final int b = inputStream.read();
if ((saslClient == null) || (b < 0))
{
return b;
}
else
{
// This should only happen the first time after the SASL client has been
// installed.
readAndDecodeSASLData(b);
return saslInputStream.read();
}
}
catch (final SocketTimeoutException ste)
{
Debug.debugException(Level.FINEST, ste);
if ((initial && ignoreInitialSocketTimeout) ||
((! initial) && ignoreSubsequentSocketTimeout))
{
while (true)
{
try
{
return inputStream.read();
}
catch (final SocketTimeoutException ste2)
{
Debug.debugException(Level.FINEST, ste2);
}
}
}
else
{
throw ste;
}
}
}
/**
* Reads data from the underlying input stream, optionally ignoring socket
* timeout exceptions.
*
* @param buffer The buffer into which the data should be read.
* @param offset The position at which to start placing the data that was
* read.
* @param length The maximum number of bytes to read.
*
* @return The number of bytes read, or -1 if the end of the input stream
* was reached.
*
* @throws IOException If a problem occurs while reading data.
*/
private int read(@NotNull final byte[] buffer, final int offset,
final int length)
throws IOException
{
if (saslClient != null)
{
if (saslInputStream != null)
{
final int bytesRead = saslInputStream.read(buffer, offset, length);
if (bytesRead > 0)
{
return bytesRead;
}
}
readAndDecodeSASLData(-1);
return saslInputStream.read(buffer, offset, length);
}
try
{
return inputStream.read(buffer, offset, length);
}
catch (final SocketTimeoutException ste)
{
Debug.debugException(Level.FINEST, ste);
if (ignoreSubsequentSocketTimeout)
{
while (true)
{
try
{
return inputStream.read(buffer, offset, length);
}
catch (final SocketTimeoutException ste2)
{
Debug.debugException(Level.FINEST, ste2);
}
}
}
else
{
throw ste;
}
}
}
/**
* Sets the SASL client to use to unwrap any data read over this ASN.1 stream
* reader.
*
* @param saslClient The SASL client to use to unwrap any data read over
* this ASN.1 stream reader.
*/
void setSASLClient(@NotNull final SaslClient saslClient)
{
this.saslClient = saslClient;
}
/**
* Reads data from the underlying input stream, unwraps it using the
* configured SASL client, and makes the result available in a byte array
* input stream that will be used for subsequent reads.
*
* @param firstByte The first byte that has already been read. This should
* only be used if the value is greater than or equal to
* zero.
*
* @throws IOException If a problem is encountered while reading from the
* underlying input stream or decoding the data that
* has been read.
*/
private void readAndDecodeSASLData(final int firstByte)
throws IOException
{
// The first four bytes must be the number of bytes of data to unwrap.
int numWrappedBytes = 0;
int numLengthBytes = 4;
if (firstByte >= 0)
{
numLengthBytes = 3;
numWrappedBytes = firstByte;
}
for (int i=0; i < numLengthBytes; i++)
{
final int b = inputStream.read();
if (b < 0)
{
if ((i == 0) && (firstByte < 0))
{
// This means that we hit the end of the input stream without
// reading any data. This is fine and just means that the end of
// the input stream has been reached.
saslInputStream = new ByteArrayInputStream(StaticUtils.NO_BYTES);
}
else
{
// This means that we hit the end of the input stream after having
// read a portion of the number of wrapped bytes. This is an error.
throw new IOException(
ERR_STREAM_READER_EOS_READING_SASL_LENGTH.get(i));
}
}
else
{
numWrappedBytes = (numWrappedBytes << 8) | (b & 0xFF);
}
}
if ((maxElementSize > 0) && (numWrappedBytes > maxElementSize))
{
throw new IOException(ERR_READ_SASL_LENGTH_EXCEEDS_MAX.get(
numWrappedBytes, maxElementSize));
}
int wrappedDataPos = 0;
final byte[] wrappedData = new byte[numWrappedBytes];
while (true)
{
final int numBytesRead = inputStream.read(wrappedData, wrappedDataPos,
(numWrappedBytes - wrappedDataPos));
if (numBytesRead < 0)
{
throw new IOException(ERR_STREAM_READER_EOS_READING_SASL_DATA.get(
wrappedDataPos, numWrappedBytes));
}
wrappedDataPos += numBytesRead;
if (wrappedDataPos >= numWrappedBytes)
{
break;
}
}
final byte[] unwrappedData =
saslClient.unwrap(wrappedData, 0, numWrappedBytes);
saslInputStream = new ByteArrayInputStream(unwrappedData, 0,
unwrappedData.length);
}
}