| | |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2015 ForgeRock AS. |
| | | * Portions Copyright 2012-2016 ForgeRock AS. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | | import static org.forgerock.opendj.ldap.ResultCode.UNWILLING_TO_PERFORM; |
| | | import static org.forgerock.util.Reject.*; |
| | | import static org.forgerock.util.Utils.*; |
| | | import static org.opends.messages.BackendMessages.*; |
| | |
| | | import static org.opends.server.core.DirectoryServer.*; |
| | | |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | | import java.util.zip.DataFormatException; |
| | | import java.util.zip.DeflaterOutputStream; |
| | | import java.util.zip.InflaterInputStream; |
| | | import java.util.zip.InflaterOutputStream; |
| | | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.io.ASN1; |
| | | import org.forgerock.opendj.io.ASN1Reader; |
| | | import org.forgerock.opendj.io.ASN1Writer; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | | import org.forgerock.opendj.ldap.ByteSequenceReader; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ByteStringBuilder; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | |
| | | import org.opends.server.backends.pluggable.spi.TreeName; |
| | | import org.opends.server.backends.pluggable.spi.WriteableTransaction; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.types.CryptoManagerException; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.types.LDAPException; |
| | |
| | | /** A cached set of ByteStringBuilder buffers and ASN1Writer used to encode entries. */ |
| | | private static final class EntryCodec |
| | | { |
| | | /** |
| | | * The format version used encode and decode entries in previous versions. |
| | | * Not used anymore, kept for compatibility during upgrade. |
| | | */ |
| | | static final byte FORMAT_VERSION = 0x01; |
| | | |
| | | /** The ASN1 tag for the ByteString type. */ |
| | | private static final byte TAG_TREE_ENTRY = 0x60; |
| | | private static final int BUFFER_INIT_SIZE = 512; |
| | | private static final byte PLAIN_ENTRY = 0x00; |
| | | private static final byte COMPRESS_ENTRY = 0x01; |
| | | private static final byte ENCRYPT_ENTRY = 0x02; |
| | | |
| | | /** The format version for entry encoding. */ |
| | | static final byte FORMAT_VERSION_V2 = 0x02; |
| | | |
| | | private final ByteStringBuilder encodedBuffer = new ByteStringBuilder(); |
| | | private final ByteStringBuilder entryBuffer = new ByteStringBuilder(); |
| | | private final ByteStringBuilder compressedEntryBuffer = new ByteStringBuilder(); |
| | | private final ASN1Writer writer; |
| | | private final int maxBufferSize; |
| | | |
| | | private EntryCodec() |
| | | { |
| | | this.maxBufferSize = getMaxInternalBufferSize(); |
| | | this.writer = ASN1.getWriter(encodedBuffer, maxBufferSize); |
| | | } |
| | | |
| | | private void release() |
| | | { |
| | | closeSilently(writer); |
| | | encodedBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE); |
| | | entryBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE); |
| | | compressedEntryBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE); |
| | | } |
| | | |
| | | private Entry decode(ByteString bytes, CompressedSchema compressedSchema) |
| | | throws DirectoryException, DecodeException, IOException |
| | | { |
| | | // Get the format version. |
| | | byte formatVersion = bytes.byteAt(0); |
| | | if(formatVersion != DnKeyFormat.FORMAT_VERSION) |
| | | final byte formatVersion = bytes.byteAt(0); |
| | | switch(formatVersion) |
| | | { |
| | | case FORMAT_VERSION: |
| | | return decodeV1(bytes, compressedSchema); |
| | | case FORMAT_VERSION_V2: |
| | | return decodeV2(bytes, compressedSchema); |
| | | default: |
| | | throw DecodeException.error(ERR_INCOMPATIBLE_ENTRY_VERSION.get(formatVersion)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Decodes an entry from the old format. |
| | | * <p> |
| | | * An entry on disk is ASN1 encoded in this format: |
| | | * |
| | | * <pre> |
| | | * ByteString ::= [APPLICATION 0] IMPLICIT SEQUENCE { |
| | | * uncompressedSize INTEGER, -- A zero value means not compressed. |
| | | * dataBytes OCTET STRING -- Optionally compressed encoding of |
| | | * the data bytes. |
| | | * } |
| | | * |
| | | * ID2EntryValue ::= ByteString |
| | | * -- Where dataBytes contains an encoding of DirectoryServerEntry. |
| | | * |
| | | * DirectoryServerEntry ::= [APPLICATION 1] IMPLICIT SEQUENCE { |
| | | * dn LDAPDN, |
| | | * objectClasses SET OF LDAPString, |
| | | * userAttributes AttributeList, |
| | | * operationalAttributes AttributeList |
| | | * } |
| | | * </pre> |
| | | * |
| | | * @param bytes A byte array containing the encoded tree value. |
| | | * @param compressedSchema The compressed schema manager to use when decoding. |
| | | * @return The decoded entry. |
| | | * @throws DecodeException If the data is not in the expected ASN.1 encoding |
| | | * format. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws IOException if an error occurs while reading the ASN1 sequence. |
| | | */ |
| | | private Entry decodeV1(ByteString bytes, CompressedSchema compressedSchema) |
| | | throws DirectoryException, DecodeException, IOException |
| | | { |
| | | // Read the ASN1 sequence. |
| | | ASN1Reader reader = ASN1.getReader(bytes.subSequence(1, bytes.length())); |
| | | reader.readStartSequence(); |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Decodes an entry in the new extensible format. |
| | | * Enties are encoded according to the sequence |
| | | * {VERSION_BYTE, FLAG_BYTE, COMPACT_INTEGER_LENGTH, ID2ENTRY_VALUE} |
| | | * where |
| | | * |
| | | * ID2ENTRY_VALUE = encoding of Entry as in decodeV1() |
| | | * VERSION_BYTE = 0x2 |
| | | * FLAG_BYTE = bit field of OR'ed values indicating post-encoding processing. |
| | | * possible meaningful flags are COMPRESS_ENTRY and ENCRYPT_ENTRY. |
| | | * COMPACT_INTEGER_LENGTH = length of ID2ENTRY_VALUE |
| | | * |
| | | * @param bytes A byte array containing the encoded tree value. |
| | | * @param compressedSchema The compressed schema manager to use when decoding. |
| | | * @return The decoded entry. |
| | | * @throws DecodeException If the data is not in the expected ASN.1 encoding |
| | | * format or a decryption error occurs. |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws IOException if an error occurs while reading the ASN1 sequence. |
| | | */ |
| | | private Entry decodeV2(ByteString bytes, CompressedSchema compressedSchema) |
| | | throws DirectoryException, DecodeException, IOException |
| | | { |
| | | ByteSequenceReader reader = bytes.asReader(); |
| | | // skip version byte |
| | | reader.position(1); |
| | | int format = reader.readByte(); |
| | | int encodedEntryLen = reader.readCompactUnsignedInt(); |
| | | try |
| | | { |
| | | if (format == PLAIN_ENTRY) |
| | | { |
| | | return Entry.decode(reader, compressedSchema); |
| | | } |
| | | InputStream is = reader.asInputStream(); |
| | | if ((format & ENCRYPT_ENTRY) == ENCRYPT_ENTRY) |
| | | { |
| | | is = getCryptoManager().getCipherInputStream(is); |
| | | } |
| | | if ((format & COMPRESS_ENTRY) == COMPRESS_ENTRY) |
| | | { |
| | | is = new InflaterInputStream(is); |
| | | } |
| | | byte[] data = new byte[encodedEntryLen]; |
| | | int readBytes; |
| | | int position = 0; |
| | | int leftToRead = encodedEntryLen; |
| | | // CipherInputStream does not read more than block size... |
| | | do |
| | | { |
| | | if ((readBytes = is.read(data, position, leftToRead)) == -1 ) |
| | | { |
| | | throw DecodeException.error(ERR_CANNOT_DECODE_ENTRY.get()); |
| | | } |
| | | position += readBytes; |
| | | leftToRead -= readBytes; |
| | | } while (leftToRead > 0 && readBytes > 0); |
| | | return Entry.decode(ByteString.wrap(data).asReader(), compressedSchema); |
| | | } |
| | | catch (CryptoManagerException cme) |
| | | { |
| | | throw DecodeException.error(cme.getMessageObject()); |
| | | } |
| | | } |
| | | |
| | | private ByteString encode(Entry entry, DataConfig dataConfig) throws DirectoryException |
| | | { |
| | | encodeVolatile(entry, dataConfig); |
| | |
| | | |
| | | private void encodeVolatile(Entry entry, DataConfig dataConfig) throws DirectoryException |
| | | { |
| | | // Encode the entry for later use. |
| | | entry.encode(entryBuffer, dataConfig.getEntryEncodeConfig()); |
| | | |
| | | // First write the DB format version byte. |
| | | encodedBuffer.appendByte(DnKeyFormat.FORMAT_VERSION); |
| | | |
| | | OutputStream os = encodedBuffer.asOutputStream(); |
| | | try |
| | | { |
| | | // Then start the ASN1 sequence. |
| | | writer.writeStartSequence(TAG_TREE_ENTRY); |
| | | |
| | | byte[] formatFlags = { FORMAT_VERSION_V2, 0}; |
| | | os.write(formatFlags); |
| | | encodedBuffer.appendCompactUnsigned(entryBuffer.length()); |
| | | if (dataConfig.isCompressed()) |
| | | { |
| | | OutputStream compressor = null; |
| | | try { |
| | | compressor = new DeflaterOutputStream(compressedEntryBuffer.asOutputStream()); |
| | | entryBuffer.copyTo(compressor); |
| | | } |
| | | finally { |
| | | closeSilently(compressor); |
| | | } |
| | | |
| | | // Compression needed and successful. |
| | | writer.writeInteger(entryBuffer.length()); |
| | | writer.writeOctetString(compressedEntryBuffer); |
| | | os = new DeflaterOutputStream(os); |
| | | formatFlags[1] = COMPRESS_ENTRY; |
| | | } |
| | | else |
| | | if (dataConfig.isEncrypted()) |
| | | { |
| | | writer.writeInteger(0); |
| | | writer.writeOctetString(entryBuffer); |
| | | os = dataConfig.getCryptoSuite().getCipherOutputStream(os); |
| | | formatFlags[1] |= ENCRYPT_ENTRY; |
| | | } |
| | | encodedBuffer.setByte(1, formatFlags[1]); |
| | | |
| | | writer.writeEndSequence(); |
| | | entryBuffer.copyTo(os); |
| | | os.flush(); |
| | | } |
| | | catch(IOException ioe) |
| | | catch(CryptoManagerException | IOException e) |
| | | { |
| | | // TODO: This should never happen with byte buffer. |
| | | logger.traceException(ioe); |
| | | logger.traceException(e); |
| | | throw new DirectoryException(UNWILLING_TO_PERFORM, ERR_CANNOT_ENCODE_ENTRY.get(e.getLocalizedMessage())); |
| | | } |
| | | finally |
| | | { |
| | | try |
| | | { |
| | | os.close(); |
| | | } |
| | | catch (IOException ioe) |
| | | { |
| | | throw new DirectoryException(UNWILLING_TO_PERFORM, ERR_CANNOT_ENCODE_ENTRY.get(ioe.getLocalizedMessage())); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | /** |
| | | * Decodes an entry from its tree representation. |
| | | * <p> |
| | | * An entry on disk is ASN1 encoded in this format: |
| | | * |
| | | * <pre> |
| | | * ByteString ::= [APPLICATION 0] IMPLICIT SEQUENCE { |
| | | * uncompressedSize INTEGER, -- A zero value means not compressed. |
| | | * dataBytes OCTET STRING -- Optionally compressed encoding of |
| | | * the data bytes. |
| | | * } |
| | | * |
| | | * ID2EntryValue ::= ByteString |
| | | * -- Where dataBytes contains an encoding of DirectoryServerEntry. |
| | | * |
| | | * DirectoryServerEntry ::= [APPLICATION 1] IMPLICIT SEQUENCE { |
| | | * dn LDAPDN, |
| | | * objectClasses SET OF LDAPString, |
| | | * userAttributes AttributeList, |
| | | * operationalAttributes AttributeList |
| | | * } |
| | | * </pre> |
| | | * |
| | | * @param bytes A byte array containing the encoded tree value. |
| | | * @param compressedSchema The compressed schema manager to use when decoding. |
| | |
| | | * @throws DirectoryException If a Directory Server error occurs. |
| | | * @throws IOException if an error occurs while reading the ASN1 sequence. |
| | | */ |
| | | static Entry entryFromDatabase(ByteString bytes, |
| | | Entry entryFromDatabase(ByteString bytes, |
| | | CompressedSchema compressedSchema) throws DirectoryException, |
| | | DecodeException, LDAPException, DataFormatException, IOException |
| | | { |
| | |
| | | * @throws DirectoryException If a problem occurs while attempting to encode |
| | | * the entry. |
| | | */ |
| | | static ByteString entryToDatabase(Entry entry, DataConfig dataConfig) throws DirectoryException |
| | | ByteString entryToDatabase(Entry entry, DataConfig dataConfig) throws DirectoryException |
| | | { |
| | | EntryCodec codec = acquireEntryCodec(); |
| | | try |