commit bdb39ae448580a40c98bbfd5c00c63e553909237 Author: Jörg Prante Date: Mon Aug 5 23:28:21 2024 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..021874f --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/.settings +/.classpath +/.project +/.gradle +**/data +**/work +**/logs +**/.idea +**/target +**/out +**/build +.DS_Store +*.iml +*~ +*.key +*.crt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/asn1/src/main/java/module-info.java b/asn1/src/main/java/module-info.java new file mode 100644 index 0000000..38b1625 --- /dev/null +++ b/asn1/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module org.xbib.net.ldap.asnone { + exports org.xbib.asn1; +} diff --git a/asn1/src/main/java/org/xbib/asn1/AbstractDERTag.java b/asn1/src/main/java/org/xbib/asn1/AbstractDERTag.java new file mode 100644 index 0000000..a282cbd --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/AbstractDERTag.java @@ -0,0 +1,55 @@ + +package org.xbib.asn1; + +/** + * Abstract base class for custom DER tag types. + * + */ +public abstract class AbstractDERTag implements DERTag { + + /** + * Tag number. + */ + private final int tagNo; + + /** + * Flag indicating whether value is primitive or constructed. + */ + private final boolean constructed; + + + /** + * Creates a new tag with given tag number. + * + * @param number Tag number. + * @param isConstructed True for constructed tag, false otherwise. + */ + public AbstractDERTag(final int number, final boolean isConstructed) { + tagNo = number; + constructed = isConstructed; + } + + + @Override + public int getTagNo() { + return tagNo; + } + + + @Override + public boolean isConstructed() { + return constructed; + } + + + @Override + public int getTagByte() { + return constructed ? tagNo | ASN_CONSTRUCTED : tagNo; + } + + + @Override + public String toString() { + return name() + "(" + tagNo + ")"; + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/AbstractDERType.java b/asn1/src/main/java/org/xbib/asn1/AbstractDERType.java new file mode 100644 index 0000000..575e935 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/AbstractDERType.java @@ -0,0 +1,81 @@ + +package org.xbib.asn1; + +import java.nio.ByteBuffer; + +/** + * Provides functionality common to DER types implementations. + * + */ +public abstract class AbstractDERType { + + /** + * Length of short form integers. + */ + private static final int SHORT_FORM_INT_LENGTH = 127; + + /** + * Constructed tag. + */ + private final int derTag; + + + /** + * Creates a new abstract der type. + * + * @param tag to encode for this type + */ + public AbstractDERType(final DERTag tag) { + derTag = tag.getTagByte(); + } + + + /** + * DER encodes the supplied items with the tag associated with this type. If the length is greater than 127 bytes the + * long form is always expressed using 4 bytes. + * + * @param items to encode + * @return DER encoded items + */ + protected byte[] encode(final byte[]... items) { + int itemLength = 0; + if (items != null) { + for (byte[] b : items) { + if (b != null) { + itemLength += b.length; + } + } + } + + final byte[] lengthBytes; + if (itemLength <= SHORT_FORM_INT_LENGTH) { + lengthBytes = new byte[]{(byte) itemLength}; + } else { + // use 4 bytes for all long form integers + // CheckStyle:MagicNumber OFF + lengthBytes = new byte[]{ + (byte) 0x84, + (byte) (itemLength >>> 24), + (byte) (itemLength >>> 16), + (byte) (itemLength >>> 8), + (byte) itemLength, + }; + // CheckStyle:MagicNumber ON + } + + // add 1 for the type tag, 1 or 5 for the length + final ByteBuffer encodedItem = ByteBuffer.allocate(itemLength + 1 + lengthBytes.length); + encodedItem.put((byte) derTag); + for (byte b : lengthBytes) { + encodedItem.put(b); + } + if (items != null) { + for (byte[] b : items) { + if (b != null) { + encodedItem.put(b); + } + } + } + return encodedItem.array(); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/AbstractParseHandler.java b/asn1/src/main/java/org/xbib/asn1/AbstractParseHandler.java new file mode 100644 index 0000000..f2d1d0c --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/AbstractParseHandler.java @@ -0,0 +1,35 @@ + +package org.xbib.asn1; + +/** + * Parse handler for managing and initializing an object. + * + * @param type of object initialized by this handler + */ +public abstract class AbstractParseHandler implements ParseHandler { + + /** + * Object to initialize. + */ + private final T object; + + + /** + * Creates a new abstract parse handler. + * + * @param t object to initialize + */ + public AbstractParseHandler(final T t) { + object = t; + } + + + /** + * Returns the object. + * + * @return object + */ + public T getObject() { + return object; + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/ApplicationDERTag.java b/asn1/src/main/java/org/xbib/asn1/ApplicationDERTag.java new file mode 100644 index 0000000..2b0c63b --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/ApplicationDERTag.java @@ -0,0 +1,48 @@ + +package org.xbib.asn1; + +/** + * Generic application-specific tag. + * + */ +public class ApplicationDERTag extends AbstractDERTag { + + /** + * Generic tag name "APP" for an application-specific type. + */ + public static final String TAG_NAME = "APP"; + + /** + * Application class is 01b in first two high-order bits. + */ + public static final int TAG_CLASS = 0x40; + + + /** + * Creates a new application-specific tag with given tag number. + * + * @param number Tag number. + * @param isConstructed True for constructed tag, false otherwise. + */ + public ApplicationDERTag(final int number, final boolean isConstructed) { + super(number, isConstructed); + } + + + @Override + public int getTagByte() { + return super.getTagByte() | TAG_CLASS; + } + + + @Override + public String name() { + return TAG_NAME + "(" + getTagNo() + ")"; + } + + + @Override + public String toString() { + return name(); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/BooleanType.java b/asn1/src/main/java/org/xbib/asn1/BooleanType.java new file mode 100644 index 0000000..f62ea0b --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/BooleanType.java @@ -0,0 +1,87 @@ + +package org.xbib.asn1; + +/** + * Converts booleans to and from their DER encoded format. + * + */ +public class BooleanType extends AbstractDERType implements DEREncoder { + + /** + * Boolean true byte representation. + */ + private static final byte TRUE_BYTE = (byte) 0xff; + + /** + * Boolean false byte representation. + */ + private static final byte FALSE_BYTE = (byte) 0x00; + + /** + * Boolean to encode. + */ + private final byte[] derItem; + + + /** + * Creates a new boolean type. + * + * @param item to DER encode + */ + public BooleanType(final boolean item) { + super(UniversalDERTag.BOOL); + derItem = toBytes(item); + } + + + /** + * Creates a new boolean type. + * + * @param tag der tag associated with this type + * @param item to DER encode + * @throws IllegalArgumentException if the der tag is constructed + */ + public BooleanType(final DERTag tag, final boolean item) { + super(tag); + if (tag.isConstructed()) { + throw new IllegalArgumentException("DER tag must not be constructed"); + } + derItem = toBytes(item); + } + + /** + * Converts bytes in the buffer to a boolean by reading from the current position to the limit. + * + * @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of boolean bytes + * and the limit is set beyond the last byte of integer data. + * @return decoded bytes as a boolean. + */ + public static boolean decode(final DERBuffer encoded) { + final byte[] bytes = encoded.getRemainingBytes(); + if (bytes.length > 1) { + throw new IllegalArgumentException("Boolean cannot be longer than 1 byte"); + } + if (bytes[0] == TRUE_BYTE) { + return true; + } else if (bytes[0] == FALSE_BYTE) { + return false; + } else { + throw new IllegalArgumentException("Invalid boolean value: " + (int) bytes[0]); + } + } + + /** + * Converts the supplied boolean to a byte array. + * + * @param b to convert + * @return byte array + */ + public static byte[] toBytes(final boolean b) { + return new byte[]{b ? TRUE_BYTE : FALSE_BYTE}; + } + + @Override + public byte[] encode() { + return encode(derItem); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/ConstructedDEREncoder.java b/asn1/src/main/java/org/xbib/asn1/ConstructedDEREncoder.java new file mode 100644 index 0000000..1767eca --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/ConstructedDEREncoder.java @@ -0,0 +1,51 @@ + +package org.xbib.asn1; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Encodes constructed types to their DER format. + * + */ +public class ConstructedDEREncoder extends AbstractDERType implements DEREncoder { + + /** + * Encoders in this sequence. + */ + private final DEREncoder[] derEncoders; + + + /** + * Creates a new sequence encoder. + * + * @param tag der tag associated with this type + * @param encoders to encode in this sequence + */ + public ConstructedDEREncoder(final DERTag tag, final DEREncoder... encoders) { + super(tag); + if (!tag.isConstructed()) { + throw new IllegalArgumentException("DER tag must be constructed"); + } + if (encoders == null || encoders.length == 0) { + throw new IllegalArgumentException("Encoders cannot be null or empty"); + } + derEncoders = encoders; + } + + + @Override + public byte[] encode() { + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try { + try (bytes) { + for (DEREncoder encoder : derEncoders) { + bytes.write(encoder.encode()); + } + } + } catch (IOException e) { + throw new IllegalStateException("Encode failed", e); + } + return encode(bytes.toByteArray()); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/ContextDERTag.java b/asn1/src/main/java/org/xbib/asn1/ContextDERTag.java new file mode 100644 index 0000000..0cde9d6 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/ContextDERTag.java @@ -0,0 +1,42 @@ + +package org.xbib.asn1; + +/** + * Generic context-specific tag. + * + */ +public class ContextDERTag extends AbstractDERTag { + + /** + * Generic tag name "CTX" for a context-specific type. + */ + public static final String TAG_NAME = "CTX"; + + /** + * Context-specific class is 10b in first two high-order bits. + */ + public static final int TAG_CLASS = 0x80; + + + /** + * Creates a new context-specific tag with given tag number. + * + * @param number Tag number. + * @param isConstructed True for constructed tag, false otherwise. + */ + public ContextDERTag(final int number, final boolean isConstructed) { + super(number, isConstructed); + } + + + @Override + public int getTagByte() { + return super.getTagByte() | TAG_CLASS; + } + + + @Override + public String name() { + return TAG_NAME + "(" + getTagNo() + ")"; + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/ContextType.java b/asn1/src/main/java/org/xbib/asn1/ContextType.java new file mode 100644 index 0000000..37d508e --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/ContextType.java @@ -0,0 +1,67 @@ + +package org.xbib.asn1; + +import java.math.BigInteger; + +/** + * Converts context types to their DER encoded format. + * + */ +public class ContextType extends AbstractDERType implements DEREncoder { + + /** + * Data to encode. + */ + private final byte[] derItem; + + + /** + * Creates a new context type. + * + * @param index of this item in the context + * @param item to encode + */ + public ContextType(final int index, final byte[] item) { + super(new ContextDERTag(index, false)); + derItem = item; + } + + + /** + * Creates a new context type. + * + * @param index of this item in the context + * @param item to encode + */ + public ContextType(final int index, final String item) { + this(index, OctetStringType.toBytes(item)); + } + + + /** + * Creates a new context type. + * + * @param index of this item in the context + * @param item to encode + */ + public ContextType(final int index, final boolean item) { + this(index, BooleanType.toBytes(item)); + } + + + /** + * Creates a new context type. + * + * @param index of this item in the context + * @param item to encode + */ + public ContextType(final int index, final BigInteger item) { + this(index, IntegerType.toBytes(item)); + } + + + @Override + public byte[] encode() { + return encode(derItem); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/CustomDERTag.java b/asn1/src/main/java/org/xbib/asn1/CustomDERTag.java new file mode 100644 index 0000000..320c4f6 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/CustomDERTag.java @@ -0,0 +1,40 @@ + +package org.xbib.asn1; + +/** + * Describes the tag of an application-specific, context-specific, or private DER type where the tag name may be + * specified for clarity in application code. + * + */ +public class CustomDERTag extends AbstractDERTag { + + /** + * Tag name. + */ + private final String tagName; + + + /** + * Creates a new custom DER tag. + * + * @param number of the tag + * @param name of the tag + * @param isConstructed whether this tag is primitive or constructed + */ + public CustomDERTag(final int number, final String name, final boolean isConstructed) { + super(number, isConstructed); + tagName = name; + } + + + @Override + public String name() { + return tagName; + } + + + @Override + public String toString() { + return name() + "(" + getTagNo() + ")"; + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/DERBuffer.java b/asn1/src/main/java/org/xbib/asn1/DERBuffer.java new file mode 100644 index 0000000..456b4cb --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/DERBuffer.java @@ -0,0 +1,128 @@ + +package org.xbib.asn1; + +/** + * Byte buffer used for DER parsing. + * + */ +public interface DERBuffer { + + + /** + * Returns this buffer's position. + * + * @return position of this buffer + */ + int position(); + + + /** + * Sets this buffer's position. + * + * @param newPosition The new position value; must be non-negative + * and no larger than the current limit + * @return This buffer + * @throws IllegalArgumentException if the preconditions on newPosition do not hold + */ + DERBuffer position(int newPosition); + + + /** + * Returns this buffer's limit. + * + * @return limit of this buffer + */ + int limit(); + + + /** + * Sets this buffer's limit. + * + * @param newLimit The new limit value; must be non-negative + * and no larger than this buffer's capacity + * @return This buffer + * @throws IllegalArgumentException if the preconditions on newLimit do not hold + */ + DERBuffer limit(int newLimit); + + + /** + * Sets the position to zero and the limit to the capacity. + * + *

This method does not actually erase the data in the buffer.

+ * + * @return This buffer + */ + DERBuffer clear(); + + + /** + * Returns the number of elements between the current position and the limit. + * + * @return number of elements remaining in this buffer + */ + default int remaining() { + return limit() - position(); + } + + + /** + * Returns whether there are any elements between the current position and the limit. + * + * @return true iff there is at least one element remaining in this buffer + */ + default boolean hasRemaining() { + return position() < limit(); + } + + + /** + * Returns this buffer's capacity. + * + * @return capacity of this buffer + */ + int capacity(); + + + /** + * Relative get method. Reads the byte at this buffer's current position and then increments the position. + * + * @return byte at the buffer's current position + */ + byte get(); + + + /** + * Relative bulk get method. + * + * @param dst destination array + * @return This buffer + */ + DERBuffer get(byte[] dst); + + + /** + * Returns the bytes remaining in the buffer. Those bytes between {@link #position()} and {@link #limit()}. + * + * @return remaining bytes + */ + default byte[] getRemainingBytes() { + final byte[] b = new byte[remaining()]; + get(b); + return b; + } + + + /** + * Creates a new DER buffer whose content is a shared sub-sequence of this buffer's content. + * + *

The content of the new buffer will start at this buffer's current position. Changes to this buffer's content + * will be visible in the new buffer, and vice versa; the two buffers' position and limit will be independent.

+ * + *

The new buffer's position will be zero, its capacity and its limit will be the number of bytes remaining in this + * buffer.

+ * + * @return The new byte buffer + */ + DERBuffer slice(); +} diff --git a/asn1/src/main/java/org/xbib/asn1/DEREncoder.java b/asn1/src/main/java/org/xbib/asn1/DEREncoder.java new file mode 100644 index 0000000..79bf2a8 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/DEREncoder.java @@ -0,0 +1,17 @@ + +package org.xbib.asn1; + +/** + * Interface for encoding DER objects. + * + */ +public interface DEREncoder { + + + /** + * Encode this object into its DER type. + * + * @return DER encoded object + */ + byte[] encode(); +} diff --git a/asn1/src/main/java/org/xbib/asn1/DERParser.java b/asn1/src/main/java/org/xbib/asn1/DERParser.java new file mode 100644 index 0000000..5e8c223 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/DERParser.java @@ -0,0 +1,213 @@ + +package org.xbib.asn1; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; + +/** + * This class provides a SAX-like parsing facility for DER-encoded data where elements of interest in the parse tree may + * be registered to handlers via the {@link #registerHandler} methods. {@link DERPath} strings are used to map handlers + * to elements of interest. + */ +public class DERParser { + + /** + * Handlers for DER paths. + */ + private final Map handlerMap = new HashMap<>(); + + /** + * Permutations of the current path. + */ + private final Queue permutations = new ArrayDeque<>(); + + public DERParser() { + } + + /** + * Registers the supplied handler to fire when the supplied path is encountered. + * + * @param path to register + * @param handler to associate with the path + */ + public void registerHandler(final DERPath path, final ParseHandler handler) { + handlerMap.put(path, handler); + } + + + /** + * Parse a DER-encoded data structure by calling registered handlers when points of interest are encountered in the + * parse tree. + * + * @param encoded DER-encoded bytes. + */ + public void parse(final DERBuffer encoded) { + parseTags(encoded); + } + + + /** + * Reads a DER tag from a single byte at the current position of the given buffer. The buffer position is naturally + * advanced one byte in this operation. + * + * @param encoded Buffer containing DER-encoded bytes positioned at tag. + * @return Tag or null if no universal tag or application-specific tag is known that matches the byte read in. + */ + public DERTag readTag(final DERBuffer encoded) { + if (encoded.position() >= encoded.limit()) { + return null; + } + + final DERTag tag; + final byte b = encoded.get(); + // CheckStyle:MagicNumber OFF + final int tagNo = b & 0x1F; + final boolean constructed = (b & 0x20) == 0x20; + // Read class from first two high-order bits + switch (b & 0xC0) { + + case UniversalDERTag.TAG_CLASS: + tag = UniversalDERTag.fromTagNo(tagNo); + break; + + case ApplicationDERTag.TAG_CLASS: + tag = new ApplicationDERTag(tagNo, constructed); + break; + + case ContextDERTag.TAG_CLASS: + tag = new ContextDERTag(tagNo, constructed); + break; + + default: + // Private class (class 11b) + throw new IllegalArgumentException("Private classes not supported."); + } + // CheckStyle:MagicNumber ON + return tag; + } + + + /** + * Reads the length of a DER-encoded value from the given byte buffer. The buffer is expected to be positioned at the + * byte immediately following the tag byte, which is where the length byte(s) begin(s). Invocation of this method has + * two generally beneficial side effects: + * + *
    + *
  1. Buffer is positioned at start of value bytes.
  2. + *
  3. Buffer limit is set to the end of value bytes.
  4. + *
+ * + * @param encoded buffer containing DER-encoded bytes positioned at start of length byte(s). + * @return number of bytes occupied by tag value. + */ + public int readLength(final DERBuffer encoded) { + int length = 0; + final byte b = encoded.get(); + // CheckStyle:MagicNumber OFF + if ((b & 0x80) == 0x80) { + final int len = b & 0x7F; + if (len > 0) { + final int limit = encoded.limit(); + encoded.limit(encoded.position() + len); + length = IntegerType.decodeUnsignedPrimitive(encoded); + encoded.limit(limit); + } + } else { + length = b; + } + // CheckStyle:MagicNumber ON + return length; + } + + + /** + * Reads the supplied DER encoded bytes and invokes handlers as configured paths are encountered. + * + * @param encoded to parse + */ + private void parseTags(final DERBuffer encoded) { + int index = 0; + while (encoded.position() < encoded.limit() && !handlerMap.isEmpty()) { + final DERTag tag = readTag(encoded); + if (tag != null) { + addTag(tag, index++); + parseTag(tag, encoded); + removeTag(); + } + } + } + + + /** + * Invokes the parse handler for the current path and advances to the next position in the encoded bytes. + * + * @param tag to inspect for internal tags + * @param encoded to parse + */ + private void parseTag(final DERTag tag, final DERBuffer encoded) { + final int limit = encoded.limit(); + final int end = readLength(encoded) + encoded.position(); + final int start = encoded.position(); + + // Invoke handlers for all permutations of current path + ParseHandler handler; + for (DERPath p : permutations) { + handler = handlerMap.get(p); + if (handler != null) { + encoded.limit(end).position(start); + handler.handle(this, encoded); + } + } + + if (tag.isConstructed()) { + parseTags(encoded); + } + encoded.limit(limit).position(end); + } + + + /** + * Add the given tag at the specified index to all permutations of the current parser path and increases the number of + * permutations as necessary to satisfy the following relation: + * + *
size = 2^n
+ * + *

where n is the path length.

+ * + * @param tag to add to path. + * @param index of tag relative to parent. + */ + private void addTag(final DERTag tag, final int index) { + if (permutations.isEmpty()) { + permutations.add(new DERPath().pushNode(tag.name())); + permutations.add(new DERPath().pushNode(tag.name(), index)); + } else { + final Collection generation = new ArrayDeque<>(permutations.size()); + for (DERPath p : permutations) { + generation.add(new DERPath(p).pushNode(tag.name())); + p.pushNode(tag.name(), index); + } + permutations.addAll(generation); + } + } + + + /** + * Removes the tag at the leaf position of all permutations of the current parser path, and reduces the number of + * permutations as necessary to satisfy the following relation: + * + *
size = 2^n
+ * + *

where n is the path length.

+ */ + private void removeTag() { + final int half = permutations.size() / 2; + while (permutations.size() > half) { + permutations.remove(); + } + permutations.forEach(DERPath::popNode); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/DERPath.java b/asn1/src/main/java/org/xbib/asn1/DERPath.java new file mode 100644 index 0000000..d5229f2 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/DERPath.java @@ -0,0 +1,353 @@ + +package org.xbib.asn1; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Describes paths to individual elements of an encoded DER object that may be addressed during parsing to associate a + * parsed element with a handler to handle that element. Consider the following production rule for a complex type that + * may be DER encoded: + * + *
+ *
+ * BankAccountSet ::= SET OF {
+ * account BankAccount
+ * }
+ *
+ * BankAccount ::= SEQUENCE OF {
+ * accountNumber OCTET STRING,
+ * accountName OCTET STRING,
+ * accountType AccountType,
+ * balance REAL
+ * }
+ *
+ * AccountType ::= ENUM {
+ * checking (0),
+ * savings (1)
+ * }
+ *
+ * 
+ * + *

Given an instance of BankAccountSet with two elements, the path to the balance of each bank account in the set is + * given by the following expression:

+ * + *
/SET/SEQ/REAL
+ * + *

Individual child elements can be accessed by explicitly mentioning the index of the item relative to its parent. + * For example, the second bank account in the set can be accessed as follows:

+ * + *
/SET/SEQ[1]
+ * + *

Node names in DER paths are constrained to the following:

+ * + *
    + *
  • {@link UniversalDERTag} tag names
  • + *
  • {@link ApplicationDERTag#TAG_NAME}
  • + *
  • {@link ContextDERTag#TAG_NAME}
  • + *
+ * + * @see DERParser + */ +public class DERPath { + + /** + * Separates nodes in a path specification. + */ + public static final String PATH_SEPARATOR = "/"; + + /** + * General pattern for DER path nodes. + */ + private static final Pattern NODE_PATTERN; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 601; + + static { + final StringBuilder validNames = new StringBuilder(); + validNames.append(ApplicationDERTag.TAG_NAME).append("\\(\\d+\\)|"); + validNames.append(ContextDERTag.TAG_NAME).append("\\(\\d+\\)|"); + for (UniversalDERTag tag : UniversalDERTag.values()) { + validNames.append('|').append(tag.name()); + } + NODE_PATTERN = Pattern.compile(String.format("(%s)(?:\\[(\\d+)\\])?", validNames)); + } + + /** + * Describes the path as a FIFO set of nodes. + */ + private final Deque nodeStack = new ArrayDeque<>(); + + + /** + * Creates an empty path specification. + */ + public DERPath() { + this(PATH_SEPARATOR); + } + + + /** + * Copy constructor. + * + * @param path to read nodes from + */ + public DERPath(final DERPath path) { + nodeStack.addAll(path.nodeStack); + } + + + /** + * Creates a path specification from its string representation. + * + * @param pathSpec string representation of a path, e.g. /SEQ[1]/CHOICE. + */ + public DERPath(final String pathSpec) { + final String[] nodes = pathSpec.split(PATH_SEPARATOR); + for (String node : nodes) { + if ("".equals(node)) { + continue; + } + // Normalize node names to upper case + nodeStack.add(toNode(LdapUtils.toUpperCaseAscii(node))); + } + } + + /** + * Converts a string representation of a node into a {@link Node} object. + * + * @param node String representation of node. + * @return Node corresponding to given string representation. + * @throws IllegalArgumentException for an invalid node name. + */ + static Node toNode(final String node) { + final Matcher matcher = NODE_PATTERN.matcher(node); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid node: " + node); + } + + final String name = matcher.group(1); + final String index = matcher.group(2); + if (index != null) { + return new Node(name, Integer.parseInt(index)); + } + return new Node(name); + } + + /** + * Appends a node to the path. + * + * @param name of the path element to add + * @return This instance with new node appended. + */ + public DERPath pushNode(final String name) { + nodeStack.addLast(new Node(name)); + return this; + } + + /** + * Appends a node to the path with the given child index. + * + * @param name of the path element to add + * @param index child index + * @return This instance with new node appended. + */ + public DERPath pushNode(final String name, final int index) { + nodeStack.addLast(new Node(name, index)); + return this; + } + + /** + * Examines the first node in the path without removing it. + * + * @return first node in the path or null if no nodes remain + */ + public String peekNode() { + return nodeStack.peek().toString(); + } + + /** + * Removes the last node in the path. + * + * @return last node in the path or null if no more nodes remain. + */ + public String popNode() { + if (nodeStack.isEmpty()) { + return null; + } + return nodeStack.removeLast().toString(); + } + + /** + * Gets the number of nodes in the path. + * + * @return node count. + */ + public int getSize() { + return nodeStack.size(); + } + + /** + * Determines whether the path contains any nodes. + * + * @return True if path contains 0 nodes, false otherwise. + */ + public boolean isEmpty() { + return nodeStack.isEmpty(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof DERPath v) { + return LdapUtils.areEqual( + nodeStack != null ? nodeStack.toArray() : null, + v.nodeStack != null ? v.nodeStack.toArray() : null); + } + return false; + } + + @Override + public int hashCode() { + int hc = HASH_CODE_SEED; + if (nodeStack != null && !nodeStack.isEmpty()) { + for (Node n : nodeStack) { + hc = HASH_CODE_SEED * hc + n.hashCode(); + } + } + return hc; + } + + @Override + public String toString() { + if (nodeStack == null) { + return ""; + } + final StringBuilder sb = new StringBuilder(nodeStack.size() * 10); + for (Node node : nodeStack) { + sb.append(PATH_SEPARATOR); + node.toString(sb); + } + return sb.toString(); + } + + /** + * DER path node encapsulates the path name and its location among other children that share a common parent. + * + */ + static class Node { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 607; + + /** + * Name of this node. + */ + private final String name; + + /** + * Index of this node. + */ + private final int childIndex; + + + /** + * Creates a new node with an indeterminate index. + * + * @param n name of this node + */ + Node(final String n) { + name = n; + childIndex = -1; + } + + + /** + * Creates a new node with the given index. + * + * @param n name of this node + * @param i child index location of this node in the path + */ + Node(final String n, final int i) { + if (i < 0) { + throw new IllegalArgumentException("Child index cannot be negative."); + } + name = n; + childIndex = i; + } + + + /** + * Returns the name. + * + * @return name + */ + public String getName() { + return name; + } + + + /** + * Returns the child index. + * + * @return child index + */ + public int getChildIndex() { + return childIndex; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof Node v) { + return LdapUtils.areEqual(name, v.name) && + LdapUtils.areEqual(childIndex, v.childIndex); + } + return false; + } + + + @Override + public int hashCode() { + int result = HASH_CODE_SEED + (name == null ? 0 : name.hashCode()); + result = HASH_CODE_SEED * result + childIndex; + return result; + } + + + @Override + public String toString() { + // CheckStyle:MagicNumber OFF + final StringBuilder sb = new StringBuilder(name.length() + 4); + // CheckStyle:MagicNumber ON + toString(sb); + return sb.toString(); + } + + + /** + * Appends the string representation of this instance to the given string builder. + * + * @param builder Builder to hold string representation of this instance. + */ + public void toString(final StringBuilder builder) { + builder.append(name); + if (childIndex < 0) { + return; + } + builder.append('[').append(childIndex).append(']'); + } + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/DERTag.java b/asn1/src/main/java/org/xbib/asn1/DERTag.java new file mode 100644 index 0000000..ae356d9 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/DERTag.java @@ -0,0 +1,46 @@ + +package org.xbib.asn1; + +/** + * Describes the tag of a DER-encoded type. + * + */ +public interface DERTag { + + /** + * Constructed tags should have the 6th bit set. + */ + int ASN_CONSTRUCTED = 0x20; + + + /** + * Gets the decimal value of the tag. + * + * @return decimal tag number. + */ + int getTagNo(); + + + /** + * Gets the name of the tag. + * + * @return tag name. + */ + String name(); + + + /** + * Determines whether the tag is constructed or primitive. + * + * @return true if constructed, false if primitive. + */ + boolean isConstructed(); + + + /** + * Gets the value of this tag for encoding. + * + * @return byte value of this tag + */ + int getTagByte(); +} diff --git a/asn1/src/main/java/org/xbib/asn1/DefaultDERBuffer.java b/asn1/src/main/java/org/xbib/asn1/DefaultDERBuffer.java new file mode 100644 index 0000000..650b84b --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/DefaultDERBuffer.java @@ -0,0 +1,127 @@ + +package org.xbib.asn1; + +import java.nio.ByteBuffer; + +/** + * {@link DERBuffer} that uses a {@link ByteBuffer}. + * + */ +public class DefaultDERBuffer implements DERBuffer { + + /** + * Underlying byte buffer. + */ + private final ByteBuffer buffer; + + + /** + * Creates a new default DER buffer. See {@link ByteBuffer#allocate(int)}. + * + * @param capacity of this buffer + */ + public DefaultDERBuffer(final int capacity) { + buffer = ByteBuffer.allocate(capacity); + } + + + /** + * Creates a new default DER buffer. See {@link ByteBuffer#wrap(byte[])}. + * + * @param array contents of the buffer + */ + public DefaultDERBuffer(final byte[] array) { + buffer = ByteBuffer.wrap(array); + } + + + /** + * Creates a new default DER buffer. + * + * @param buf existing byte buffer + */ + public DefaultDERBuffer(final ByteBuffer buf) { + buffer = buf; + } + + + /** + * Creates a new default DER buffer and sets the initial position and limit. + * + * @param buf existing byte buffer + * @param pos initial buffer position + * @param lim initial buffer limit + */ + public DefaultDERBuffer(final ByteBuffer buf, final int pos, final int lim) { + buffer = buf; + buffer.position(pos); + buffer.limit(lim); + } + + + @Override + public int position() { + return buffer.position(); + } + + + @Override + public DERBuffer position(final int newPosition) { + buffer.position(newPosition); + return this; + } + + + @Override + public int limit() { + return buffer.limit(); + } + + + @Override + public int capacity() { + return buffer.capacity(); + } + + + @Override + public DERBuffer limit(final int newLimit) { + buffer.limit(newLimit); + return this; + } + + + @Override + public DERBuffer clear() { + buffer.clear(); + return this; + } + + + @Override + public byte get() { + return buffer.get(); + } + + + @Override + public DERBuffer get(final byte[] dst) { + buffer.get(dst); + return this; + } + + + @Override + public DERBuffer slice() { + return new DefaultDERBuffer(buffer.slice()); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "pos=" + position() + ", " + + "lim=" + limit() + ", " + + "cap=" + capacity(); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/IntegerType.java b/asn1/src/main/java/org/xbib/asn1/IntegerType.java new file mode 100644 index 0000000..f6f8c42 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/IntegerType.java @@ -0,0 +1,133 @@ + +package org.xbib.asn1; + +import java.math.BigInteger; + +/** + * Converts arbitrary-precision integers to and from their DER encoded format. + * + */ +public class IntegerType extends AbstractDERType implements DEREncoder { + + /** + * Integer to encode. + */ + private final byte[] derItem; + + + /** + * Creates a new integer type. + * + * @param item to DER encode + */ + public IntegerType(final BigInteger item) { + super(UniversalDERTag.INT); + derItem = item.toByteArray(); + } + + + /** + * Creates a new integer type. + * + * @param item to DER encode + */ + public IntegerType(final int item) { + super(UniversalDERTag.INT); + derItem = BigInteger.valueOf(item).toByteArray(); + } + + + /** + * Creates a new integer type. + * + * @param tag der tag associated with this type + * @param item to DER encode + * @throws IllegalArgumentException if the der tag is constructed + */ + public IntegerType(final DERTag tag, final BigInteger item) { + super(tag); + if (tag.isConstructed()) { + throw new IllegalArgumentException("DER tag must not be constructed"); + } + derItem = item.toByteArray(); + } + + + /** + * Creates a new integer type. + * + * @param tag der tag associated with this type + * @param item to DER encode + * @throws IllegalArgumentException if the der tag is constructed + */ + public IntegerType(final DERTag tag, final int item) { + super(tag); + if (tag.isConstructed()) { + throw new IllegalArgumentException("DER tag must not be constructed"); + } + derItem = BigInteger.valueOf(item).toByteArray(); + } + + /** + * Converts bytes in the buffer to an integer by reading from the current position to the limit, which assumes the + * bytes of the integer are in big-endian order. + * + * @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of integer bytes + * and the limit is set beyond the last byte of integer data. + * @return decoded bytes as an integer of arbitrary size. + */ + public static BigInteger decode(final DERBuffer encoded) { + return new BigInteger(encoded.getRemainingBytes()); + } + + /** + * Converts bytes in the buffer to an unsigned integer by reading from the current position to the limit, which + * assumes the bytes of the integer are in big-endian order. + * + * @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of integer bytes + * and the limit is set beyond the last byte of integer data. + * @return decoded bytes as an unsigned integer of arbitrary size. + */ + public static BigInteger decodeUnsigned(final DERBuffer encoded) { + return new BigInteger(1, encoded.getRemainingBytes()); + } + + /** + * Converts bytes in the buffer to an unsigned primitive integer by reading from the current position to the limit, + * which assumes the bytes of the integer are in big-endian order. This method reads up to 4 bytes from the buffer. + * + * @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of integer bytes + * and the limit is set beyond the last byte of integer data. + * @return decoded bytes as an unsigned integer. + * @throws IllegalArgumentException if the buffer contains more than 4 bytes + */ + public static int decodeUnsignedPrimitive(final DERBuffer encoded) { + // CheckStyle:MagicNumber OFF + final byte[] bytes = encoded.getRemainingBytes(); + if (bytes.length > 4) { + throw new IllegalArgumentException("Buffer length must be <= 4 bytes"); + } + int i = 0; + for (byte b : bytes) { + i <<= 8; + i |= b & 0xFF; + } + return i; + // CheckStyle:MagicNumber ON + } + + /** + * Converts the supplied big integer to a byte array. + * + * @param i to convert + * @return byte array + */ + public static byte[] toBytes(final BigInteger i) { + return i.toByteArray(); + } + + @Override + public byte[] encode() { + return encode(derItem); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/LdapUtils.java b/asn1/src/main/java/org/xbib/asn1/LdapUtils.java new file mode 100644 index 0000000..e44fb59 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/LdapUtils.java @@ -0,0 +1,508 @@ +package org.xbib.asn1; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Queue; +import java.util.regex.Pattern; + +/** + * Provides utility methods for this package. + * + */ +public final class LdapUtils { + + /** + * Size of buffer in bytes to use when reading files. + */ + private static final int READ_BUFFER_SIZE = 128; + + /** + * Prime number to assist in calculating hash codes. + */ + private static final int HASH_CODE_PRIME = 113; + + /** + * Pattern to match ipv4 addresses. + */ + private static final Pattern IPV4_PATTERN = Pattern.compile( + "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)" + + "(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$"); + + /** + * Pattern to match ipv6 addresses. + */ + private static final Pattern IPV6_STD_PATTERN = Pattern.compile("^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$"); + + /** + * Pattern to match ipv6 hex compressed addresses. + */ + private static final Pattern IPV6_HEX_COMPRESSED_PATTERN = Pattern.compile( + "^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::" + + "((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$"); + + /** + * Pattern that matches control characters. + */ + private static final Pattern CNTRL_PATTERN = Pattern.compile("\\p{Cntrl}"); + + + /** + * Default constructor. + */ + private LdapUtils() { + } + + + /** + * This will convert the supplied value to a base64 encoded string. Returns null if the supplied byte array is null. + * + * @param value to base64 encode + * @return base64 encoded value + */ + public static String base64Encode(final byte... value) { + return value != null ? new String(Base64.getEncoder().encode(value), StandardCharsets.UTF_8) : null; + } + + + /** + * This will convert the supplied value to a base64 encoded string. Returns null if the supplied string is null. + * + * @param value to base64 encode + * @return base64 encoded value + */ + public static String base64Encode(final String value) { + return value != null ? base64Encode(value.getBytes(StandardCharsets.UTF_8)) : null; + } + + + /** + * This will convert the supplied value to a UTF-8 encoded string. Returns null if the supplied byte array is null. + * + * @param value to UTF-8 encode + * @return UTF-8 encoded value + */ + public static String utf8Encode(final byte[] value) { + return utf8Encode(value, true); + } + + + /** + * This will convert the supplied value to a UTF-8 encoded string. + * + * @param value to UTF-8 encode + * @param allowNull whether to throw {@link NullPointerException} if value is null + * @return UTF-8 encoded value + * @throws NullPointerException if allowNull is false and value is null + */ + public static String utf8Encode(final byte[] value, final boolean allowNull) { + if (!allowNull && value == null) { + throw new NullPointerException("Cannot UTF-8 encode null value"); + } + return value != null ? new String(value, StandardCharsets.UTF_8) : null; + } + + + /** + * This will convert the supplied value to a UTF-8 encoded byte array. Returns null if the supplied string is null. + * + * @param value to UTF-8 encode + * @return UTF-8 encoded value + */ + public static byte[] utf8Encode(final String value) { + return utf8Encode(value, true); + } + + + /** + * This will convert the supplied value to a UTF-8 encoded byte array. + * + * @param value to UTF-8 encode + * @param allowNull whether to throw {@link NullPointerException} if value is null + * @return UTF-8 encoded value + * @throws NullPointerException if allowNull is false and value is null + */ + public static byte[] utf8Encode(final String value, final boolean allowNull) { + if (!allowNull && value == null) { + throw new NullPointerException("Cannot UTF-8 encode null value"); + } + return value != null ? value.getBytes(StandardCharsets.UTF_8) : null; + } + + /** + * Removes the space character from both the beginning and end of the supplied value. + * + * @param value to trim space character from + * @return trimmed value or same value if no trim was performed + */ + public static String trimSpace(final String value) { + if (value == null || value.isEmpty()) { + return value; + } + + int startIndex = 0; + int endIndex = value.length(); + while (startIndex < endIndex && value.charAt(startIndex) == ' ') { + startIndex++; + } + while (startIndex < endIndex && value.charAt(endIndex - 1) == ' ') { + endIndex--; + } + if (startIndex == 0 && endIndex == value.length()) { + return value; + } + return value.substring(startIndex, endIndex); + } + + + /** + * Changes the supplied value by replacing multiple spaces with a single space. + * + * @param value to compress spaces + * @param trim whether to remove any leading or trailing space characters + * @return normalized value or value if no compress was performed + */ + public static String compressSpace(final String value, final boolean trim) { + if (value == null || value.isEmpty()) { + return value; + } + + final StringBuilder sb = new StringBuilder(); + boolean foundSpace = false; + for (int i = 0; i < value.length(); i++) { + final char ch = value.charAt(i); + if (ch == ' ') { + if (i == value.length() - 1) { + // last char is a space + sb.append(ch); + } + foundSpace = true; + } else { + if (foundSpace) { + sb.append(' '); + } + sb.append(ch); + foundSpace = false; + } + } + + if (sb.length() == 0 && foundSpace) { + return trim ? "" : " "; + } + if (trim) { + if (sb.length() > 0 && sb.charAt(0) == ' ') { + sb.deleteCharAt(0); + } + if (sb.length() > 0 && sb.charAt(sb.length() - 1) == ' ') { + sb.deleteCharAt(sb.length() - 1); + } + } + return sb.toString(); + } + + + /** + * This will decode the supplied value as a base64 encoded string to a byte[]. Returns null if the supplied string is + * null. + * + * @param value to base64 decode + * @return base64 decoded value + */ + public static byte[] base64Decode(final String value) { + try { + return value != null ? Base64.getDecoder().decode(value.getBytes(StandardCharsets.UTF_8)) : null; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error decoding value: " + value, e); + } + } + + /** + * Converts the supplied string to lower case. If the string contains non-ascii characters, {@link Locale#ROOT} is + * used. + * + * @param s to lower case + * @return new lower case string + */ + public static String toLowerCase(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + // CheckStyle:MagicNumber OFF + // if string contains non-ascii, use locale specific lowercase + if (s.chars().anyMatch(c -> c > 0x7F)) { + return s.toLowerCase(Locale.ROOT); + } + return toLowerCaseAscii(s); + } + + + /** + * Converts the characters A-Z to a-z. + * + * @param s to lower case + * @return new string with lower case alphabetical characters + * @throws IllegalArgumentException if the supplied string contains non-ascii characters + */ + public static String toLowerCaseAscii(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + // mutate A-Z to a-z + // CheckStyle:MagicNumber OFF + final char[] chars = s.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (chars[i] > 0x7F) { + throw new IllegalArgumentException("String contains non-ascii characters: " + s); + } else if (chars[i] >= 'A' && chars[i] <= 'Z') { + chars[i] = (char) (chars[i] + 32); + } + } + // CheckStyle:MagicNumber ON + return new String(chars); + } + + + /** + * Converts the supplied string to upper case. If the string contains non-ascii characters, {@link Locale#ROOT} is + * used. + * + * @param s to upper case + * @return new upper case string + */ + public static String toUpperCase(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + // CheckStyle:MagicNumber OFF + // if string contains non-ascii, use locale specific uppercase + if (s.chars().anyMatch(c -> c > 0x7F)) { + return s.toUpperCase(Locale.ROOT); + } + return toUpperCaseAscii(s); + } + + + /** + * Converts the characters a-z to A-Z. + * + * @param s to upper case + * @return new string with upper case alphabetical characters + * @throws IllegalArgumentException if the supplied string contains non-ascii characters + */ + public static String toUpperCaseAscii(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + // mutate a-z to A-Z + // CheckStyle:MagicNumber OFF + final char[] chars = s.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (chars[i] > 0x7F) { + throw new IllegalArgumentException("String contains non-ascii characters: " + s); + } else if (chars[i] >= 'a' && chars[i] <= 'z') { + chars[i] = (char) (chars[i] - 32); + } + } + // CheckStyle:MagicNumber ON + return new String(chars); + } + + + /** + * Reads the data in the supplied stream and returns it as a byte array. + * + * @param is stream to read + * @return bytes read from the stream + * @throws IOException if an error occurs reading data + */ + public static byte[] readInputStream(final InputStream is) + throws IOException { + final ByteArrayOutputStream data = new ByteArrayOutputStream(); + try (is; data) { + final byte[] buffer = new byte[READ_BUFFER_SIZE]; + int length; + while ((length = is.read(buffer)) != -1) { + data.write(buffer, 0, length); + } + } + return data.toByteArray(); + } + + + /** + * Concatenates multiple arrays together. + * + * @param type of array + * @param first array to concatenate. Cannot be null. + * @param rest of the arrays to concatenate. May be null. + * @return array containing the concatenation of all parameters + */ + @SuppressWarnings("unchecked") + public static T[] concatArrays(final T[] first, final T[]... rest) { + int totalLength = first.length; + for (T[] array : rest) { + if (array != null) { + totalLength += array.length; + } + } + + final T[] result = Arrays.copyOf(first, totalLength); + + int offset = first.length; + for (T[] array : rest) { + if (array != null) { + System.arraycopy(array, 0, result, offset, array.length); + offset += array.length; + } + } + return result; + } + + + /** + * Determines equality of the supplied objects. Array types are automatically detected. + * + * @param o1 to test equality of + * @param o2 to test equality of + * @return whether o1 equals o2 + */ + public static boolean areEqual(final Object o1, final Object o2) { + if (o1 == o2) { + return true; + } + final boolean areEqual; + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + areEqual = Arrays.equals((boolean[]) o1, (boolean[]) o2); + } else if (o1 instanceof byte[] && o2 instanceof byte[]) { + areEqual = Arrays.equals((byte[]) o1, (byte[]) o2); + } else if (o1 instanceof char[] && o2 instanceof char[]) { + areEqual = Arrays.equals((char[]) o1, (char[]) o2); + } else if (o1 instanceof double[] && o2 instanceof double[]) { + areEqual = Arrays.equals((double[]) o1, (double[]) o2); + } else if (o1 instanceof float[] && o2 instanceof float[]) { + areEqual = Arrays.equals((float[]) o1, (float[]) o2); + } else if (o1 instanceof int[] && o2 instanceof int[]) { + areEqual = Arrays.equals((int[]) o1, (int[]) o2); + } else if (o1 instanceof long[] && o2 instanceof long[]) { + areEqual = Arrays.equals((long[]) o1, (long[]) o2); + } else if (o1 instanceof short[] && o2 instanceof short[]) { + areEqual = Arrays.equals((short[]) o1, (short[]) o2); + } else if (o1 instanceof Object[] && o2 instanceof Object[]) { + areEqual = Arrays.deepEquals((Object[]) o1, (Object[]) o2); + } else { + areEqual = o1 != null && o1.equals(o2); + } + return areEqual; + } + + + /** + * Computes a hash code for the supplied objects using the supplied seed. If a Collection type is found it is iterated + * over. + * + * @param seed odd/prime number + * @param objects to calculate hashCode for + * @return hash code for the supplied objects + */ + public static int computeHashCode(final int seed, final Object... objects) { + if (objects == null || objects.length == 0) { + return seed * HASH_CODE_PRIME; + } + + int hc = seed; + for (Object object : objects) { + hc = HASH_CODE_PRIME * hc; + if (object != null) { + if (object instanceof List || object instanceof Queue) { + int index = 1; + for (Object o : (Collection) object) { + hc += computeHashCode(o) * index++; + } + } else if (object instanceof Collection) { + for (Object o : (Collection) object) { + hc += computeHashCode(o); + } + } else { + hc += computeHashCode(object); + } + } + } + return hc; + } + + + /** + * Computes a hash code for the supplied object. Checks for arrays of primitives and Objects then delegates to the + * {@link Arrays} class. Otherwise {@link Object#hashCode()} is invoked. + * + * @param object to calculate hash code for + * @return hash code + */ + private static int computeHashCode(final Object object) { + int hc = 0; + if (object instanceof boolean[]) { + hc += Arrays.hashCode((boolean[]) object); + } else if (object instanceof byte[]) { + hc += Arrays.hashCode((byte[]) object); + } else if (object instanceof char[]) { + hc += Arrays.hashCode((char[]) object); + } else if (object instanceof double[]) { + hc += Arrays.hashCode((double[]) object); + } else if (object instanceof float[]) { + hc += Arrays.hashCode((float[]) object); + } else if (object instanceof int[]) { + hc += Arrays.hashCode((int[]) object); + } else if (object instanceof long[]) { + hc += Arrays.hashCode((long[]) object); + } else if (object instanceof short[]) { + hc += Arrays.hashCode((short[]) object); + } else if (object instanceof Object[]) { + hc += Arrays.deepHashCode((Object[]) object); + } else { + hc += object.hashCode(); + } + return hc; + } + + + /** + * Returns whether the supplied string represents an IP address. Matches both IPv4 and IPv6 addresses. + * + * @param s to match + * @return whether the supplied string represents an IP address + */ + public static boolean isIPAddress(final String s) { + return + s != null && + (IPV4_PATTERN.matcher(s).matches() || IPV6_STD_PATTERN.matcher(s).matches() || + IPV6_HEX_COMPRESSED_PATTERN.matcher(s).matches()); + } + + + /** + * Looks for the supplied system property value and loads a class with that name. The default constructor for that + * class is then returned. + * + * @param property whose value is a class + * @return class constructor or null if no system property was found + * @throws IllegalArgumentException if an error occurs instantiating the constructor + */ + public static Constructor createConstructorFromProperty(final String property) { + final String clazz = System.getProperty(property); + if (clazz != null) { + try { + return Class.forName(clazz).getDeclaredConstructor(); + } catch (Exception e) { + throw new IllegalArgumentException("Error getting declared constructor for " + clazz, e); + } + } + return null; + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/NullType.java b/asn1/src/main/java/org/xbib/asn1/NullType.java new file mode 100644 index 0000000..4e85e16 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/NullType.java @@ -0,0 +1,25 @@ + +package org.xbib.asn1; + +/** + * Convenience type for a tag with a null value. + * + */ +public class NullType extends AbstractDERType implements DEREncoder { + + + /** + * Creates a new null type. + * + * @param tag der tag associated with this type + */ + public NullType(final DERTag tag) { + super(tag); + } + + + @Override + public byte[] encode() { + return encode((byte[]) null); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/OctetStringType.java b/asn1/src/main/java/org/xbib/asn1/OctetStringType.java new file mode 100644 index 0000000..66eff8e --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/OctetStringType.java @@ -0,0 +1,90 @@ + +package org.xbib.asn1; + +/** + * Converts strings to and from their DER encoded format. + * + */ +public class OctetStringType extends AbstractDERType implements DEREncoder { + + /** + * String to encode. + */ + private final byte[] derItem; + + + /** + * Creates a new octet string type. + * + * @param item to DER encode + */ + public OctetStringType(final String item) { + this(LdapUtils.utf8Encode(item, false)); + } + + + /** + * Creates a new octet string type. + * + * @param item to DER encode + */ + public OctetStringType(final byte[] item) { + super(UniversalDERTag.OCTSTR); + derItem = item; + } + + + /** + * Creates a new octet string type. + * + * @param tag der tag associated with this type + * @param item to DER encode + * @throws IllegalArgumentException if the der tag is constructed + */ + public OctetStringType(final DERTag tag, final String item) { + this(tag, LdapUtils.utf8Encode(item, false)); + } + + + /** + * Creates a new octet string type. + * + * @param tag der tag associated with this type + * @param item to DER encode + * @throws IllegalArgumentException if the der tag is constructed + */ + public OctetStringType(final DERTag tag, final byte[] item) { + super(tag); + if (tag.isConstructed()) { + throw new IllegalArgumentException("DER tag must not be constructed"); + } + derItem = item; + } + + /** + * Converts bytes in the buffer to a string by reading from the current position to the limit, which assumes the bytes + * of the string are in big-endian order. + * + * @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of string bytes and + * the limit is set beyond the last byte of string data. + * @return decoded bytes as an string + */ + public static String decode(final DERBuffer encoded) { + return LdapUtils.utf8Encode(encoded.getRemainingBytes(), false); + } + + /** + * Converts the supplied string to a byte array using the UTF-8 encoding. + * + * @param s to convert + * @return byte array + */ + public static byte[] toBytes(final String s) { + return LdapUtils.utf8Encode(s, false); + } + + @Override + public byte[] encode() { + return encode(derItem); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/OidType.java b/asn1/src/main/java/org/xbib/asn1/OidType.java new file mode 100644 index 0000000..018c61d --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/OidType.java @@ -0,0 +1,248 @@ + +package org.xbib.asn1; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.StringTokenizer; + +/** + * Converts object identifiers to and from their DER encoded format. + * + */ +public class OidType extends AbstractDERType implements DEREncoder { + + /** + * Integer to encode. + */ + private final byte[] derItem; + + + /** + * Creates a new oid type. + * + * @param item to DER encode + */ + public OidType(final String item) { + super(UniversalDERTag.OID); + derItem = toBytes(parse(item)); + } + + + /** + * Creates a new oid type. + * + * @param item to DER encode + */ + public OidType(final int[] item) { + super(UniversalDERTag.OID); + derItem = toBytes(item); + } + + + /** + * Creates a new oid type. + * + * @param tag der tag associated with this type + * @param item to DER encode + * @throws IllegalArgumentException if the der tag is constructed + */ + public OidType(final DERTag tag, final String item) { + super(tag); + if (tag.isConstructed()) { + throw new IllegalArgumentException("DER tag must not be constructed"); + } + derItem = toBytes(parse(item)); + } + + + /** + * Creates a new oid type. + * + * @param tag der tag associated with this type + * @param item to DER encode + * @throws IllegalArgumentException if the der tag is constructed + */ + public OidType(final DERTag tag, final int[] item) { + super(tag); + if (tag.isConstructed()) { + throw new IllegalArgumentException("DER tag must not be constructed"); + } + derItem = toBytes(item); + } + + /** + * Converts bytes in the buffer to an OID by reading from the current position to the limit, which assumes the bytes + * of the integer are in big-endian order. + * + * @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of OID bytes and + * the limit is set beyond the last byte of OID data. + * @return decoded bytes as an OID. + */ + public static String decode(final DERBuffer encoded) { + final StringBuilder sb = new StringBuilder(); + final int firstId = encoded.get(); + // CheckStyle:MagicNumber OFF + if (firstId < 40) { + sb.append("0").append(".").append(firstId).append("."); + } else if (firstId < 80) { + sb.append("1").append(".").append(firstId - 40).append("."); + } else { + sb.append("2").append(".").append(firstId - 80).append("."); + } + // CheckStyle:MagicNumber ON + while (encoded.hasRemaining()) { + sb.append(readInt(encoded)).append("."); + } + sb.setLength(sb.length() - 1); + return sb.toString(); + } + + /** + * Converts the supplied list of oid components to a byte array. + * + * @param oid to convert + * @return byte array + * @throws IllegalArgumentException if the oid is not valid. See {@link #isValid(int[])} + */ + public static byte[] toBytes(final int[] oid) { + isValid(oid); + + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try { + try { + // CheckStyle:MagicNumber OFF + if (oid[0] < 2) { + // should always fit into one byte, since oid[1] must be <= 38 + bytes.write((oid[0] * 40) + oid[1]); + } else { + bytes.write(toBytes((oid[0] * 40) + oid[1])); + } + for (int i = 2; i < oid.length; i++) { + bytes.write(toBytes(oid[i])); + } + // CheckStyle:MagicNumber ON + } finally { + bytes.close(); + } + } catch (IOException e) { + throw new IllegalStateException("Byte conversion failed", e); + } + return bytes.toByteArray(); + } + + /** + * Checks whether the supplied oid is valid. Oids must meet the following criteria: + * + *
    + *
  • must not be null and must have at least 2 elements
  • + *
  • components must not be negative
  • + *
  • first component must be 0, 1, or 2
  • + *
  • if first component 0 or 1, second component must be <= 38
  • + *
+ * + * @param oid to check + * @throws IllegalArgumentException if the oid is not valid. + */ + protected static void isValid(final int[] oid) { + // CheckStyle:MagicNumber OFF + if (oid == null || oid.length < 2) { + throw new IllegalArgumentException("OIDs must have at least two components"); + } + if (oid[0] < 0 || oid[0] > 2) { + throw new IllegalArgumentException("The first OID must be 0, 1, or 2"); + } + if (oid[0] < 2 && oid[1] > 39) { + throw new IllegalArgumentException("The second OID must be less than or equal to 38"); + } + for (int i : oid) { + if (i < 0) { + throw new IllegalArgumentException("OIDs cannot be negative"); + } + } + // CheckStyle:MagicNumber ON + } + + /** + * Converts the supplied oid component to a byte array. The length of the byte array is the minimal size needed to + * contain the oid component. + * + * @param component to convert to bytes + * @return oid bytes + */ + protected static byte[] toBytes(final int component) { + // CheckStyle:MagicNumber OFF + final byte[] buffer = new byte[4]; + int size = 0; + int val = component; + while (val != 0) { + if (size > 0) { + buffer[size++] = (byte) ((val & 0x7F) | 0x80); + } else { + buffer[size++] = (byte) (val & 0x7F); + } + val >>>= 7; + } + + final byte[] bytes = new byte[size]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = buffer[--size]; + } + return bytes; + // CheckStyle:MagicNumber ON + } + + /** + * Reads the necessary encoded bytes from the supplied buffer to create an integer. + * + * @param buffer to read + * @return OID component integer + */ + protected static int readInt(final DERBuffer buffer) { + // CheckStyle:MagicNumber OFF + int val = 0; + for (int i = 0; i < 4; i++) { + final byte b = buffer.get(); + if (i == 0 && b == 0x80) { + throw new IllegalArgumentException("Component starts with 0x80"); + } + val <<= 7; + val |= b & 0x7F; + if ((b & 0x80) == 0) { + return val; + } + } + // CheckStyle:MagicNumber ON + throw new IllegalArgumentException("Integer greater than 4 bytes in size"); + } + + /** + * Converts the supplied oid into an array on integers. + * + * @param oid to parse + * @return array of oid components + * @throws IllegalArgumentException if the oid is not valid. See {@link #isValid(int[])} + */ + public static int[] parse(final String oid) { + if (oid == null) { + throw new IllegalArgumentException("OID cannot be null"); + } + + final StringTokenizer st = new StringTokenizer(oid, "."); + final int[] oids = new int[st.countTokens()]; + int i = 0; + while (st.hasMoreTokens()) { + try { + oids[i++] = Integer.parseInt(st.nextToken()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(e); + } + } + isValid(oids); + return oids; + } + + @Override + public byte[] encode() { + return encode(derItem); + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/ParseHandler.java b/asn1/src/main/java/org/xbib/asn1/ParseHandler.java new file mode 100644 index 0000000..e6807c5 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/ParseHandler.java @@ -0,0 +1,18 @@ + +package org.xbib.asn1; + +/** + * Provides a hook in the DER parser for handling specific paths as they are encountered. + * + */ +public interface ParseHandler { + + + /** + * Invoked when a DER path is encountered that belongs to this parse handler. + * + * @param parser that invoked this handler + * @param encoded to handle + */ + void handle(DERParser parser, DERBuffer encoded); +} diff --git a/asn1/src/main/java/org/xbib/asn1/UniversalDERTag.java b/asn1/src/main/java/org/xbib/asn1/UniversalDERTag.java new file mode 100644 index 0000000..866e671 --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/UniversalDERTag.java @@ -0,0 +1,248 @@ + +package org.xbib.asn1; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration with common BER/DER universal tag types. + * + */ +public enum UniversalDERTag implements DERTag { + + /** + * BOOLEAN type. + */ + BOOL(1, false), + + /** + * INTEGER type. + */ + INT(2, false), + + /** + * BITSTRING type. + */ + BITSTR(3, false), + + /** + * OCTETSTRING type. + */ + OCTSTR(4, false), + + /** + * NULL type. + */ + NULL(5, false), + + /** + * OBJECT IDENTIFIER type. + */ + OID(6, false), + + /** + * ObjectDescriptor type. + */ + ODESC(7, false), + + /** + * EXTERNAL type. + */ + EXT(8, false), + + /** + * REAL type. + */ + REAL(9, false), + + /** + * ENUMERATED type. + */ + ENUM(10, false), + + /** + * EMBEDDED PDV type. + */ + EMBPDV(11, false), + + /** + * UTF8String type. + */ + UTF8STR(12, false), + + /** + * RELATIVE-OID type. + */ + ROID(13, false), + + /** + * SEQUENCE type. + */ + SEQ(16, true), + + /** + * SET type. + */ + SET(17, true), + + /** + * NumericString type. + */ + NUMSTR(18, false), + + /** + * PrintableString type. + */ + PRINTSTR(19, false), + + /** + * T61String type. + */ + T61STR(20, false), + + /** + * VideotexString type. + */ + VTEXSTR(21, false), + + /** + * IA5String type. + */ + IA5STR(22, false), + + /** + * UTCTime type. + */ + UTCTIME(23, false), + + /** + * GeneralizedTime type. + */ + GENTIME(24, false), + + /** + * GraphicString type. + */ + GRAPHICSTR(25, false), + + /** + * ISO646String type. + */ + ISO646STR(26, false), + + /** + * GeneralString type. + */ + GENSTR(27, false), + + /** + * UniversalString type. + */ + UNISTR(28, false), + + /** + * CharacterString type. + */ + CHARSTR(29, false), + + /** + * BMPString type. + */ + BMPSTR(30, false); + + /** + * Universal tag class is 00b in first two high-order bytes. + */ + public static final int TAG_CLASS = 0; + + /** + * Maps tag numbers to tags. + */ + private static final Map TAGNO_MAP = new HashMap<>(); + + /** + * Maps tag names to tags. + */ + private static final Map TAGNAME_MAP = new HashMap<>(); + + static { + // Initializes tag mapping + for (UniversalDERTag tag : UniversalDERTag.values()) { + TAGNO_MAP.put(tag.getTagNo(), tag); + TAGNAME_MAP.put(tag.name(), tag); + } + } + + /** + * Tag number. + */ + private final int tagNo; + + /** + * Flag indicating whether value is primitive or constructed. + */ + private final boolean constructed; + + + /** + * Creates a new universal DER tag. + * + * @param number of the tag + * @param isConstructed whether this tag is primitive or constructed + */ + UniversalDERTag(final int number, final boolean isConstructed) { + tagNo = number; + constructed = isConstructed; + } + + /** + * Looks up a universal tag from a tag number. + * + * @param number tag number. + * @return tag object corresponding to given number. + * @throws IllegalArgumentException if tag is unknown + */ + public static UniversalDERTag fromTagNo(final int number) { + final UniversalDERTag derTag = TAGNO_MAP.get(number); + if (derTag == null) { + throw new IllegalArgumentException("Unknown tag number: " + number); + } + return derTag; + } + + /** + * Looks up a universal tag from a tag name. This method differs from {@link #valueOf(String)} in that it does not + * throw for unknown names. + * + * @param name tag name. + * @return tag object corresponding to given name or null if no tag of the given name is found. + */ + public static UniversalDERTag fromTagName(final String name) { + return TAGNAME_MAP.get(name); + } + + /** + * Gets the decimal value of the tag. + * + * @return decimal tag number. + */ + @Override + public int getTagNo() { + return tagNo; + } + + /** + * Determines whether the tag is constructed or primitive. + * + * @return true if constructed, false if primitive. + */ + @Override + public boolean isConstructed() { + return constructed; + } + + @Override + public int getTagByte() { + return constructed ? tagNo | ASN_CONSTRUCTED : tagNo; + } +} diff --git a/asn1/src/main/java/org/xbib/asn1/UuidType.java b/asn1/src/main/java/org/xbib/asn1/UuidType.java new file mode 100644 index 0000000..e4a670f --- /dev/null +++ b/asn1/src/main/java/org/xbib/asn1/UuidType.java @@ -0,0 +1,100 @@ + +package org.xbib.asn1; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * Converts UUIDs to and from their DER encoded format. See RFC 4122. + * + */ +public class UuidType extends AbstractDERType implements DEREncoder { + + /** + * Number of bytes in a uuid. + */ + private static final int UUID_LENGTH = 16; + + /** + * UUID to encode. + */ + private final byte[] derItem; + + + /** + * Creates a new uuid type. + * + * @param item to DER encode + */ + public UuidType(final UUID item) { + super(UniversalDERTag.OCTSTR); + derItem = toBytes(item); + } + + + /** + * Creates a new uuid type. + * + * @param tag der tag associated with this type + * @param item to DER encode + * @throws IllegalArgumentException if the der tag is constructed + */ + public UuidType(final DERTag tag, final UUID item) { + super(tag); + if (tag.isConstructed()) { + throw new IllegalArgumentException("DER tag must not be constructed"); + } + derItem = toBytes(item); + } + + /** + * Converts bytes in the buffer to a uuid by reading from the current position to the limit. + * + * @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of uuid bytes and + * the limit is set beyond the last byte of uuid data. + * @return decoded bytes as a uuid. + */ + public static UUID decode(final DERBuffer encoded) { + final long mostSig = readLong(encoded); + final long leastSig = readLong(encoded); + return new UUID(mostSig, leastSig); + } + + /** + * Reads the next 8 bytes from the supplied buffer to create a long. + * + * @param buffer to read + * @return UUID component integer + */ + protected static long readLong(final DERBuffer buffer) { + // CheckStyle:MagicNumber OFF + return + (((long) buffer.get()) << 56) | + (((long) buffer.get() & 0xff) << 48) | + (((long) buffer.get() & 0xff) << 40) | + (((long) buffer.get() & 0xff) << 32) | + (((long) buffer.get() & 0xff) << 24) | + (((long) buffer.get() & 0xff) << 16) | + (((long) buffer.get() & 0xff) << 8) | + (((long) buffer.get() & 0xff)); + // CheckStyle:MagicNumber ON + } + + /** + * Converts the supplied uuid to a byte array. + * + * @param uuid to convert + * @return byte array + */ + public static byte[] toBytes(final UUID uuid) { + final ByteBuffer buffer = ByteBuffer.wrap(new byte[UUID_LENGTH]); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + return buffer.array(); + } + + @Override + public byte[] encode() { + return encode(derItem); + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cd33a7c --- /dev/null +++ b/build.gradle @@ -0,0 +1,36 @@ + +plugins { + id 'maven-publish' + id 'signing' + id "io.github.gradle-nexus.publish-plugin" version "2.0.0-rc-1" +} + +wrapper { + gradleVersion = libs.versions.gradle.get() + distributionType = Wrapper.DistributionType.BIN +} + +ext { + user = 'joerg' + name = 'net-ldap' + description = 'LDAP classes for Java' + inceptionYear = '2034' + url = 'https://xbib.org/' + user + '/' + name + scmUrl = 'https://xbib.org/' + user + '/' + name + scmConnection = 'scm:git:git://xbib.org/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:ssh://forgejo@xbib.org:' + user + '/' + name + '.git' + issueManagementSystem = 'Github' + issueManagementUrl = ext.scmUrl + '/issues' + licenseName = 'The Apache License, Version 2.0' + licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' +} + +subprojects { + apply from: rootProject.file('gradle/ide/idea.gradle') + apply from: rootProject.file('gradle/repositories/maven.gradle') + apply from: rootProject.file('gradle/compile/java.gradle') + apply from: rootProject.file('gradle/test/junit5.gradle') + apply from: rootProject.file('gradle/publish/maven.gradle') +} +apply from: rootProject.file('gradle/publish/sonatype.gradle') +apply from: rootProject.file('gradle/publish/forgejo.gradle') diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..abb7ec0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +group = org.xbib +name = net-ldap +version = 1.0.0 diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle new file mode 100644 index 0000000..fbf9679 --- /dev/null +++ b/gradle/compile/java.gradle @@ -0,0 +1,45 @@ +apply plugin: 'java-library' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + modularity.inferModulePath.set(true) + withSourcesJar() + withJavadocJar() +} + +jar { + manifest { + attributes('Implementation-Version': project.version) + } + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +tasks.withType(JavaCompile) { + doFirst { + options.fork = true + options.forkOptions.jvmArgs += ['-Duser.language=en','-Duser.country=US'] + options.encoding = 'UTF-8' + options.compilerArgs.add('-Xlint:all') + // enforce presence of module-info.java + options.compilerArgs.add("--module-path") + options.compilerArgs.add(classpath.asPath) + classpath = files() + } +} + +tasks.withType(Javadoc) { + doFirst { + options.addStringOption('Xdoclint:none', '-quiet') + options.encoding = 'UTF-8' + } +} + +tasks.withType(JavaExec) { + doFirst { + jvmArguments.add("--module-path") + jvmArguments.add(classpath.asPath) + classpath = files() + } +} diff --git a/gradle/documentation/asciidoc.gradle b/gradle/documentation/asciidoc.gradle new file mode 100644 index 0000000..87196cf --- /dev/null +++ b/gradle/documentation/asciidoc.gradle @@ -0,0 +1,13 @@ +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + +asciidoctor { + attributes 'source-highlighter': 'coderay', + toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img' +} \ No newline at end of file diff --git a/gradle/ide/idea.gradle b/gradle/ide/idea.gradle new file mode 100644 index 0000000..5bd2095 --- /dev/null +++ b/gradle/ide/idea.gradle @@ -0,0 +1,8 @@ +apply plugin: 'idea' + +idea { + module { + outputDir file('build/classes/java/main') + testOutputDir file('build/classes/java/test') + } +} diff --git a/gradle/publish/forgejo.gradle b/gradle/publish/forgejo.gradle new file mode 100644 index 0000000..b99b2fb --- /dev/null +++ b/gradle/publish/forgejo.gradle @@ -0,0 +1,16 @@ +if (project.hasProperty('forgeJoToken')) { + publishing { + repositories { + maven { + url 'https://xbib.org/api/packages/joerg/maven' + credentials(HttpHeaderCredentials) { + name = "Authorization" + value = "token ${project.property('forgeJoToken')}" + } + authentication { + header(HttpHeaderAuthentication) + } + } + } + } +} diff --git a/gradle/publish/ivy.gradle b/gradle/publish/ivy.gradle new file mode 100644 index 0000000..fe0a848 --- /dev/null +++ b/gradle/publish/ivy.gradle @@ -0,0 +1,27 @@ +apply plugin: 'ivy-publish' + +publishing { + repositories { + ivy { + url = "https://xbib.org/repo" + } + } + publications { + ivy(IvyPublication) { + from components.java + descriptor { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + author { + name = 'Jörg Prante' + url = 'http://example.com/users/jane' + } + descriptor.description { + text = rootProject.ext.description + } + } + } + } +} \ No newline at end of file diff --git a/gradle/publish/maven.gradle b/gradle/publish/maven.gradle new file mode 100644 index 0000000..02d909e --- /dev/null +++ b/gradle/publish/maven.gradle @@ -0,0 +1,51 @@ + +publishing { + publications { + "${project.name}"(MavenPublication) { + from components.java + pom { + artifactId = project.name + name = project.name + description = rootProject.ext.description + url = rootProject.ext.url + inceptionYear = rootProject.ext.inceptionYear + packaging = 'jar' + organization { + name = 'xbib' + url = 'https://xbib.org' + } + developers { + developer { + id = 'jprante' + name = 'Jörg Prante' + email = 'joergprante@gmail.com' + url = 'https://xbib.org/joerg' + } + } + scm { + url = rootProject.ext.scmUrl + connection = rootProject.ext.scmConnection + developerConnection = rootProject.ext.scmDeveloperConnection + } + issueManagement { + system = rootProject.ext.issueManagementSystem + url = rootProject.ext.issueManagementUrl + } + licenses { + license { + name = rootProject.ext.licenseName + url = rootProject.ext.licenseUrl + distribution = 'repo' + } + } + } + } + } +} + +if (project.hasProperty("signing.keyId")) { + apply plugin: 'signing' + signing { + sign publishing.publications."${project.name}" + } +} diff --git a/gradle/publish/sonatype.gradle b/gradle/publish/sonatype.gradle new file mode 100644 index 0000000..02744cd --- /dev/null +++ b/gradle/publish/sonatype.gradle @@ -0,0 +1,12 @@ + +if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { + nexusPublishing { + repositories { + sonatype { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } + } + } +} diff --git a/gradle/quality/checkstyle.gradle b/gradle/quality/checkstyle.gradle new file mode 100644 index 0000000..85b8bd8 --- /dev/null +++ b/gradle/quality/checkstyle.gradle @@ -0,0 +1,19 @@ + +apply plugin: 'checkstyle' + +tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.getRequired().set(true) + html.getRequired().set(true) + } +} + +checkstyle { + configFile = rootProject.file('gradle/quality/checkstyle.xml') + ignoreFailures = true + showViolations = true + checkstyleMain { + source = sourceSets.main.allSource + } +} diff --git a/gradle/quality/checkstyle.xml b/gradle/quality/checkstyle.xml new file mode 100644 index 0000000..66a9aae --- /dev/null +++ b/gradle/quality/checkstyle.xml @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/quality/cyclonedx.gradle b/gradle/quality/cyclonedx.gradle new file mode 100644 index 0000000..d94a87c --- /dev/null +++ b/gradle/quality/cyclonedx.gradle @@ -0,0 +1,11 @@ +cyclonedxBom { + includeConfigs = [ 'runtimeClasspath' ] + skipConfigs = [ 'compileClasspath', 'testCompileClasspath' ] + projectType = "library" + schemaVersion = "1.4" + destination = file("build/reports") + outputName = "bom" + outputFormat = "json" + includeBomSerialNumber = true + componentVersion = "2.0.0" +} \ No newline at end of file diff --git a/gradle/quality/pmd.gradle b/gradle/quality/pmd.gradle new file mode 100644 index 0000000..55fcfda --- /dev/null +++ b/gradle/quality/pmd.gradle @@ -0,0 +1,17 @@ + +apply plugin: 'pmd' + +tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.getRequired().set(true) + html.getRequired().set(true) + } +} + +pmd { + ignoreFailures = true + consoleOutput = false + toolVersion = "6.51.0" + ruleSetFiles = rootProject.files('gradle/quality/pmd/category/java/bestpractices.xml') +} diff --git a/gradle/quality/sonarqube.gradle b/gradle/quality/sonarqube.gradle new file mode 100644 index 0000000..d8eddd0 --- /dev/null +++ b/gradle/quality/sonarqube.gradle @@ -0,0 +1,10 @@ + +sonarqube { + properties { + property "sonar.projectName", "${project.group} ${project.name}" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.junit.reportsPath", "build/test-results/test/" + } +} diff --git a/gradle/quality/spotbugs.gradle b/gradle/quality/spotbugs.gradle new file mode 100644 index 0000000..2e5b0cd --- /dev/null +++ b/gradle/quality/spotbugs.gradle @@ -0,0 +1,15 @@ + +apply plugin: 'com.github.spotbugs' + +spotbugs { + effort = "max" + reportLevel = "low" + ignoreFailures = true +} + +spotbugsMain { + reports { + xml.getRequired().set(false) + html.getRequired().set(true) + } +} diff --git a/gradle/repositories/maven.gradle b/gradle/repositories/maven.gradle new file mode 100644 index 0000000..ec58acb --- /dev/null +++ b/gradle/repositories/maven.gradle @@ -0,0 +1,4 @@ +repositories { + mavenLocal() + mavenCentral() +} diff --git a/gradle/test/jmh.gradle b/gradle/test/jmh.gradle new file mode 100644 index 0000000..8c38e5c --- /dev/null +++ b/gradle/test/jmh.gradle @@ -0,0 +1,22 @@ +sourceSets { + jmh { + java.srcDirs = ['src/jmh/java'] + resources.srcDirs = ['src/jmh/resources'] + compileClasspath += sourceSets.main.runtimeClasspath + } +} + +dependencies { + jmhImplementation 'org.openjdk.jmh:jmh-core:1.34' + jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.34' +} + +task jmh(type: JavaExec, group: 'jmh', dependsOn: jmhClasses) { + mainClass.set('org.openjdk.jmh.Main') + classpath = sourceSets.jmh.compileClasspath + sourceSets.jmh.runtimeClasspath + project.file('build/reports/jmh').mkdirs() + args '-rf', 'json' + args '-rff', project.file('build/reports/jmh/result.json') +} + +classes.finalizedBy(jmhClasses) diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle new file mode 100644 index 0000000..6aaed77 --- /dev/null +++ b/gradle/test/junit5.gradle @@ -0,0 +1,34 @@ +dependencies { + testImplementation testLibs.junit.jupiter.api + testImplementation testLibs.hamcrest + testRuntimeOnly testLibs.junit.jupiter.engine + testRuntimeOnly testLibs.junit.jupiter.platform.launcher +} + +test { + useJUnitPlatform() + failFast = false + jvmArgs '--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED', + '--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED', + '--add-exports=java.base/sun.nio.ch=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED', + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.nio=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED' + systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties' + testLogging { + events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED' + } + afterSuite { desc, result -> + if (!desc.parent) { + println "\nTest result: ${result.resultType}" + println "Test summary: ${result.testCount} tests, " + + "${result.successfulTestCount} succeeded, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped" + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e6aba25 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..6689b85 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/net-ldap/build.gradle b/net-ldap/build.gradle new file mode 100644 index 0000000..64a5378 --- /dev/null +++ b/net-ldap/build.gradle @@ -0,0 +1,7 @@ + +dependencies { + api project(':asn1') + api libs.netty.handler + api libs.netty.epoll + api libs.netty.kqueue +} diff --git a/net-ldap/src/main/java/module-info.java b/net-ldap/src/main/java/module-info.java new file mode 100644 index 0000000..848cad6 --- /dev/null +++ b/net-ldap/src/main/java/module-info.java @@ -0,0 +1,11 @@ +module org.xbib.net.ldap { + requires java.security.sasl; + requires java.naming; + requires jdk.security.auth; + requires io.netty.buffer; + requires io.netty.handler; + requires io.netty.transport; + requires io.netty.codec; + requires io.netty.common; + requires org.xbib.net.ldap.asnone; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbandonRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbandonRequest.java new file mode 100644 index 0000000..a96f43c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbandonRequest.java @@ -0,0 +1,103 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; + +/** + * LDAP abandon request defined as: + * + *
+ * AbandonRequest ::= [APPLICATION 16] MessageID
+ * 
+ * + */ +public class AbandonRequest extends AbstractRequestMessage { + + /** + * Protocol operation identifier. + */ + public static final int PROTOCOL_OP = 16; + + /** + * Protocol message ID. + */ + private int messageID; + + + /** + * Default constructor. + */ + private AbandonRequest() { + } + + + /** + * Creates a new abandon request. + * + * @param id message ID + */ + public AbandonRequest(final int id) { + messageID = id; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + public int getMessageID() { + return messageID; + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + return new DEREncoder[]{ + new IntegerType(id), + new IntegerType(new ApplicationDERTag(PROTOCOL_OP, false), messageID), + }; + } + + @Override + public String toString() { + return super.toString() + ", " + + "messageID=" + messageID; + } + + /** + * Abandon request builder. + */ + public static class Builder extends AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new AbandonRequest()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the message ID. + * + * @param id message ID + * @return this builder + */ + public Builder id(final int id) { + object.messageID = id; + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConfig.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConfig.java new file mode 100644 index 0000000..7b7cffc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConfig.java @@ -0,0 +1,45 @@ + +package org.xbib.net.ldap; + +/** + * Provides common implementations for configuration objects. + * + */ +public abstract class AbstractConfig { + + /** + * Verifies that an array does not contain a null element. + * + * @param array to verify + * @throws IllegalArgumentException if the array contains null + */ + protected void checkArrayContainsNull(final Object[] array) { + if (array != null) { + for (Object o : array) { + if (o == null) { + throw new IllegalArgumentException("Array element cannot be null"); + } + } + } + } + + + /** + * Verifies that a string is not null or empty. + * + * @param s to verify + * @param allowNull whether null strings are valid + * @throws IllegalArgumentException if the string is null or empty + */ + protected void checkStringInput(final String s, final boolean allowNull) { + if (allowNull) { + if ("".equals(s)) { + throw new IllegalArgumentException("Input cannot be empty"); + } + } else { + if (s == null || "".equals(s)) { + throw new IllegalArgumentException("Input cannot be null or empty"); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConnectionStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConnectionStrategy.java new file mode 100644 index 0000000..a5bb5fe --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConnectionStrategy.java @@ -0,0 +1,171 @@ + +package org.xbib.net.ldap; + +import java.time.Instant; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Base class for connection strategy implementations. + * + */ +public abstract class AbstractConnectionStrategy implements ConnectionStrategy { + + /** + * Set of LDAP URLs to attempt connections to. + */ + protected LdapURLSet ldapURLSet; + + /** + * Whether this strategy has been successfully initialized. + */ + private boolean initialized; + + /** + * Condition used to determine whether to activate a URL. + */ + private Predicate activateCondition; + + /** + * Condition used to determine whether to test an inactive URL. + */ + private Predicate retryCondition = new Predicate<>() { + @Override + public boolean test(final LdapURL url) { + return Instant.now().isAfter(url.getRetryMetadata().getFailureTime().plus(LdapURLActivatorService.getPeriod())); + } + + @Override + public String toString() { + return "DEFAULT_RETRY_CONDITION"; + } + }; + + + @Override + public boolean isInitialized() { + return initialized; + } + + + @Override + public synchronized void initialize(final String urls, final Predicate condition) { + if (isInitialized()) { + throw new IllegalStateException("Strategy has already been initialized"); + } + ldapURLSet = new LdapURLSet(this, urls); + activateCondition = condition; + initialized = true; + } + + + @Override + public void populate(final String urls, final LdapURLSet urlSet) { + if (urls == null || urls.isEmpty()) { + throw new IllegalArgumentException("urls cannot be empty or null"); + } + if (urls.contains(" ")) { + urlSet.populate(Stream.of(urls.split(" ")) + .map(s -> { + final LdapURL url = new LdapURL(s); + url.setRetryMetadata(new LdapURLRetryMetadata(this)); + return url; + }).collect(Collectors.toList())); + } else { + final LdapURL url = new LdapURL(urls); + url.setRetryMetadata(new LdapURLRetryMetadata(this)); + urlSet.populate(Collections.singletonList(url)); + } + } + + + @Override + public Predicate getActivateCondition() { + return activateCondition; + } + + + @Override + public Predicate getRetryCondition() { + return retryCondition; + } + + + /** + * Sets the retry condition which determines whether an attempt should be made to activate a URL. + * + * @param condition that determines whether to test an inactive URL + */ + public void setRetryCondition(final Predicate condition) { + retryCondition = condition; + } + + + @Override + public void success(final LdapURL url) { + url.activate(); + url.getRetryMetadata().recordSuccess(Instant.now()); + } + + + @Override + public void failure(final LdapURL url) { + url.deactivate(); + url.getRetryMetadata().recordFailure(Instant.now()); + LdapURLActivatorService.getInstance().registerUrl(url); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "ldapURLSet=" + ldapURLSet + ", " + + "activateCondition=" + activateCondition + ", " + + "retryCondition=" + retryCondition + ", " + + "initialized=" + initialized + "]"; + } + + + /** + * Default iterator implementation. + */ + protected static class DefaultLdapURLIterator implements Iterator { + + /** + * URLs to iterate over. + */ + private final List ldapUrls; + + /** + * Iterator index. + */ + private int i; + + + /** + * Creates a new default LDAP URL iterator. + * + * @param urls to iterate over + */ + public DefaultLdapURLIterator(final List urls) { + ldapUrls = urls; + } + + + @Override + public boolean hasNext() { + return i < ldapUrls.size(); + } + + + @Override + public LdapURL next() { + return ldapUrls.get(i++); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConnectionValidator.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConnectionValidator.java new file mode 100644 index 0000000..93e7c0a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractConnectionValidator.java @@ -0,0 +1,290 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Base class for connection validator implementations. + * + */ +public abstract class AbstractConnectionValidator implements ConnectionValidator { + + /** + * Default validation period, value is 30 minutes. + */ + public static final Duration DEFAULT_VALIDATE_PERIOD = Duration.ofMinutes(30); + + /** + * Default per connection validate timeout, value is 5 seconds. + */ + public static final Duration DEFAULT_VALIDATE_TIMEOUT = Duration.ofSeconds(5); + + /** + * Validation period. + */ + private Duration validatePeriod; + + /** + * Maximum length of time a connection validation should block. + */ + private Duration validateTimeout; + + /** + * Consumer to execute on a successful validation. + */ + private Consumer onSuccess; + + /** + * Consumer to execute on a failed validation. + */ + private Consumer onFailure; + + /** + * Whether the occurrence of a timeout should result in a validation failure. + */ + private boolean timeoutIsFailure = true; + + + @Override + public Duration getValidatePeriod() { + return validatePeriod; + } + + + public void setValidatePeriod(final Duration period) { + if (period == null || period.isNegative() || period.isZero()) { + throw new IllegalArgumentException("Period cannot be null, negative or zero"); + } + validatePeriod = period; + } + + @Override + public Duration getValidateTimeout() { + return validateTimeout; + } + + + /** + * Sets the validate timeout. + * + * @param timeout to set + */ + public void setValidateTimeout(final Duration timeout) { + if (timeout == null || timeout.isNegative()) { + throw new IllegalArgumentException("Timeout cannot be null or negative"); + } + validateTimeout = timeout; + } + + + /** + * Returns a consumer to handle a connection that has been successfully validated. + * + * @return success consumer + */ + public Consumer getOnSuccess() { + return onSuccess; + } + + + /** + * Sets a consumer to handle a connection that has been successfully validated. + * + * @param consumer to invoke on success + */ + public void setOnSuccess(final Consumer consumer) { + onSuccess = consumer; + } + + + /** + * Returns a consumer to handle a connection that has failed validation. + * + * @return failure consumer + */ + public Consumer getOnFailure() { + return onFailure; + } + + + /** + * Sets a consumer to handle a connection that has failed validation. + * + * @param consumer to invoke on failure + */ + public void setOnFailure(final Consumer consumer) { + onFailure = consumer; + } + + + /** + * Returns whether a timeout should be considered a validation failure. + * + * @return whether a timeout should be considered a validation failure + */ + public boolean getTimeoutIsFailure() { + return timeoutIsFailure; + } + + + /** + * Sets whether a timeout should be considered a validation failure. + * + * @param failure whether a timeout should be considered a validation failure + */ + public void setTimeoutIsFailure(final boolean failure) { + timeoutIsFailure = failure; + } + + + @Override + public Boolean apply(final Connection conn) { + if (conn == null) { + if (onFailure != null) { + onFailure.accept(null); + } + return false; + } + final Boolean result = applyAsync(conn).get(); + if (result && onSuccess != null) { + onSuccess.accept(conn); + } else if (!result && onFailure != null) { + onFailure.accept(conn); + } + return result; + } + + + @Override + public Supplier applyAsync(final Connection conn) { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean result = new AtomicBoolean(); + applyAsync(conn, value -> { + result.compareAndSet(false, value); + latch.countDown(); + }); + return () -> { + try { + if (Duration.ZERO.equals(getValidateTimeout())) { + // waits indefinitely for the validation response + latch.await(); + } else { + if (!latch.await(getValidateTimeout().toMillis(), TimeUnit.MILLISECONDS) && !timeoutIsFailure) { + result.compareAndSet(false, true); + } + } + } catch (Exception e) { + // + } + return result.get(); + }; + } + + + @Override + public String toString() { + return + getClass().getName() + "@" + hashCode() + "::" + + "validatePeriod=" + validatePeriod + ", " + + "validateTimeout=" + validateTimeout + ", " + + "onSuccess=" + onSuccess + ", " + + "onFailure=" + onFailure + ", " + + "timeoutIsFailure=" + timeoutIsFailure; + } + + + /** + * Base class for validator builders. + * + * @param type of builder + * @param type of validator + */ + protected abstract static class AbstractBuilder { + + /** + * Validator to build. + */ + protected final T object; + + + /** + * Creates a new abstract builder. + * + * @param t validator to build + */ + protected AbstractBuilder(final T t) { + object = t; + } + + + /** + * Returns this builder. + * + * @return builder + */ + protected abstract B self(); + + + /** + * Sets the validation period. + * + * @param period to set + * @return this builder + */ + public B period(final Duration period) { + object.setValidatePeriod(period); + return self(); + } + + + /** + * Sets the validation timeout. + * + * @param timeout to set + * @return this builder + */ + public B timeout(final Duration timeout) { + object.setValidateTimeout(timeout); + return self(); + } + + + public B onSuccess(final Consumer consumer) { + object.setOnSuccess(consumer); + return self(); + } + + + public B onFailure(final Consumer consumer) { + object.setOnFailure(consumer); + return self(); + } + + + /** + * Sets whether timeout is a validation failure. + * + * @param failure whether timeout is a validation failure + * @return this builder + */ + public B timeoutIsFailure(final boolean failure) { + object.setTimeoutIsFailure(failure); + return self(); + } + + + /** + * Returns the connection validator. + * + * @return connection validator + */ + public T build() { + return object; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractMessage.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractMessage.java new file mode 100644 index 0000000..997f67b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractMessage.java @@ -0,0 +1,313 @@ + +package org.xbib.net.ldap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.net.ldap.control.ControlFactory; +import org.xbib.net.ldap.control.ResponseControl; + +/** + * LDAP message envelope defined as: + * + *
+ * LDAPMessage ::= SEQUENCE {
+ * messageID       MessageID,
+ * protocolOp      CHOICE {
+ * ...,
+ * controls       [0] Controls OPTIONAL }
+ *
+ * Control ::= SEQUENCE {
+ * controlType             LDAPOID,
+ * criticality             BOOLEAN DEFAULT FALSE,
+ * controlValue            OCTET STRING OPTIONAL }
+ * 
+ * + */ +public abstract class AbstractMessage implements Message { + + /** + * Protocol message ID. + */ + private int messageID; + + /** + * LDAP controls. + */ + private List controls = new ArrayList<>(); + + + @Override + public int getMessageID() { + return messageID; + } + + + public void setMessageID(final int id) { + messageID = id; + } + + + @Override + public ResponseControl[] getControls() { + return controls != null ? controls.toArray(new ResponseControl[0]) : null; + } + + + /** + * Adds the supplied controls to this message. + * + * @param cntrls to add + */ + public void addControls(final ResponseControl... cntrls) { + Collections.addAll(controls, cntrls); + } + + + /** + * Copies the property values from the supplied message to this message. + * + * @param type of message + * @param message to copy from + */ + protected void copyValues(final T message) { + setMessageID(message.getMessageID()); + addControls(message.getControls()); + } + + + // CheckStyle:EqualsHashCode OFF + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AbstractMessage) { + final AbstractMessage v = (AbstractMessage) o; + return LdapUtils.areEqual(getMessageID(), v.getMessageID()) && + LdapUtils.areEqual(getControls(), v.getControls()); + } + return false; + } + // CheckStyle:EqualsHashCode ON + + + /** + * Returns the hash code for this object. + * + * @return hash code + */ + @Override + public abstract int hashCode(); + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + "messageID=" + messageID + ", " + "controls=" + controls; + } + + + /** + * Parse handler implementation for the message ID. + */ + protected static class MessageIDHandler extends AbstractParseHandler { + + /** + * DER path to message id. + */ + public static final DERPath PATH = new DERPath("/SEQ/INT[0]"); + + + /** + * Creates a new message ID handler. + * + * @param response to configure + */ + public MessageIDHandler(final AbstractMessage response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setMessageID(IntegerType.decodeUnsignedPrimitive(encoded)); + } + } + + + /** + * Parse handler implementation for the message controls. + */ + protected static class ControlsHandler extends AbstractParseHandler { + + /** + * DER path to controls. + */ + public static final DERPath PATH = new DERPath("/SEQ/CTX(0)/SEQ"); + + + /** + * Creates a new controls handler. + * + * @param response to configure + */ + public ControlsHandler(final AbstractMessage response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final ControlParser p = new ControlParser(); + p.parse(encoded); + getObject().addControls( + ControlFactory.createResponseControl( + p.getOid().isPresent() ? p.getOid().get() : null, + p.getCritical().isPresent() ? p.getCritical().get() : false, + p.getValue().isPresent() ? p.getValue().get() : null)); + } + } + + + /** + * Parses a buffer containing an LDAP control. + */ + protected static class ControlParser { + + /** + * DER path to criticality. + */ + private static final DERPath CRITICAL_PATH = new DERPath("/BOOL[1]"); + + /** + * DER path to OID. + */ + private static final DERPath OID_PATH = new DERPath("/OCTSTR[0]"); + + /** + * DER path to value. + */ + private static final DERPath VALUE_PATH = new DERPath("/OCTSTR[1]"); + + /** + * DER path to alternate value. + */ + private static final DERPath ALT_VALUE_PATH = new DERPath("/OCTSTR[2]"); + + /** + * Parser for decoding LDAP controls. + */ + private final DERParser parser = new DERParser(); + + /** + * Control criticality. + */ + private Boolean critical; + + /** + * Control oid. + */ + private String oid; + + /** + * Control value. + */ + private DERBuffer value; + + + /** + * Creates a new control parser. + */ + public ControlParser() { + parser.registerHandler(CRITICAL_PATH, (p, e) -> critical = BooleanType.decode(e)); + parser.registerHandler(OID_PATH, (p, e) -> oid = OctetStringType.decode(e)); + parser.registerHandler(VALUE_PATH, (p, e) -> value = e.slice()); + parser.registerHandler(ALT_VALUE_PATH, (p, e) -> { + if (value == null) { + value = e.slice(); + } + }); + } + + + /** + * Examines the supplied buffer and parses an LDAP control if one is found. + * + * @param buffer to parse + */ + public void parse(final DERBuffer buffer) { + parser.parse(buffer); + } + + + /** + * Returns the control criticality. + * + * @return criticality or empty + */ + public Optional getCritical() { + return Optional.ofNullable(critical); + } + + + /** + * Returns the control oid. + * + * @return control oid or empty + */ + public Optional getOid() { + return Optional.ofNullable(oid); + } + + + /** + * Returns the control value. + * + * @return control value or empty + */ + public Optional getValue() { + return Optional.ofNullable(value); + } + } + + + // CheckStyle:OFF + protected abstract static class AbstractBuilder { + + protected final T object; + + + protected AbstractBuilder(final T t) { + object = t; + } + + + protected abstract B self(); + + + public B messageID(final int id) { + object.setMessageID(id); + return self(); + } + + + public B controls(final ResponseControl... controls) { + object.addControls(controls); + return self(); + } + + + public T build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractOperation.java new file mode 100644 index 0000000..d8d2e6f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractOperation.java @@ -0,0 +1,378 @@ + +package org.xbib.net.ldap; + +import java.util.Arrays; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.RequestHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Base class for operations. + * + * @param type of request + * @param type of result + */ +public abstract class AbstractOperation implements Operation { + + /** + * Connection factory. + */ + private ConnectionFactory connectionFactory; + + /** + * Functions to handle requests. + */ + private RequestHandler[] requestHandlers; + + /** + * Functions to handle response results. + */ + private ResultHandler[] resultHandlers; + + /** + * Functions to handle response controls. + */ + private ResponseControlHandler[] controlHandlers; + + /** + * Functions to handle referrals. + */ + private ReferralHandler[] referralHandlers; + + /** + * Functions to handle intermediate responses. + */ + private IntermediateResponseHandler[] intermediateResponseHandlers; + + /** + * Function to handle exceptions. + */ + private ExceptionHandler exceptionHandler; + + /** + * Function to test results. + */ + private ResultPredicate throwCondition; + + /** + * Functions to handle unsolicited notifications. + */ + private UnsolicitedNotificationHandler[] unsolicitedNotificationHandlers; + + + /** + * Default constructor. + */ + public AbstractOperation() { + } + + + /** + * Creates a new abstract operation. + * + * @param factory connection factory + */ + public AbstractOperation(final ConnectionFactory factory) { + setConnectionFactory(factory); + } + + + public ConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + + public void setConnectionFactory(final ConnectionFactory factory) { + connectionFactory = factory; + } + + + public RequestHandler[] getRequestHandlers() { + return requestHandlers; + } + + + @SuppressWarnings("unchecked") + public void setRequestHandlers(final RequestHandler... handlers) { + requestHandlers = handlers; + } + + + public ResultHandler[] getResultHandlers() { + return resultHandlers; + } + + + public void setResultHandlers(final ResultHandler... handlers) { + resultHandlers = handlers; + } + + + public ResponseControlHandler[] getControlHandlers() { + return controlHandlers; + } + + + public void setControlHandlers(final ResponseControlHandler... handlers) { + controlHandlers = handlers; + } + + + public ReferralHandler[] getReferralHandlers() { + return referralHandlers; + } + + + public void setReferralHandlers(final ReferralHandler... handlers) { + referralHandlers = handlers; + } + + + public IntermediateResponseHandler[] getIntermediateResponseHandlers() { + return intermediateResponseHandlers; + } + + + public void setIntermediateResponseHandlers(final IntermediateResponseHandler... handlers) { + intermediateResponseHandlers = handlers; + } + + + public ExceptionHandler getExceptionHandler() { + return exceptionHandler; + } + + + public void setExceptionHandler(final ExceptionHandler handler) { + exceptionHandler = handler; + } + + + public ResultPredicate getThrowCondition() { + return throwCondition; + } + + + public void setThrowCondition(final ResultPredicate function) { + throwCondition = function; + } + + + public UnsolicitedNotificationHandler[] getUnsolicitedNotificationHandlers() { + return unsolicitedNotificationHandlers; + } + + + public void setUnsolicitedNotificationHandlers(final UnsolicitedNotificationHandler... handlers) { + unsolicitedNotificationHandlers = handlers; + } + + + /** + * Applies any configured request handlers to the supplied request. Returns the supplied request unaltered if no + * request handlers are configured. + * + * @param request to configure + * @return configured request + */ + protected Q configureRequest(final Q request) { + if (requestHandlers == null || requestHandlers.length == 0) { + return request; + } + Q req = request; + for (RequestHandler func : requestHandlers) { + req = func.apply(req); + } + return req; + } + + + /** + * Adds configured functions to the supplied handle. + * + * @param handle to configure + * @return configured handle + */ + protected OperationHandle configureHandle(final OperationHandle handle) { + return handle + .onControl(getControlHandlers()) + .onReferral(getReferralHandlers()) + .onIntermediate(getIntermediateResponseHandlers()) + .onException(getExceptionHandler()) + .throwIf(getThrowCondition()) + .onUnsolicitedNotification(getUnsolicitedNotificationHandlers()) + .onResult(getResultHandlers()); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "connectionFactory=" + connectionFactory + ", " + + "requestHandlers=" + Arrays.toString(requestHandlers) + ", " + + "resultHandlers=" + Arrays.toString(resultHandlers) + ", " + + "controlHandlers=" + Arrays.toString(controlHandlers) + ", " + + "referralHandlers=" + Arrays.toString(referralHandlers) + ", " + + "intermediateResponseHandlers=" + Arrays.toString(intermediateResponseHandlers) + ", " + + "exceptionHandler=" + exceptionHandler + ", " + + "throwCondition=" + throwCondition + ", " + + "unsolicitedNotificationHandlers=" + Arrays.toString(unsolicitedNotificationHandlers); + } + + + /** + * Base class for operation builders. + * + * @param type of builder + * @param type of operation + */ + protected abstract static class AbstractBuilder { + + /** + * Operation to build. + */ + protected final T object; + + + /** + * Creates a new abstract builder. + * + * @param t operation to build + */ + protected AbstractBuilder(final T t) { + object = t; + } + + + /** + * Returns this builder. + * + * @return builder + */ + protected abstract B self(); + + + /** + * Sets the connection factory. + * + * @param factory to set + * @return this builder + */ + public B factory(final ConnectionFactory factory) { + object.setConnectionFactory(factory); + return self(); + } + + + /** + * Sets the functions to execute before a request is sent. + * + * @param handlers to execute on a request + * @return this builder + */ + @SuppressWarnings("unchecked") + public B onRequest(final RequestHandler... handlers) { + object.setRequestHandlers(handlers); + return self(); + } + + + /** + * Sets the functions to execute when a result is received. + * + * @param handlers to execute on a result + * @return this builder + */ + public B onResult(final ResultHandler... handlers) { + object.setResultHandlers(handlers); + return self(); + } + + + /** + * Sets the functions to execute when a control is received. + * + * @param handlers to execute on a control + * @return this builder + */ + public B onControl(final ResponseControlHandler... handlers) { + object.setControlHandlers(handlers); + return self(); + } + + + /** + * Sets the functions to execute when a referral is received. + * + * @param handlers to execute on a referral + * @return this builder + */ + public B onReferral(final ReferralHandler... handlers) { + object.setReferralHandlers(handlers); + return self(); + } + + + /** + * Sets the functions to execute when an intermediate response is received. + * + * @param handlers to execute on an intermediate response + * @return this builder + */ + public B onIntermediate(final IntermediateResponseHandler... handlers) { + object.setIntermediateResponseHandlers(handlers); + return self(); + } + + + /** + * Sets the functions to execute when an unsolicited notification is received. + * + * @param handlers to execute on an unsolicited notification + * @return this builder + */ + public B onUnsolicitedNotification(final UnsolicitedNotificationHandler... handlers) { + object.setUnsolicitedNotificationHandlers(handlers); + return self(); + } + + + /** + * Sets the function to execute when an exception occurs. + * + * @param handler to execute on an exception occurs + * @return this builder + */ + public B onException(final ExceptionHandler handler) { + object.setExceptionHandler(handler); + return self(); + } + + + /** + * Sets the function to test a result. + * + * @param function to test a result + * @return this builder + */ + public B throwIf(final ResultPredicate function) { + object.setThrowCondition(function); + return self(); + } + + + /** + * Returns the operation. + * + * @return operation + */ + public T build() { + return object; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractOperationConnectionValidator.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractOperationConnectionValidator.java new file mode 100644 index 0000000..d4cd658 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractOperationConnectionValidator.java @@ -0,0 +1,163 @@ + +package org.xbib.net.ldap; + +import java.util.Arrays; +import java.util.function.Consumer; + +/** + * Base class for validators that use an operation to perform validation. By default, validation is considered + * successful if the operation result contains any result code. Stricter validation can be configured by setting {@link + * #validResultCodes}. + * + * @param type of request + * @param type of result + */ +public abstract class AbstractOperationConnectionValidator + extends AbstractConnectionValidator { + + /** + * Operation request. + */ + private Q request; + + /** + * Valid result codes. + */ + private ResultCode[] validResultCodes; + + + /** + * Returns the operation request. + * + * @return operation request + */ + public Q getRequest() { + return request; + } + + + /** + * Sets the operation request. + * + * @param req operation request + */ + public void setRequest(final Q req) { + request = req; + } + + + /** + * Returns the valid result codes. + * + * @return valid result codes + */ + public ResultCode[] getValidResultCodes() { + return validResultCodes; + } + + + /** + * Sets the valid result codes. + * + * @param codes that represent a valid connection + */ + public void setValidResultCodes(final ResultCode... codes) { + validResultCodes = codes; + } + + + /** + * Perform the operation for this validator. + * + * @param conn to validate + * @return operation handle + */ + protected abstract OperationHandle performOperation(Connection conn); + + + @Override + public void applyAsync(final Connection conn, final Consumer function) { + if (conn == null) { + function.accept(false); + } else { + final OperationHandle h = performOperation(conn); + h.onResult(r -> { + if (validResultCodes != null) { + function.accept(Arrays.stream(validResultCodes).anyMatch(rc -> rc.equals(r.getResultCode()))); + } else { + function.accept(r.getResultCode() != null); + } + }); + h.onException(e -> { + if (e != null && e.getResultCode() == ResultCode.LDAP_TIMEOUT && !getTimeoutIsFailure()) { + function.accept(true); + } else { + function.accept(false); + } + }); + h.send(); + } + } + + + @Override + public String toString() { + return super.toString() + ", request=" + request + ", validResultCodes=" + Arrays.toString(validResultCodes); + } + + + /** + * Base class for operation validator builders. + * + * @param type of request + * @param type of result + * @param type of builder + * @param type of validator + */ + protected abstract static class AbstractBuilder + > extends + AbstractConnectionValidator.AbstractBuilder { + + + /** + * Creates a new abstract builder. + * + * @param t validator to build + */ + protected AbstractBuilder(final T t) { + super(t); + } + + + /** + * Returns this builder. + * + * @return builder + */ + protected abstract B self(); + + + /** + * Sets the request to use for validation. + * + * @param request operation request + * @return this builder + */ + public B request(final Q request) { + object.setRequest(request); + return self(); + } + + + /** + * Sets the result codes to use for validation. + * + * @param codes valid result codes + * @return this builder + */ + public B validResultCodes(final ResultCode... codes) { + object.setValidResultCodes(codes); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractRequestMessage.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractRequestMessage.java new file mode 100644 index 0000000..818fbd0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractRequestMessage.java @@ -0,0 +1,202 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; +import java.util.Arrays; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; +import org.xbib.net.ldap.control.RequestControl; + +/** + * LDAP message envelope defined as: + * + *
+ * LDAPMessage ::= SEQUENCE {
+ * messageID       MessageID,
+ * protocolOp      CHOICE {
+ * ...,
+ * controls       [0] Controls OPTIONAL }
+ *
+ * Control ::= SEQUENCE {
+ * controlType             LDAPOID,
+ * criticality             BOOLEAN DEFAULT FALSE,
+ * controlValue            OCTET STRING OPTIONAL }
+ * 
+ * + */ +public abstract class AbstractRequestMessage implements Request { + + /** + * LDAP controls. + */ + private RequestControl[] controls; + + /** + * Duration of time to wait for a response. This property is not part of the request specification. + */ + private Duration responseTimeout; + + + public RequestControl[] getControls() { + return controls; + } + + + public void setControls(final RequestControl... cntrls) { + controls = cntrls; + } + + + /** + * Returns the response timeout. + * + * @return timeout + */ + public Duration getResponseTimeout() { + return responseTimeout; + } + + + /** + * Sets the maximum amount of time to wait for a response from this request. + * + * @param time timeout for a response + */ + public void setResponseTimeout(final Duration time) { + if (time == null || time.isNegative()) { + throw new IllegalArgumentException("Response timeout cannot be null or negative"); + } + responseTimeout = time; + } + + + @Override + public byte[] encode(final int id) { + final DEREncoder[] requestEncoders = getRequestEncoders(id); + final DEREncoder controlEncoder = getControlEncoder(); + final DEREncoder[] encoders; + if (controlEncoder != null) { + encoders = LdapUtils.concatArrays(requestEncoders, new DEREncoder[]{controlEncoder}); + } else { + encoders = requestEncoders; + } + final ConstructedDEREncoder se = new ConstructedDEREncoder(UniversalDERTag.SEQ, encoders); + return se.encode(); + } + + + /** + * Returns the request encoders for this message. + * + * @param id message ID + * @return request encoders + */ + protected abstract DEREncoder[] getRequestEncoders(int id); + + + /** + * Returns the encoder to any controls that may be set on this message. + * + * @return control encoder + */ + private DEREncoder getControlEncoder() { + if (controls == null || controls.length == 0) { + return null; + } + final DEREncoder[] controlEncoders = new DEREncoder[controls.length]; + for (int i = 0; i < controls.length; i++) { + if (controls[i].hasValue()) { + controlEncoders[i] = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new OctetStringType(controls[i].getOID()), + new BooleanType(controls[i].getCriticality()), + new OctetStringType(controls[i].encode())); + } else { + controlEncoders[i] = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new OctetStringType(controls[i].getOID()), + new BooleanType(controls[i].getCriticality())); + } + } + return new ConstructedDEREncoder(new ContextDERTag(0, true), controlEncoders); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "controls=" + Arrays.toString(controls) + ", " + + "responseTimeout=" + responseTimeout; + } + + + /** + * Base class for request builders. + * + * @param type of builder + * @param type of message + */ + protected abstract static class AbstractBuilder { + + /** + * Message to build. + */ + protected final T object; + + + /** + * Creates a new abstract builder. + * + * @param t message to build + */ + protected AbstractBuilder(final T t) { + object = t; + } + + + /** + * Returns this builder. + * + * @return builder + */ + protected abstract B self(); + + + /** + * Sets controls on the message. + * + * @param cntrls controls + * @return this builder + */ + public B controls(final RequestControl... cntrls) { + object.setControls(cntrls); + return self(); + } + + + /** + * Sets the response timeout on the message. + * + * @param time response timeout + * @return this builder + */ + public B responseTimeout(final Duration time) { + object.setResponseTimeout(time); + return self(); + } + + + /** + * Returns the message. + * + * @return message + */ + public T build() { + return object; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractResult.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractResult.java new file mode 100644 index 0000000..ddd621f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractResult.java @@ -0,0 +1,267 @@ + +package org.xbib.net.ldap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP result message defined as: + * + *
+ * LDAPResult ::= SEQUENCE {
+ * resultCode         ENUMERATED {
+ * ...  },
+ * matchedDN          LDAPDN,
+ * diagnosticMessage  LDAPString,
+ * referral           [3] Referral OPTIONAL }
+ *
+ * Referral ::= SEQUENCE SIZE (1..MAX) OF uri URI
+ *
+ * URI ::= LDAPString     -- limited to characters permitted in
+ * -- URIs
+ * 
+ * + */ +public abstract class AbstractResult extends AbstractMessage implements Result { + + /** + * Result code. + */ + private ResultCode resultCode; + + /** + * Matched DN. + */ + private String matchedDN; + + /** + * Diagnostic message. + */ + private String diagnosticMessage; + + /** + * Referral URLS. + */ + private final List referralURLs = new ArrayList<>(); + + + public ResultCode getResultCode() { + return resultCode; + } + + + public void setResultCode(final ResultCode code) { + resultCode = code; + } + + + public String getMatchedDN() { + return matchedDN; + } + + + public void setMatchedDN(final String dn) { + matchedDN = dn; + } + + + public String getDiagnosticMessage() { + return diagnosticMessage; + } + + + public void setDiagnosticMessage(final String message) { + diagnosticMessage = message; + } + + + public String[] getReferralURLs() { + return referralURLs != null ? referralURLs.toArray(new String[0]) : null; + } + + + /** + * Adds referral URLs to the result. + * + * @param urls to add + */ + public void addReferralURLs(final String... urls) { + Collections.addAll(referralURLs, urls); + } + + + /** + * Copies the property values from the supplied result to this result. + * + * @param type of result + * @param result to copy from + */ + protected void copyValues(final T result) { + super.copyValues(result); + setResultCode(result.getResultCode()); + setMatchedDN(result.getMatchedDN()); + setDiagnosticMessage(result.getDiagnosticMessage()); + addReferralURLs(result.getReferralURLs()); + } + + + // CheckStyle:EqualsHashCode OFF + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AbstractResult v && super.equals(o)) { + return LdapUtils.areEqual(getResultCode(), v.getResultCode()) && + LdapUtils.areEqual(getMatchedDN(), v.getMatchedDN()) && + LdapUtils.areEqual(getDiagnosticMessage(), v.getDiagnosticMessage()) && + LdapUtils.areEqual(getReferralURLs(), v.getReferralURLs()); + } + return false; + } + // CheckStyle:EqualsHashCode ON + + + @Override + public String toString() { + return super.toString() + ", " + + "resultCode=" + resultCode + ", " + + "matchedDN=" + matchedDN + ", " + + "diagnosticMessage=" + getEncodedDiagnosticMessage() + ", " + + "referralURLs=" + referralURLs; + } + + + /** + * Parse handler implementation for the LDAP result code. + */ + protected static class ResultCodeHandler extends AbstractParseHandler { + + + /** + * Creates a new LDAP result code handler. + * + * @param response to configure + */ + public ResultCodeHandler(final AbstractResult response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setResultCode(ResultCode.valueOf(IntegerType.decodeUnsignedPrimitive(encoded))); + } + } + + + /** + * Parse handler implementation for the LDAP matched DN. + */ + protected static class MatchedDNHandler extends AbstractParseHandler { + + + /** + * Creates a new LDAP matched DN handler. + * + * @param response to configure + */ + public MatchedDNHandler(final AbstractResult response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setMatchedDN(OctetStringType.decode(encoded)); + } + } + + + /** + * Parse handler implementation for the LDAP diagnostic message. + */ + protected static class DiagnosticMessageHandler extends AbstractParseHandler { + + + /** + * Creates a new LDAP diagnostic message handler. + * + * @param response to configure + */ + public DiagnosticMessageHandler(final AbstractResult response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setDiagnosticMessage(OctetStringType.decode(encoded)); + } + } + + + /** + * Parse handler implementation for the LDAP referral. + */ + protected static class ReferralHandler extends AbstractParseHandler { + + + /** + * Creates a new LDAP referral handler. + * + * @param response to configure + */ + public ReferralHandler(final AbstractResult response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().addReferralURLs(OctetStringType.decode(encoded)); + } + } + + + // CheckStyle:OFF + protected abstract static class AbstractBuilder + extends AbstractMessage.AbstractBuilder { + + + protected AbstractBuilder(final T t) { + super(t); + } + + + public B resultCode(final ResultCode code) { + object.setResultCode(code); + return self(); + } + + + public B matchedDN(final String dn) { + object.setMatchedDN(dn); + return self(); + } + + + public B diagnosticMessage(final String message) { + object.setDiagnosticMessage(message); + return self(); + } + + + public B referralURLs(final String... url) { + object.addReferralURLs(url); + return self(); + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractRetryMetadata.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractRetryMetadata.java new file mode 100644 index 0000000..657d018 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractRetryMetadata.java @@ -0,0 +1,63 @@ + +package org.xbib.net.ldap; + +import java.time.Instant; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Common implementation of retry metadata. + * + */ +public abstract class AbstractRetryMetadata implements RetryMetadata { + + /** + * Attempt count. + */ + private final AtomicInteger attempts = new AtomicInteger(); + /** + * Time at which the last success occurred. + */ + protected Instant successTime; + /** + * Time at which the failure occurred. + */ + protected Instant failureTime; + + @Override + public Instant getSuccessTime() { + return successTime; + } + + + @Override + public Instant getFailureTime() { + return failureTime; + } + + + @Override + public int getAttempts() { + return attempts.get(); + } + + + @Override + public void recordSuccess(final Instant time) { + successTime = time; + } + + + @Override + public void recordFailure(final Instant time) { + failureTime = time; + attempts.incrementAndGet(); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "attempts=" + attempts + ", " + + "failureTime=" + failureTime; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AbstractSearchOperationFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractSearchOperationFactory.java new file mode 100644 index 0000000..ad89749 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AbstractSearchOperationFactory.java @@ -0,0 +1,377 @@ + +package org.xbib.net.ldap; + +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.LdapEntryHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.RequestHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.SearchReferenceHandler; +import org.xbib.net.ldap.handler.SearchResultHandler; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Base class for classes that perform searches. + * + */ +public abstract class AbstractSearchOperationFactory implements ConnectionFactoryManager { + + /** + * Connection factory. + */ + private ConnectionFactory factory; + + /** + * Functions to handle requests. + */ + private RequestHandler[] requestHandlers; + + /** + * Functions to handle response results. + */ + private ResultHandler[] resultHandlers; + + /** + * Functions to handle response controls. + */ + private ResponseControlHandler[] controlHandlers; + + /** + * Functions to handle referrals. + */ + private ReferralHandler[] referralHandlers; + + /** + * Functions to handle intermediate responses. + */ + private IntermediateResponseHandler[] intermediateResponseHandlers; + + /** + * Function to handle exceptions. + */ + private ExceptionHandler exceptionHandler; + + /** + * Function to test results. + */ + private ResultPredicate throwCondition; + + /** + * Functions to handle unsolicited notifications. + */ + private UnsolicitedNotificationHandler[] unsolicitedNotificationHandlers; + + /** + * Functions to handle entries. + */ + private LdapEntryHandler[] entryHandlers; + + /** + * Functions to handle response references. + */ + private SearchReferenceHandler[] referenceHandlers; + + /** + * Functions to handle search response results. + */ + private SearchResultHandler[] searchResultHandlers; + + + /** + * Returns the connection factory. + * + * @return connection factory + */ + public ConnectionFactory getConnectionFactory() { + return factory; + } + + + /** + * Sets the connection factory. + * + * @param cf connection factory + */ + public void setConnectionFactory(final ConnectionFactory cf) { + factory = cf; + } + + + /** + * Returns the search request handlers. + * + * @return search request handlers + */ + public RequestHandler[] getRequestHandlers() { + return requestHandlers; + } + + + /** + * Sets the search request handlers. + * + * @param handlers search request handler + */ + @SuppressWarnings("unchecked") + public void setRequestHandlers(final RequestHandler... handlers) { + requestHandlers = handlers; + } + + + /** + * Returns the search result handlers. + * + * @return search result handlers + */ + public ResultHandler[] getResultHandlers() { + return resultHandlers; + } + + + /** + * Sets the search result handlers. + * + * @param handlers search result handlers + */ + public void setResultHandlers(final ResultHandler... handlers) { + resultHandlers = handlers; + } + + + /** + * Returns the control handlers. + * + * @return control handlers + */ + public ResponseControlHandler[] getControlHandlers() { + return controlHandlers; + } + + + /** + * Sets the control handlers. + * + * @param handlers control handlers + */ + public void setControlHandlers(final ResponseControlHandler... handlers) { + controlHandlers = handlers; + } + + + /** + * Returns the referral handlers. + * + * @return referral handlers + */ + public ReferralHandler[] getReferralHandlers() { + return referralHandlers; + } + + + /** + * Sets the referral handlers. + * + * @param handlers referral handlers + */ + public void setReferralHandlers(final ReferralHandler... handlers) { + referralHandlers = handlers; + } + + + /** + * Returns the intermediate response handlers. + * + * @return intermediate response handlers + */ + public IntermediateResponseHandler[] getIntermediateResponseHandlers() { + return intermediateResponseHandlers; + } + + + /** + * Sets the intermediate response handlers. + * + * @param handlers intermediate response handlers + */ + public void setIntermediateResponseHandlers(final IntermediateResponseHandler... handlers) { + intermediateResponseHandlers = handlers; + } + + + /** + * Returns the search exception handler. + * + * @return search exception handler + */ + public ExceptionHandler getExceptionHandler() { + return exceptionHandler; + } + + + /** + * Sets the search exception handler. + * + * @param handler search exception handler + */ + public void setExceptionHandler(final ExceptionHandler handler) { + exceptionHandler = handler; + } + + + /** + * Returns the throw condition. + * + * @return throw condition + */ + public ResultPredicate getThrowCondition() { + return throwCondition; + } + + + /** + * Sets the throw condition. + * + * @param function throw condition + */ + public void setThrowCondition(final ResultPredicate function) { + throwCondition = function; + } + + + /** + * Returns the unsolicited notification handlers. + * + * @return unsolicited notification handlers + */ + public UnsolicitedNotificationHandler[] getUnsolicitedNotificationHandlers() { + return unsolicitedNotificationHandlers; + } + + + /** + * Sets the unsolicited notification handlers. + * + * @param handlers unsolicited notification handlers + */ + public void setUnsolicitedNotificationHandlers(final UnsolicitedNotificationHandler... handlers) { + unsolicitedNotificationHandlers = handlers; + } + + + /** + * Returns the search entry handlers. + * + * @return search entry handlers + */ + public LdapEntryHandler[] getEntryHandlers() { + return entryHandlers; + } + + + /** + * Sets the search entry handlers. + * + * @param handlers search entry handlers + */ + public void setEntryHandlers(final LdapEntryHandler... handlers) { + entryHandlers = handlers; + } + + + /** + * Returns the search reference handlers. + * + * @return search reference handlers + */ + public SearchReferenceHandler[] getReferenceHandlers() { + return referenceHandlers; + } + + + /** + * Sets the search reference handlers. + * + * @param handlers search reference handlers + */ + public void setReferenceHandlers(final SearchReferenceHandler... handlers) { + referenceHandlers = handlers; + } + + + /** + * Returns the search result handlers. + * + * @return search result handlers + */ + public SearchResultHandler[] getSearchResultHandlers() { + return searchResultHandlers; + } + + + /** + * Sets the search result handlers. + * + * @param handlers search result handlers + */ + public void setSearchResultHandlers(final SearchResultHandler... handlers) { + searchResultHandlers = handlers; + } + + + /** + * Creates a new search operation configured with the properties on this factory. + * + * @return search operation + */ + protected SearchOperation createSearchOperation() { + return createSearchOperation(factory); + } + + + /** + * Creates a new search operation configured with the properties on this factory. + * + * @param cf connection factory to set on the search operation + * @return search operation + */ + protected SearchOperation createSearchOperation(final ConnectionFactory cf) { + final SearchOperation op = new SearchOperation(cf); + if (requestHandlers != null) { + op.setRequestHandlers(requestHandlers); + } + if (resultHandlers != null) { + op.setResultHandlers(resultHandlers); + } + if (controlHandlers != null) { + op.setControlHandlers(controlHandlers); + } + if (referralHandlers != null) { + op.setReferralHandlers(referralHandlers); + } + if (intermediateResponseHandlers != null) { + op.setIntermediateResponseHandlers(intermediateResponseHandlers); + } + if (exceptionHandler != null) { + op.setExceptionHandler(exceptionHandler); + } + if (throwCondition != null) { + op.setThrowCondition(throwCondition); + } + if (unsolicitedNotificationHandlers != null) { + op.setUnsolicitedNotificationHandlers(unsolicitedNotificationHandlers); + } + if (entryHandlers != null) { + op.setEntryHandlers(entryHandlers); + } + if (referenceHandlers != null) { + op.setReferenceHandlers(referenceHandlers); + } + if (searchResultHandlers != null) { + op.setSearchResultHandlers(searchResultHandlers); + } + return op; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ActivePassiveConnectionStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/ActivePassiveConnectionStrategy.java new file mode 100644 index 0000000..78a9c14 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ActivePassiveConnectionStrategy.java @@ -0,0 +1,57 @@ + +package org.xbib.net.ldap; + +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +/** + * Connection strategy that attempts hosts ordered exactly the way they are configured. This means that the first host + * will always be attempted first, followed by each host in the list. + * + */ +public class ActivePassiveConnectionStrategy extends AbstractConnectionStrategy { + + /** + * Custom iterator function. + */ + private final Function, Iterator> iterFunction; + + + /** + * Default constructor. + */ + public ActivePassiveConnectionStrategy() { + this(null); + } + + + /** + * Creates a new active passive connection strategy. + * + * @param function that produces a custom iterator + */ + public ActivePassiveConnectionStrategy(final Function, Iterator> function) { + iterFunction = function; + } + + + @Override + public Iterator iterator() { + if (!isInitialized()) { + throw new IllegalStateException("Strategy is not initialized"); + } + if (iterFunction != null) { + return iterFunction.apply(ldapURLSet.getUrls()); + } + return new DefaultLdapURLIterator(ldapURLSet.getUrls()); + } + + + @Override + public ActivePassiveConnectionStrategy newInstance() { + final ActivePassiveConnectionStrategy strategy = new ActivePassiveConnectionStrategy(iterFunction); + strategy.setRetryCondition(getRetryCondition()); + return strategy; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AddOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/AddOperation.java new file mode 100644 index 0000000..c1bf9aa --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AddOperation.java @@ -0,0 +1,149 @@ + +package org.xbib.net.ldap; + +/** + * Executes an ldap add operation. + * + */ +public class AddOperation extends AbstractOperation { + + + /** + * Default constructor. + */ + public AddOperation() { + } + + + /** + * Creates a new add operation. + * + * @param factory connection factory + */ + public AddOperation(final ConnectionFactory factory) { + super(factory); + } + + /** + * Sends an add request. See {@link OperationHandle#send()}. + * + * @param factory connection factory + * @param request add request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + public static OperationHandle send( + final ConnectionFactory factory, + final AddRequest request) + throws LdapException { + final Connection conn = factory.getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return conn.operation(request).onComplete(conn::close).send(); + } + + /** + * Executes an add request. See {@link OperationHandle#execute()}. + * + * @param factory connection factory + * @param request add request + * @return add result + * @throws LdapException if the connection cannot be opened + */ + public static AddResponse execute(final ConnectionFactory factory, final AddRequest request) + throws LdapException { + try (Connection conn = factory.getConnection()) { + conn.open(); + return conn.operation(request).execute(); + } + } + + /** + * Returns a new add operation with the same properties as the supplied operation. + * + * @param operation to copy + * @return copy of the supplied add operation + */ + public static AddOperation copy(final AddOperation operation) { + final AddOperation op = new AddOperation(); + op.setRequestHandlers(operation.getRequestHandlers()); + op.setResultHandlers(operation.getResultHandlers()); + op.setControlHandlers(operation.getControlHandlers()); + op.setReferralHandlers(operation.getReferralHandlers()); + op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers()); + op.setExceptionHandler(operation.getExceptionHandler()); + op.setThrowCondition(operation.getThrowCondition()); + op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers()); + op.setConnectionFactory(operation.getConnectionFactory()); + return op; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Sends an add request. See {@link OperationHandle#send()}. + * + * @param request add request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + @Override + public OperationHandle send(final AddRequest request) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send(); + } + + /** + * Executes an add request. See {@link OperationHandle#execute()}. + * + * @param request add request + * @return add result + * @throws LdapException if the connection cannot be opened + */ + @Override + public AddResponse execute(final AddRequest request) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + return configureHandle(conn.operation(configureRequest(request))).execute(); + } + } + + /** + * Add operation builder. + */ + public static class Builder extends AbstractOperation.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new AddOperation()); + } + + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AddRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/AddRequest.java new file mode 100644 index 0000000..df6ae40 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AddRequest.java @@ -0,0 +1,181 @@ + +package org.xbib.net.ldap; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * LDAP add request defined as: + * + *
+ * AddRequest ::= [APPLICATION 8] SEQUENCE {
+ * entry           LDAPDN,
+ * attributes      AttributeList }
+ *
+ * AttributeList ::= SEQUENCE OF attribute Attribute
+ * 
+ * + */ +public class AddRequest extends AbstractRequestMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 8; + + /** + * LDAP DN to add. + */ + private String ldapDn; + + /** + * Attributes to add to the entry. + */ + private LdapAttribute[] attributes; + + + /** + * Default constructor. + */ + private AddRequest() { + } + + + /** + * Creates a new add request. + * + * @param dn DN to add + * @param attrs to add to the entry + */ + public AddRequest(final String dn, final LdapAttribute... attrs) { + ldapDn = dn; + attributes = attrs; + } + + + /** + * Creates a new add request. + * + * @param dn DN to add + * @param attrs to add to the entry + */ + public AddRequest(final String dn, final Collection attrs) { + ldapDn = dn; + attributes = attrs.toArray(LdapAttribute[]::new); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the DN. + * + * @return DN + */ + public String getDn() { + return ldapDn; + } + + /** + * Returns the attributes. + * + * @return add attributes + */ + public LdapAttribute[] getAttributes() { + return attributes; + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new OctetStringType(ldapDn), + new ConstructedDEREncoder( + UniversalDERTag.SEQ, + Stream.of(attributes).map(a -> + new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new OctetStringType(a.getName()), + new ConstructedDEREncoder( + UniversalDERTag.SET, + a.getBinaryValues().stream().map( + OctetStringType::new).toArray(DEREncoder[]::new)))).toArray(DEREncoder[]::new))), + }; + } + + @Override + public String toString() { + return super.toString() + ", " + "dn=" + ldapDn + ", " + "attributes=" + Arrays.toString(attributes); + } + + /** + * Add request builder. + */ + public static class Builder extends AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new AddRequest()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the ldap DN. + * + * @param dn ldap DN + * @return this builder + */ + public Builder dn(final String dn) { + object.ldapDn = dn; + return self(); + } + + + /** + * Sets the attributes. + * + * @param attrs attributes + * @return this builder + */ + public Builder attributes(final LdapAttribute... attrs) { + object.attributes = attrs; + return self(); + } + + + /** + * Sets the attributes. + * + * @param attrs attributes + * @return this builder + */ + public Builder attributes(final Collection attrs) { + object.attributes = attrs.toArray(LdapAttribute[]::new); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AddResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/AddResponse.java new file mode 100644 index 0000000..75ce0b5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AddResponse.java @@ -0,0 +1,117 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; + +/** + * LDAP add response defined as: + * + *
+ * AddResponse ::= [APPLICATION 9] LDAPResult
+ * 
+ * + */ +public class AddResponse extends AbstractResult { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 9; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10211; + + /** + * DER path to result code. + */ + private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(9)/ENUM[0]"); + + /** + * DER path to matched DN. + */ + private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(9)/OCTSTR[1]"); + + /** + * DER path to diagnostic message. + */ + private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(9)/OCTSTR[2]"); + + /** + * DER path to referral. + */ + private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(9)/CTX(3)/OCTSTR[0]"); + + + /** + * Default constructor. + */ + private AddResponse() { + } + + + /** + * Creates a new add response. + * + * @param buffer to decode + */ + public AddResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this)); + parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this)); + parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this)); + parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof AddResponse && super.equals(o); + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs()); + } + + // CheckStyle:OFF + public static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new AddResponse()); + } + + + @Override + protected Builder self() { + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AnonymousBindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/AnonymousBindRequest.java new file mode 100644 index 0000000..36eeb3d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AnonymousBindRequest.java @@ -0,0 +1,59 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextType; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP anonymous bind request. + * + */ +public class AnonymousBindRequest extends AbstractRequestMessage implements BindRequest { + + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new IntegerType(VERSION), + new OctetStringType(""), + new ContextType(0, (byte[]) null)), + }; + } + + /** + * Simple bind request builder. + */ + public static class Builder extends + AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new AnonymousBindRequest()); + } + + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/AttributeModification.java b/net-ldap/src/main/java/org/xbib/net/ldap/AttributeModification.java new file mode 100644 index 0000000..e8028b5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/AttributeModification.java @@ -0,0 +1,76 @@ + +package org.xbib.net.ldap; + +/** + * LDAP modification defined as: + * + *
+ * modification    PartialAttribute
+ *
+ * PartialAttribute ::= SEQUENCE {
+ * type       AttributeDescription,
+ * vals       SET OF value AttributeValue }
+ * 
+ * + */ +public class AttributeModification { + /** + * Modification type. + */ + private final Type operation; + /** + * Attribute to modify. + */ + private final LdapAttribute attribute; + + /** + * Creates a new modification. + * + * @param type of modification + * @param attr attribute to modify + */ + public AttributeModification(final Type type, final LdapAttribute attr) { + operation = type; + attribute = attr; + } + + public Type getOperation() { + return operation; + } + + public LdapAttribute getAttribute() { + return attribute; + } + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + "operation=" + operation + ", " + "attribute=" + attribute; + } + + + /** + * Modification type. + */ + public enum Type { + + /** + * Add a new attribute. + */ + ADD, + + /** + * Delete an attribute. + */ + DELETE, + + /** + * Replace an attribute. + */ + REPLACE, + + /** + * Increment the value of an attribute. + */ + INCREMENT, + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/BindConnectionInitializer.java b/net-ldap/src/main/java/org/xbib/net/ldap/BindConnectionInitializer.java new file mode 100644 index 0000000..6949e41 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/BindConnectionInitializer.java @@ -0,0 +1,279 @@ + +package org.xbib.net.ldap; + +import java.util.Arrays; +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.sasl.CramMD5BindRequest; +import org.xbib.net.ldap.sasl.DigestMD5BindRequest; +import org.xbib.net.ldap.sasl.GssApiBindRequest; +import org.xbib.net.ldap.sasl.Mechanism; +import org.xbib.net.ldap.sasl.SaslBindRequest; +import org.xbib.net.ldap.sasl.SaslConfig; +import org.xbib.net.ldap.sasl.ScramBindRequest; + +/** + * Initializes a connection by performing a bind operation. Useful if you need all connections to bind as the same + * principal. + * + */ +public class BindConnectionInitializer implements ConnectionInitializer { + + /** + * DN to bind as before performing operations. + */ + private String bindDn; + + /** + * Credential for the bind DN. + */ + private Credential bindCredential; + + /** + * Configuration for bind SASL authentication. + */ + private SaslConfig bindSaslConfig; + + /** + * Bind controls. + */ + private RequestControl[] bindControls; + + + /** + * Default constructor. + */ + public BindConnectionInitializer() { + } + + + /** + * Creates a new bind connection initializer. + * + * @param dn bind dn + * @param credential bind credential + */ + public BindConnectionInitializer(final String dn, final String credential) { + setBindDn(dn); + setBindCredential(new Credential(credential)); + } + + + /** + * Creates a new bind connection initializer. + * + * @param dn bind dn + * @param credential bind credential + */ + public BindConnectionInitializer(final String dn, final Credential credential) { + setBindDn(dn); + setBindCredential(credential); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the bind DN. + * + * @return DN to bind as + */ + public String getBindDn() { + return bindDn; + } + + /** + * Sets the bind DN to authenticate as before performing operations. + * + * @param dn to bind as + */ + public void setBindDn(final String dn) { + bindDn = dn; + } + + /** + * Returns the credential used with the bind DN. + * + * @return bind DN credential + */ + public Credential getBindCredential() { + return bindCredential; + } + + /** + * Sets the credential of the bind DN. + * + * @param credential to use with bind DN + */ + public void setBindCredential(final Credential credential) { + bindCredential = credential; + } + + /** + * Returns the bind sasl config. + * + * @return sasl config + */ + public SaslConfig getBindSaslConfig() { + return bindSaslConfig; + } + + /** + * Sets the bind sasl config. + * + * @param config sasl config + */ + public void setBindSaslConfig(final SaslConfig config) { + bindSaslConfig = config; + } + + /** + * Returns the bind controls. + * + * @return controls + */ + public RequestControl[] getBindControls() { + return bindControls; + } + + /** + * Sets the bind controls. + * + * @param cntrls controls to set + */ + public void setBindControls(final RequestControl... cntrls) { + bindControls = cntrls; + } + + @Override + public Result initialize(final Connection c) + throws LdapException { + final Result result; + if (bindSaslConfig != null) { + switch (bindSaslConfig.getMechanism()) { + case EXTERNAL: + result = c.operation(SaslBindRequest.builder() + .mechanism(Mechanism.EXTERNAL.mechanism()) + .credentials(bindSaslConfig.getAuthorizationId() != null ? bindSaslConfig.getAuthorizationId() : "") + .controls(bindControls).build()).execute(); + break; + case DIGEST_MD5: + result = c.operation(new DigestMD5BindRequest( + bindDn, + bindSaslConfig.getAuthorizationId(), + bindCredential != null ? bindCredential.getString() : null, + bindSaslConfig.getRealm(), + DigestMD5BindRequest.createProperties(bindSaslConfig))); + break; + case CRAM_MD5: + result = c.operation(new CramMD5BindRequest( + bindDn, + bindCredential != null ? bindCredential.getString() : null)); + break; + case GSSAPI: + result = c.operation(new GssApiBindRequest( + bindDn, + bindSaslConfig.getAuthorizationId(), + bindCredential != null ? bindCredential.getString() : null, + bindSaslConfig.getRealm(), + GssApiBindRequest.createProperties(bindSaslConfig))); + break; + case SCRAM_SHA_1: + result = c.operation(new ScramBindRequest(Mechanism.SCRAM_SHA_1, bindDn, bindCredential.getString())); + break; + case SCRAM_SHA_256: + result = c.operation(new ScramBindRequest(Mechanism.SCRAM_SHA_256, bindDn, bindCredential.getString())); + break; + case SCRAM_SHA_512: + result = c.operation(new ScramBindRequest(Mechanism.SCRAM_SHA_512, bindDn, bindCredential.getString())); + break; + default: + throw new IllegalStateException("Unknown SASL mechanism: " + bindSaslConfig.getMechanism()); + } + } else if (bindDn == null && bindCredential == null) { + result = c.operation(AnonymousBindRequest.builder() + .controls(bindControls).build()).execute(); + } else { + result = c.operation(SimpleBindRequest.builder() + .dn(bindDn) + .password(bindCredential.getString()) + .controls(bindControls).build()).execute(); + } + return result; + } + + /** + * Returns whether this connection initializer contains any configuration data. + * + * @return whether all properties are null + */ + public boolean isEmpty() { + return bindDn == null && bindCredential == null && bindSaslConfig == null && bindControls == null; + } + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "bindDn=" + bindDn + ", " + + "bindSaslConfig=" + bindSaslConfig + ", " + + "bindControls=" + Arrays.toString(bindControls); + } + + // CheckStyle:OFF + public static class Builder { + + + private final BindConnectionInitializer object = new BindConnectionInitializer(); + + + protected Builder() { + } + + + public Builder dn(final String dn) { + object.setBindDn(dn); + return this; + } + + + public Builder credential(final Credential credential) { + object.setBindCredential(credential); + return this; + } + + + public Builder credential(final String credential) { + object.setBindCredential(new Credential(credential)); + return this; + } + + + public Builder credential(final byte[] credential) { + object.setBindCredential(new Credential(credential)); + return this; + } + + + public Builder saslConfig(final SaslConfig config) { + object.setBindSaslConfig(config); + return this; + } + + + public Builder controls(final RequestControl... controls) { + object.setBindControls(controls); + return this; + } + + + public BindConnectionInitializer build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/BindOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/BindOperation.java new file mode 100644 index 0000000..98aa254 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/BindOperation.java @@ -0,0 +1,149 @@ + +package org.xbib.net.ldap; + +/** + * Executes an ldap bind operation. + * + */ +public class BindOperation extends AbstractOperation { + + + /** + * Default constructor. + */ + public BindOperation() { + } + + + /** + * Creates a new bind operation. + * + * @param factory connection factory + */ + public BindOperation(final ConnectionFactory factory) { + super(factory); + } + + /** + * Sends a bind request. See {@link OperationHandle#send()}. + * + * @param factory connection factory + * @param request bind request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + public static OperationHandle send( + final ConnectionFactory factory, + final BindRequest request) + throws LdapException { + final Connection conn = factory.getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return conn.operation(request).onComplete(conn::close).send(); + } + + /** + * Executes a bind request. See {@link OperationHandle#execute()}. + * + * @param factory connection factory + * @param request bind request + * @return bind result + * @throws LdapException if the connection cannot be opened + */ + public static BindResponse execute(final ConnectionFactory factory, final BindRequest request) + throws LdapException { + try (Connection conn = factory.getConnection()) { + conn.open(); + return conn.operation(request).execute(); + } + } + + /** + * Returns a new bind operation with the same properties as the supplied operation. + * + * @param operation to copy + * @return copy of the supplied bind operation + */ + public static BindOperation copy(final BindOperation operation) { + final BindOperation op = new BindOperation(); + op.setRequestHandlers(operation.getRequestHandlers()); + op.setResultHandlers(operation.getResultHandlers()); + op.setControlHandlers(operation.getControlHandlers()); + op.setReferralHandlers(operation.getReferralHandlers()); + op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers()); + op.setExceptionHandler(operation.getExceptionHandler()); + op.setThrowCondition(operation.getThrowCondition()); + op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers()); + op.setConnectionFactory(operation.getConnectionFactory()); + return op; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Sends a bind request. See {@link OperationHandle#send()}. + * + * @param request bind request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + @Override + public OperationHandle send(final BindRequest request) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send(); + } + + /** + * Executes a bind request. See {@link OperationHandle#execute()}. + * + * @param request bind request + * @return bind result + * @throws LdapException if the connection cannot be opened + */ + @Override + public BindResponse execute(final BindRequest request) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + return configureHandle(conn.operation(configureRequest(request))).execute(); + } + } + + /** + * Bind operation builder. + */ + public static class Builder extends AbstractOperation.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new BindOperation()); + } + + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/BindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/BindRequest.java new file mode 100644 index 0000000..62b3196 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/BindRequest.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap; + +/** + * LDAP bind request defined as: + * + *
+ * BindRequest ::= [APPLICATION 0] SEQUENCE {
+ * version                 INTEGER (1 ..  127),
+ * name                    LDAPDN,
+ * authentication          AuthenticationChoice }
+ *
+ * AuthenticationChoice ::= CHOICE {
+ * simple                  [0] OCTET STRING,
+ * -- 1 and 2 reserved
+ * sasl                    [3] SaslCredentials,
+ * ...  }
+ *
+ * SaslCredentials ::= SEQUENCE {
+ * mechanism               LDAPString,
+ * credentials             OCTET STRING OPTIONAL }
+ * 
+ * + */ +// CheckStyle:InterfaceIsType OFF +public interface BindRequest extends Request { + + /** + * BER protocol number. + */ + int PROTOCOL_OP = 0; + + /** + * bind protocol version. + */ + int VERSION = 3; +} +// CheckStyle:InterfaceIsType ON diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/BindResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/BindResponse.java new file mode 100644 index 0000000..ce007d7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/BindResponse.java @@ -0,0 +1,173 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; + +/** + * LDAP bind response defined as: + * + *
+ * BindResponse ::= [APPLICATION 1] SEQUENCE {
+ * COMPONENTS OF LDAPResult,
+ * serverSaslCreds    [7] OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class BindResponse extends AbstractResult { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 1; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10243; + + /** + * DER path to result code. + */ + private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(1)/ENUM[0]"); + + /** + * DER path to matched DN. + */ + private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(1)/OCTSTR[1]"); + + /** + * DER path to diagnostic message. + */ + private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(1)/OCTSTR[2]"); + + /** + * DER path to referral. + */ + private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(1)/CTX(3)/OCTSTR[0]"); + + /** + * DER path to SASL credentials. + */ + private static final DERPath SASL_CREDENTIALS_PATH = new DERPath("/SEQ/APP(1)/CTX(7)"); + + /** + * Server SASL credentials. + */ + private byte[] serverSaslCreds; + + + /** + * Default constructor. + */ + private BindResponse() { + } + + + /** + * Creates a new bind response. + * + * @param buffer to decode + */ + public BindResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this)); + parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this)); + parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this)); + parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this)); + parser.registerHandler(SASL_CREDENTIALS_PATH, new SASLCredsHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + public byte[] getServerSaslCreds() { + return serverSaslCreds; + } + + public void setServerSaslCreds(final byte[] creds) { + serverSaslCreds = creds; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof BindResponse v && super.equals(o)) { + return LdapUtils.areEqual(serverSaslCreds, v.serverSaslCreds); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs(), + serverSaslCreds); + } + + /** + * Parse handler implementation for the server SASL creds. + */ + protected static class SASLCredsHandler extends AbstractParseHandler { + + + /** + * Creates a new server SASL creds handler. + * + * @param response to configure + */ + SASLCredsHandler(final BindResponse response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + if (encoded.remaining() > 0) { + getObject().setServerSaslCreds(encoded.getRemainingBytes()); + } + } + } + + // CheckStyle:OFF + public static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new BindResponse()); + } + + + @Override + protected Builder self() { + return this; + } + + + public Builder serverSaslCreds(final byte[] creds) { + object.setServerSaslCreds(creds); + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ClosedRetryMetadata.java b/net-ldap/src/main/java/org/xbib/net/ldap/ClosedRetryMetadata.java new file mode 100644 index 0000000..e3029c1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ClosedRetryMetadata.java @@ -0,0 +1,44 @@ + +package org.xbib.net.ldap; + +import java.time.Instant; + +/** + * Retry metadata used when a connection is unexpectedly closed. + * + */ +public class ClosedRetryMetadata extends AbstractRetryMetadata { + + /** + * Last thrown exception. + */ + protected final Throwable failureException; + + + /** + * Creates a new closed retry metadata. + * + * @param time of last successful connection + * @param ex exception that caused the connection to close + */ + public ClosedRetryMetadata(final Instant time, final Throwable ex) { + successTime = time; + failureException = ex; + } + + + /** + * Returns the exception that caused the closed connection. + * + * @return failure exception + */ + public Throwable getFailureException() { + return failureException; + } + + + @Override + public String toString() { + return super.toString() + ", " + "failureException=" + failureException; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/CompareConnectionValidator.java b/net-ldap/src/main/java/org/xbib/net/ldap/CompareConnectionValidator.java new file mode 100644 index 0000000..bce53fd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/CompareConnectionValidator.java @@ -0,0 +1,108 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; + +/** + * Validates a connection is healthy by performing a compare operation. Unless {@link + * #setValidResultCodes(ResultCode...)} is set, validation is considered successful if the compare result contains any + * result code. + * + */ +public class CompareConnectionValidator extends AbstractOperationConnectionValidator { + + + /** + * Creates a new compare validator. + */ + public CompareConnectionValidator() { + this(CompareRequest.builder().dn("").name("objectClass").value("top").build()); + } + + + /** + * Creates a new compare validator. + * + * @param cr to use for compares + */ + public CompareConnectionValidator(final CompareRequest cr) { + this(DEFAULT_VALIDATE_PERIOD, DEFAULT_VALIDATE_TIMEOUT, cr); + } + + + /** + * Creates a new compare validator. + * + * @param period execution period + * @param timeout execution timeout + * @param request to use for searches + */ + public CompareConnectionValidator(final Duration period, final Duration timeout, final CompareRequest request) { + setValidatePeriod(period); + setValidateTimeout(timeout); + setRequest(request); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the compare request. + * + * @return compare request + * @deprecated use {@link AbstractOperationConnectionValidator#getRequest()} + */ + @Deprecated + public CompareRequest getCompareRequest() { + return getRequest(); + } + + /** + * Sets the compare request. + * + * @param cr compare request + * @deprecated use {@link AbstractOperationConnectionValidator#setRequest(Request)} + */ + @Deprecated + public void setCompareRequest(final CompareRequest cr) { + setRequest(cr); + } + + @Override + protected OperationHandle performOperation(final Connection conn) { + return conn.operation(getRequest()); + } + + @Override + public String toString() { + return "[" + super.toString() + "]"; + } + + /** + * Compare validator builder. + */ + public static class Builder extends + AbstractOperationConnectionValidator.AbstractBuilder< + CompareRequest, CompareResponse, CompareConnectionValidator.Builder, CompareConnectionValidator> { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new CompareConnectionValidator()); + } + + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/CompareOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/CompareOperation.java new file mode 100644 index 0000000..496ac1a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/CompareOperation.java @@ -0,0 +1,199 @@ + +package org.xbib.net.ldap; + +import java.util.Arrays; +import org.xbib.net.ldap.handler.CompareValueHandler; + +/** + * Executes an ldap compare operation. + * + */ +public class CompareOperation extends AbstractOperation { + + /** + * Functions to handle the compare result. + */ + private CompareValueHandler[] compareValueHandlers; + + + /** + * Default constructor. + */ + public CompareOperation() { + } + + + /** + * Creates a new compare operation. + * + * @param factory connection factory + */ + public CompareOperation(final ConnectionFactory factory) { + super(factory); + } + + /** + * Sends a compare request. See {@link OperationHandle#send()}. + * + * @param factory connection factory + * @param request compare request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + public static CompareOperationHandle send(final ConnectionFactory factory, final CompareRequest request) + throws LdapException { + final Connection conn = factory.getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return conn.operation(request).onComplete(conn::close).send(); + } + + /** + * Executes a compare request. See {@link OperationHandle#execute()}. + * + * @param factory connection factory + * @param request compare request + * @return compare result + * @throws LdapException if the connection cannot be opened + */ + public static CompareResponse execute(final ConnectionFactory factory, final CompareRequest request) + throws LdapException { + try (Connection conn = factory.getConnection()) { + conn.open(); + return conn.operation(request).execute(); + } + } + + /** + * Returns a new compare operation with the same properties as the supplied operation. + * + * @param operation to copy + * @return copy of the supplied compare operation + */ + public static CompareOperation copy(final CompareOperation operation) { + final CompareOperation op = new CompareOperation(); + op.setRequestHandlers(operation.getRequestHandlers()); + op.setResultHandlers(operation.getResultHandlers()); + op.setControlHandlers(operation.getControlHandlers()); + op.setReferralHandlers(operation.getReferralHandlers()); + op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers()); + op.setExceptionHandler(operation.getExceptionHandler()); + op.setThrowCondition(operation.getThrowCondition()); + op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers()); + op.setConnectionFactory(operation.getConnectionFactory()); + op.setCompareValueHandlers(operation.getCompareValueHandlers()); + return op; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + public CompareValueHandler[] getCompareValueHandlers() { + return compareValueHandlers; + } + + public void setCompareValueHandlers(final CompareValueHandler... handlers) { + compareValueHandlers = handlers; + } + + /** + * Sends a compare request. See {@link OperationHandle#send()}. + * + * @param request compare request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + @Override + public CompareOperationHandle send(final CompareRequest request) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send(); + } + + /** + * Executes a compare request. See {@link OperationHandle#execute()}. + * + * @param request compare request + * @return compare result + * @throws LdapException if the connection cannot be opened + */ + @Override + public CompareResponse execute(final CompareRequest request) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + return configureHandle(conn.operation(configureRequest(request))).execute(); + } + } + + /** + * Adds configured functions to the supplied handle. + * + * @param handle to configure + * @return configured handle + */ + protected CompareOperationHandle configureHandle(final CompareOperationHandle handle) { + return handle + .onCompare(getCompareValueHandlers()) + .onControl(getControlHandlers()) + .onReferral(getReferralHandlers()) + .onIntermediate(getIntermediateResponseHandlers()) + .onException(getExceptionHandler()) + .throwIf(getThrowCondition()) + .onUnsolicitedNotification(getUnsolicitedNotificationHandlers()) + .onResult(getResultHandlers()); + } + + @Override + public String toString() { + return super.toString() + ", " + "compareValueHandlers=" + Arrays.toString(compareValueHandlers); + } + + /** + * Compare operation builder. + */ + public static class Builder extends AbstractOperation.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new CompareOperation()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the functions to execute when a compare result is complete. + * + * @param handlers to execute on a compare result + * @return this builder + */ + public Builder onCompare(final CompareValueHandler... handlers) { + object.setCompareValueHandlers(handlers); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/CompareOperationHandle.java b/net-ldap/src/main/java/org/xbib/net/ldap/CompareOperationHandle.java new file mode 100644 index 0000000..cdd39c2 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/CompareOperationHandle.java @@ -0,0 +1,75 @@ + +package org.xbib.net.ldap; + +import org.xbib.net.ldap.handler.CompareValueHandler; +import org.xbib.net.ldap.handler.CompleteHandler; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Handle that notifies on the components of a compare request. + * + */ +public interface CompareOperationHandle extends OperationHandle { + + + @Override + CompareOperationHandle send(); + + + @Override + CompareResponse await() throws LdapException; + + + @Override + default CompareResponse execute() + throws LdapException { + return send().await(); + } + + + @Override + CompareOperationHandle onResult(ResultHandler... function); + + + @Override + CompareOperationHandle onControl(ResponseControlHandler... function); + + + @Override + CompareOperationHandle onReferral(ReferralHandler... function); + + + @Override + CompareOperationHandle onIntermediate(IntermediateResponseHandler... function); + + + @Override + CompareOperationHandle onUnsolicitedNotification(UnsolicitedNotificationHandler... function); + + + @Override + CompareOperationHandle onException(ExceptionHandler function); + + + @Override + CompareOperationHandle throwIf(ResultPredicate function); + + + @Override + CompareOperationHandle onComplete(CompleteHandler function); + + + /** + * Sets the function to execute when a compare result is received. + * + * @param function to execute on a compare result + * @return this handle + */ + CompareOperationHandle onCompare(CompareValueHandler... function); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/CompareRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/CompareRequest.java new file mode 100644 index 0000000..90b466f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/CompareRequest.java @@ -0,0 +1,204 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * LDAP compare request defined as: + * + *
+ * CompareRequest ::= [APPLICATION 14] SEQUENCE {
+ * entry           LDAPDN,
+ * ava             AttributeValueAssertion }
+ * 
+ * + */ +public class CompareRequest extends AbstractRequestMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 14; + + /** + * LDAP DN to compare. + */ + private String ldapDn; + + /** + * Attribute description + */ + private String attributeDesc; + + /** + * Assertion value. + */ + private String assertionValue; + + + /** + * Default constructor. + */ + public CompareRequest() { + } + + + /** + * Creates a new compare request. + * + * @param dn to compare + * @param name attribute description + * @param value assertion value + */ + public CompareRequest(final String dn, final String name, final String value) { + ldapDn = dn; + attributeDesc = name; + assertionValue = value; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the DN. + * + * @return DN + */ + public String getDn() { + return ldapDn; + } + + /** + * Sets the DN. + * + * @param dn ldapDn to set + */ + public void setDn(final String dn) { + ldapDn = dn; + } + + /** + * Returns the name. + * + * @return name + */ + public String getName() { + return attributeDesc; + } + + /** + * Sets the name. + * + * @param name attributeDesc to set + */ + public void setName(final String name) { + attributeDesc = name; + } + + /** + * Returns the value. + * + * @return value + */ + public String getValue() { + return assertionValue; + } + + /** + * Sets the value. + * + * @param value assertionValue to set + */ + public void setValue(final String value) { + assertionValue = value; + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new OctetStringType(ldapDn), + new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new OctetStringType(attributeDesc), + new OctetStringType(assertionValue))), + }; + } + + @Override + public String toString() { + return super.toString() + ", " + + "dn=" + ldapDn + ", " + + "attributeDesc=" + attributeDesc + ", " + + "assertionValue=" + ("userPassword".equalsIgnoreCase(attributeDesc) ? "" : assertionValue); + } + + /** + * Compare request builder. + */ + public static class Builder extends AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new CompareRequest()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the ldap DN. + * + * @param dn ldap DN + * @return this builder + */ + public Builder dn(final String dn) { + object.ldapDn = dn; + return self(); + } + + + /** + * Sets the attribute description. + * + * @param name attribute description + * @return this builder + */ + public Builder name(final String name) { + object.attributeDesc = name; + return self(); + } + + + /** + * Sets the assertion value. + * + * @param value assertion value + * @return this builder + */ + public Builder value(final String value) { + object.assertionValue = value; + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/CompareResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/CompareResponse.java new file mode 100644 index 0000000..a7b221f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/CompareResponse.java @@ -0,0 +1,135 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; + +/** + * LDAP compare response defined as: + * + *
+ * CompareResponse ::= [APPLICATION 15] LDAPResult
+ * 
+ * + */ +public class CompareResponse extends AbstractResult { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 15; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10247; + + /** + * DER path to result code. + */ + private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(15)/ENUM[0]"); + + /** + * DER path to matched DN. + */ + private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(15)/OCTSTR[1]"); + + /** + * DER path to diagnostic message. + */ + private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(15)/OCTSTR[2]"); + + /** + * DER path to referral. + */ + private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(15)/CTX(3)/OCTSTR[0]"); + + + /** + * Default constructor. + */ + private CompareResponse() { + } + + + /** + * Creates a new compare response. + * + * @param buffer to decode + */ + public CompareResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this)); + parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this)); + parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this)); + parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns whether the result code in this result is {@link ResultCode#COMPARE_TRUE}. + * + * @return whether this result is compare true + */ + public boolean isTrue() { + return ResultCode.COMPARE_TRUE == getResultCode(); + } + + /** + * Returns whether the result code in this result is {@link ResultCode#COMPARE_FALSE}. + * + * @return whether this result is compare false + */ + public boolean isFalse() { + return ResultCode.COMPARE_FALSE == getResultCode(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof CompareResponse && super.equals(o); + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs()); + } + + // CheckStyle:OFF + public static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new CompareResponse()); + } + + + @Override + protected Builder self() { + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ConnectException.java b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectException.java new file mode 100644 index 0000000..2227d4d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectException.java @@ -0,0 +1,48 @@ + +package org.xbib.net.ldap; + +/** + * Exception that indicates a connection attempt failed. + * + */ +public class ConnectException extends LdapException { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = -5935483002226156942L; + + + /** + * Creates a new connect exception. + * + * @param code result code describing this exception + * @param msg describing this exception + */ + public ConnectException(final ResultCode code, final String msg) { + super(code, msg); + } + + + /** + * Creates a new connect exception. + * + * @param code result code describing this exception + * @param e underlying exception + */ + public ConnectException(final ResultCode code, final Throwable e) { + super(code, e); + } + + + /** + * Creates a new connect exception. + * + * @param code result code describing this exception + * @param msg describing this exception + * @param e underlying exception + */ + public ConnectException(final ResultCode code, final String msg, final Throwable e) { + super(code, msg, e); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/Connection.java b/net-ldap/src/main/java/org/xbib/net/ldap/Connection.java new file mode 100644 index 0000000..65c029f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/Connection.java @@ -0,0 +1,156 @@ + +package org.xbib.net.ldap; + +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.extended.ExtendedOperationHandle; +import org.xbib.net.ldap.extended.ExtendedRequest; +import org.xbib.net.ldap.sasl.DefaultSaslClientRequest; +import org.xbib.net.ldap.sasl.SaslClientRequest; + +/** + * Interface for connection implementations. + * + */ +public interface Connection extends AutoCloseable { + + + /** + * Executes an abandon operation. Clients should execute abandons using {@link OperationHandle#abandon()}. + * + * @param request abandon request + */ + void operation(AbandonRequest request); + + + /** + * Creates a handle for an add operation. + * + * @param request add request + * @return operation handle + */ + OperationHandle operation(AddRequest request); + + + /** + * Creates a handle for a bind operation. Since clients must not send requests while a bind is in progress, some + * methods may not be supported on the operation handle. + * + * @param request bind request + * @return operation handle + */ + OperationHandle operation(BindRequest request); + + + /** + * Creates a handle for a compare operation. + * + * @param request compare request + * @return compare operation handle + */ + CompareOperationHandle operation(CompareRequest request); + + + /** + * Creates a handle for a delete operation. + * + * @param request delete request + * @return operation handle + */ + OperationHandle operation(DeleteRequest request); + + + /** + * Creates a handle for an extended operation. + * + * @param request extended request + * @return extended operation handle + */ + ExtendedOperationHandle operation(ExtendedRequest request); + + + /** + * Creates a handle for a modify operation. + * + * @param request modify request + * @return operation handle + */ + OperationHandle operation(ModifyRequest request); + + + /** + * Creates a handle for a modify dn operation. + * + * @param request modify dn request + * @return operation handle + */ + OperationHandle operation(ModifyDnRequest request); + + + /** + * Creates a handle for a search operation. + * + * @param request search request + * @return search operation handle + */ + SearchOperationHandle operation(SearchRequest request); + + + /** + * Returns the result of a SASL request that requires use of a generic SASL client. + * + * @param request SASL client request + * @return operation result + * @throws LdapException if the operation fails or another bind is in progress + */ + BindResponse operation(SaslClientRequest request) throws LdapException; + + + /** + * Returns the result of a SASL request that requires use of the default SASL client. This includes CRAM-MD5, + * DIGEST-MD5, and GSS-API. + * + * @param request default SASL client request + * @return operation result + * @throws LdapException if the operation fails or another bind is in progress + */ + BindResponse operation(DefaultSaslClientRequest request) throws LdapException; + + + /** + * Returns the URL that was selected for this connection. The existence of this value does not indicate a current + * established connection. + * + * @return LDAP URL + */ + LdapURL getLdapURL(); + + + /** + * Returns whether this connection is open. + * + * @return whether this connection is open + */ + boolean isOpen(); + + + /** + * Opens the connection. + * + * @throws LdapException if an error occurs opening the connection + */ + void open() throws LdapException; + + + @Override + default void close() { + close((RequestControl[]) null); + } + + + /** + * Closes the connection. + * + * @param controls to send when closing the connection + */ + void close(RequestControl... controls); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionConfig.java b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionConfig.java new file mode 100644 index 0000000..14a691c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionConfig.java @@ -0,0 +1,601 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; +import org.xbib.net.ldap.ssl.SslConfig; + +/** + * Contains all the configuration data needed to control connections. + * + */ +public class ConnectionConfig extends AbstractConfig { + + /** + * Predicate that attempts a single reconnect. + */ + public static final Predicate ONE_RECONNECT_ATTEMPT = + new Predicate<>() { + @Override + public boolean test(final RetryMetadata metadata) { + return metadata instanceof ClosedRetryMetadata && metadata.getAttempts() == 0; + } + + @Override + public String toString() { + return "ONE_RECONNECT_ATTEMPT"; + } + }; + + /** + * Predicate that attempts to reconnect forever, waiting for 5 seconds after the first attempt. + */ + public static final Predicate INFINITE_RECONNECT_ATTEMPTS = + new Predicate<>() { + @Override + public boolean test(final RetryMetadata metadata) { + if (metadata instanceof ClosedRetryMetadata) { + if (metadata.getAttempts() > 0) { + try { + // CheckStyle:MagicNumber OFF + Thread.sleep(Duration.ofSeconds(5).toMillis()); + // CheckStyle:MagicNumber ON + } catch (InterruptedException ignored) { + } + } + return true; + } + return false; + } + + @Override + public String toString() { + return "INFINITE_RECONNECT_ATTEMPTS"; + } + }; + + /** + * Predicate that attempts to reconnect forever, backing off in 5 second intervals after the first attempt. + */ + public static final Predicate INFINITE_RECONNECT_ATTEMPTS_WITH_BACKOFF = + new Predicate<>() { + @Override + public boolean test(final RetryMetadata metadata) { + if (metadata instanceof ClosedRetryMetadata) { + if (metadata.getAttempts() > 0) { + try { + // CheckStyle:MagicNumber OFF + Thread.sleep(Duration.ofSeconds(5).multipliedBy(metadata.getAttempts()).toMillis()); + // CheckStyle:MagicNumber ON + } catch (InterruptedException ignored) { + } + } + return true; + } + return false; + } + + @Override + public String toString() { + return "INFINITE_RECONNECT_ATTEMPTS_WITH_BACKOFF"; + } + }; + /** + * Transport options. + */ + private final Map transportOptions = new HashMap<>(); + /** + * URL to the LDAP(s). + */ + private String ldapUrl; + /** + * Duration of time that connects will block. + */ + private Duration connectTimeout = Duration.ofMinutes(1); + /** + * Duration of time to wait for startTLS responses. + */ + private Duration startTLSTimeout = Duration.ofMinutes(1); + /** + * Duration of time to wait for responses. + */ + private Duration responseTimeout = Duration.ofMinutes(1); + /** + * Duration of time that operations will block on reconnects, should generally be longer than {@link + * #connectTimeout}. + */ + private Duration reconnectTimeout = Duration.ofMinutes(2); + /** + * Whether to automatically reconnect to the server when a connection is lost. Default is true. + */ + private boolean autoReconnect = true; + /** + * Condition used to determine whether another reconnect attempt should be made. Default makes a single attempt only + * if the connection was previously opened. + */ + private Predicate autoReconnectCondition = ONE_RECONNECT_ATTEMPT; + /** + * Whether pending operations should be replayed after a reconnect. Default is false. + */ + private boolean autoReplay; + /** + * Configuration for SSL and startTLS connections. + */ + private SslConfig sslConfig; + /** + * Connect to LDAP using startTLS. + */ + private boolean useStartTLS; + /** + * Connection initializers to execute on {@link Connection#open()}. + */ + private ConnectionInitializer[] connectionInitializers; + /** + * Connection strategy. + */ + private ConnectionStrategy connectionStrategy = new ActivePassiveConnectionStrategy(); + /** + * Connection validator. + */ + private ConnectionValidator connectionValidator; + + + /** + * Default constructor. + */ + public ConnectionConfig() { + } + + + /** + * Creates a new connection config. + * + * @param url to connect to + */ + public ConnectionConfig(final String url) { + setLdapUrl(url); + } + + /** + * Returns a new connection config initialized with the supplied config. + * + * @param config connection config to read properties from + * @return connection config + */ + public static ConnectionConfig copy(final ConnectionConfig config) { + final ConnectionConfig cc = new ConnectionConfig(); + cc.setLdapUrl(config.getLdapUrl()); + cc.setConnectTimeout(config.getConnectTimeout()); + cc.setStartTLSTimeout(config.getStartTLSTimeout()); + cc.setResponseTimeout(config.getResponseTimeout()); + cc.setReconnectTimeout(config.getReconnectTimeout()); + cc.setAutoReconnect(config.getAutoReconnect()); + cc.setAutoReconnectCondition(config.getAutoReconnectCondition()); + cc.setAutoReplay(config.getAutoReplay()); + cc.setSslConfig(config.getSslConfig() != null ? SslConfig.copy(config.getSslConfig()) : null); + cc.setUseStartTLS(config.getUseStartTLS()); + cc.setConnectionInitializers(config.getConnectionInitializers()); + cc.setConnectionStrategy( + config.getConnectionStrategy() != null ? config.getConnectionStrategy().newInstance() : null); + cc.setConnectionValidator(config.getConnectionValidator()); + cc.setTransportOptions(config.getTransportOptions()); + return cc; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the ldap url. + * + * @return ldap url + */ + public String getLdapUrl() { + return ldapUrl; + } + + /** + * Sets the ldap url. + * + * @param url of the ldap + */ + public void setLdapUrl(final String url) { + checkStringInput(url, true); + ldapUrl = url; + } + + /** + * Returns the connect timeout. + * + * @return timeout + */ + public Duration getConnectTimeout() { + return connectTimeout; + } + + /** + * Sets the maximum amount of time that connects will block. + * + * @param time timeout for connects + */ + public void setConnectTimeout(final Duration time) { + if (time == null || time.isNegative()) { + throw new IllegalArgumentException("Connect timeout cannot be null or negative"); + } + connectTimeout = time; + } + + /** + * Returns the startTLS timeout. + * + * @return timeout + */ + public Duration getStartTLSTimeout() { + return startTLSTimeout; + } + + /** + * Sets the maximum amount of time that startTLS operations will wait for a response. + * + * @param time timeout for responses + */ + public void setStartTLSTimeout(final Duration time) { + if (time == null || time.isNegative()) { + throw new IllegalArgumentException("StartTLS timeout cannot be null or negative"); + } + startTLSTimeout = time; + } + + /** + * Returns the response timeout. + * + * @return timeout + */ + public Duration getResponseTimeout() { + return responseTimeout; + } + + /** + * Sets the maximum amount of time that operations will wait for a response. + * + * @param time timeout for responses + */ + public void setResponseTimeout(final Duration time) { + if (time == null || time.isNegative()) { + throw new IllegalArgumentException("Response timeout cannot be null or negative"); + } + responseTimeout = time; + } + + /** + * Returns the reconnect timeout. + * + * @return timeout + */ + public Duration getReconnectTimeout() { + return reconnectTimeout; + } + + /** + * Sets the maximum amount of time that operations will block waiting for a reconnect. + * + * @param time timeout for reconnects + */ + public void setReconnectTimeout(final Duration time) { + if (time == null || time.isNegative()) { + throw new IllegalArgumentException("Reconnect timeout cannot be null or negative"); + } + reconnectTimeout = time; + } + + /** + * Returns whether connections will attempt to reconnect. + * + * @return whether to automatically reconnect when a connection is lost + */ + public boolean getAutoReconnect() { + return autoReconnect; + } + + /** + * Sets whether connections will attempt to reconnect when unexpectedly closed. + * + * @param b whether to automatically reconnect when a connection is lost + */ + public void setAutoReconnect(final boolean b) { + autoReconnect = b; + } + + /** + * Returns the auto reconnect condition. + * + * @return auto reconnect condition + */ + public Predicate getAutoReconnectCondition() { + return autoReconnectCondition; + } + + /** + * Sets the auto reconnect condition. + * + * @param predicate to determine whether to attempt a reconnect + */ + public void setAutoReconnectCondition(final Predicate predicate) { + autoReconnectCondition = predicate; + } + + /** + * Returns whether operations should be replayed after a reconnect. + * + * @return whether to auto replay + */ + public boolean getAutoReplay() { + return autoReplay; + } + + /** + * Sets whether operations will be replayed after a reconnect. + * + * @param b whether to replay operations + */ + public void setAutoReplay(final boolean b) { + autoReplay = b; + } + + /** + * Returns the ssl config. + * + * @return ssl config + */ + public SslConfig getSslConfig() { + return sslConfig; + } + + /** + * Sets the ssl config. + * + * @param config ssl config + */ + public void setSslConfig(final SslConfig config) { + sslConfig = config; + } + + /** + * Returns whether startTLS will be used for connections. + * + * @return whether startTLS will be used + */ + public boolean getUseStartTLS() { + return useStartTLS; + } + + /** + * Sets whether startTLS will be used for connections. + * + * @param b whether startTLS will be used + */ + public void setUseStartTLS(final boolean b) { + useStartTLS = b; + } + + /** + * Returns the connection initializers. + * + * @return connection initializers + */ + public ConnectionInitializer[] getConnectionInitializers() { + return connectionInitializers; + } + + /** + * Sets the connection initializers. + * + * @param initializers connection initializers + */ + public void setConnectionInitializers(final ConnectionInitializer... initializers) { + checkArrayContainsNull(initializers); + connectionInitializers = initializers; + } + + /** + * Returns the connection strategy. + * + * @return strategy for making connections + */ + public ConnectionStrategy getConnectionStrategy() { + return connectionStrategy; + } + + /** + * Sets the connection strategy. + * + * @param strategy for making new connections + */ + public void setConnectionStrategy(final ConnectionStrategy strategy) { + connectionStrategy = strategy; + } + + /** + * Returns the connection validator. + * + * @return connection validator + */ + public ConnectionValidator getConnectionValidator() { + return connectionValidator; + } + + /** + * Sets the connection validator. + * + * @param validator for validating connections + */ + public void setConnectionValidator(final ConnectionValidator validator) { + connectionValidator = validator; + } + + /** + * Returns transport options. + * + * @return transport options + */ + public Map getTransportOptions() { + return transportOptions; + } + + /** + * Sets transport options. + * + * @param options to set + */ + public void setTransportOptions(final Map options) { + transportOptions.putAll(options); + } + + /** + * Returns a transport option. + * + * @param id transport option id + * @return transport option + */ + public Object getTransportOption(final String id) { + return transportOptions.get(id); + } + + /** + * Sets a transport option. + * + * @param id of the transport option + * @param value of the transport option + */ + public void setTransportOption(final String id, final Object value) { + transportOptions.put(id, value); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "ldapUrl=" + ldapUrl + ", " + + "connectTimeout=" + connectTimeout + ", " + + "startTLSTimeout=" + startTLSTimeout + ", " + + "responseTimeout=" + responseTimeout + ", " + + "reconnectTimeout=" + reconnectTimeout + ", " + + "autoReconnect=" + autoReconnect + ", " + + "autoReconnectCondition=" + autoReconnectCondition + ", " + + "autoReplay=" + autoReplay + ", " + + "sslConfig=" + sslConfig + ", " + + "useStartTLS=" + useStartTLS + ", " + + "connectionInitializers=" + Arrays.toString(connectionInitializers) + ", " + + "connectionStrategy=" + connectionStrategy + ", " + + "connectionValidator=" + connectionValidator + ", " + + "transportOptions=" + transportOptions + "]"; + } + + public static class Builder { + + private final ConnectionConfig object = new ConnectionConfig(); + + + protected Builder() { + } + + + public Builder url(final String url) { + object.setLdapUrl(url); + return this; + } + + + public Builder connectTimeout(final Duration timeout) { + object.setConnectTimeout(timeout); + return this; + } + + + public Builder responseTimeout(final Duration timeout) { + object.setResponseTimeout(timeout); + return this; + } + + + public Builder startTLSTimeout(final Duration timeout) { + object.setStartTLSTimeout(timeout); + return this; + } + + + public Builder reconnectTimeout(final Duration timeout) { + object.setReconnectTimeout(timeout); + return this; + } + + + public Builder autoReconnect(final boolean b) { + object.setAutoReconnect(b); + return this; + } + + + public Builder autoReconnectCondition(final Predicate predicate) { + object.setAutoReconnectCondition(predicate); + return this; + } + + + public Builder autoReplay(final boolean b) { + object.setAutoReplay(b); + return this; + } + + + public Builder sslConfig(final SslConfig config) { + object.setSslConfig(config); + return this; + } + + + public Builder useStartTLS(final boolean b) { + object.setUseStartTLS(b); + return this; + } + + + public Builder connectionInitializers(final ConnectionInitializer... initializers) { + object.setConnectionInitializers(initializers); + return this; + } + + + public Builder connectionStrategy(final ConnectionStrategy strategy) { + object.setConnectionStrategy(strategy); + return this; + } + + + public Builder connectionValidator(final ConnectionValidator validator) { + object.setConnectionValidator(validator); + return this; + } + + + public Builder transportOption(final String id, final Object value) { + object.setTransportOption(id, value); + return this; + } + + + public ConnectionConfig build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactory.java new file mode 100644 index 0000000..d55f4e5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactory.java @@ -0,0 +1,33 @@ + +package org.xbib.net.ldap; + +/** + * Interface for connection factories. + * + */ +public interface ConnectionFactory { + + + /** + * Creates a new connection. + * + * @return connection + * @throws LdapException if a connection cannot be returned + */ + Connection getConnection() + throws LdapException; + + + /** + * Returns the connection configuration used to create connections. + * + * @return connection config + */ + ConnectionConfig getConnectionConfig(); + + + /** + * Free any resources associated with this factory. + */ + void close(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactoryManager.java b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactoryManager.java new file mode 100644 index 0000000..3887c51 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactoryManager.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap; + +/** + * Interface for objects that manage an instance of connection factory. + * + */ +public interface ConnectionFactoryManager { + + + /** + * Returns the connection factory. + * + * @return connection factory + */ + ConnectionFactory getConnectionFactory(); + + + /** + * Sets the connection factory. + * + * @param cf connection factory + */ + void setConnectionFactory(ConnectionFactory cf); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactoryMetadata.java b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactoryMetadata.java new file mode 100644 index 0000000..317bf19 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionFactoryMetadata.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap; + +/** + * Interface to describe the state of the connection factory. Used by {@link ConnectionStrategy} to produce LDAP URLs. + * + */ +public interface ConnectionFactoryMetadata { + + + /** + * Returns the LDAP URL the connection factory is using. May be space delimited for multiple URLs. + * + * @return ldap url + */ + String getLdapUrl(); + + + /** + * Returns the number of times the connection factory has created a connection. + * + * @return connection count + */ + int getConnectionCount(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionInitializer.java b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionInitializer.java new file mode 100644 index 0000000..f736fad --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionInitializer.java @@ -0,0 +1,20 @@ + +package org.xbib.net.ldap; + +/** + * Provides an interface for initializing connections after they are opened. + * + */ +public interface ConnectionInitializer { + + + /** + * Initialize the supplied connection. + * + * @param conn connection to initialize + * @return result associated with the initialization or an empty result + * @throws LdapException if initialization fails + */ + Result initialize(Connection conn) + throws LdapException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionStrategy.java new file mode 100644 index 0000000..38fb44d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionStrategy.java @@ -0,0 +1,81 @@ + +package org.xbib.net.ldap; + +import java.util.function.Predicate; + +/** + * Interface to describe various connection strategies. Each strategy returns an ordered list of LDAP URLs to attempt + * when opening a connection. + * + */ +public interface ConnectionStrategy extends Iterable { + + + /** + * Populates a {@link LdapURLSet} from the URL string provided at configuration time. + * + * @param urls Space-delimited string of URLs describing the LDAP hosts to connect to. The URLs in the string + * are commonly {@code ldap://} or {@code ldaps://} URLs that directly describe the hosts to connect to, + * but may also describe a resource from which to obtain LDAP connection URLs as is the case for + * {@link DnsSrvConnectionStrategy} that use URLs with the scheme {@code dns:}. + * @param urlSet LDAP URL set to populate. + */ + void populate(String urls, LdapURLSet urlSet); + + + /** + * Prepare this strategy for use. + * + * @param urls LDAP URLs for this strategy + * @param activateCondition predicate to determine whether a connection is active + */ + void initialize(String urls, Predicate activateCondition); + + + /** + * Whether this strategy is ready for use. + * + * @return whether this strategy is ready for use + */ + boolean isInitialized(); + + + /** + * Returns the condition used to activate connections. + * + * @return activate condition + */ + Predicate getActivateCondition(); + + + /** + * Returns the condition used to determine whether to attempt to activate a connection. + * + * @return retry condition + */ + Predicate getRetryCondition(); + + + /** + * Indicates the supplied URL was successfully connected to. + * + * @param url which was successfully connected to + */ + void success(LdapURL url); + + + /** + * Indicates the supplied URL could not be connected to. + * + * @param url which was could not be connected to + */ + void failure(LdapURL url); + + + /** + * Create a deep copy of this strategy. + * + * @return new instance of this connection strategy + */ + ConnectionStrategy newInstance(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionValidator.java b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionValidator.java new file mode 100644 index 0000000..aa31e8a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ConnectionValidator.java @@ -0,0 +1,50 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Provides an interface for defining connection validation. + * + */ +public interface ConnectionValidator extends Function { + + + /** + * Provides an asynchronous implementation of {@link #apply(Object)}. The supplied consumer will be invoked with the + * validation result. {@link #getValidateTimeout()} must be enforced by the caller. + * + * @param conn to validate + * @param function to consume the validation result + */ + void applyAsync(Connection conn, Consumer function); + + + /** + * Provides an asynchronous implementation of {@link #apply(Object)}. The returned supplier will block until a + * validation result is received respecting {@link #getValidateTimeout()}. + * + * @param conn to validate + * @return supplier to retrieve the validation result + */ + Supplier applyAsync(Connection conn); + + + /** + * Returns the interval at which the validation task will be executed. + * + * @return validation period + */ + Duration getValidatePeriod(); + + + /** + * Returns the duration at which a validate operation should be abandoned. + * + * @return validation timeout + */ + Duration getValidateTimeout(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/Credential.java b/net-ldap/src/main/java/org/xbib/net/ldap/Credential.java new file mode 100644 index 0000000..66d811e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/Credential.java @@ -0,0 +1,89 @@ + +package org.xbib.net.ldap; + +/** + * Provides convenience methods for converting the various types of passwords into a byte array. + * + */ +public class Credential { + + /** + * Credential stored as a byte array. + */ + private final byte[] bytes; + + + /** + * Creates a new credential. + * + * @param password converted from UTF-8 to a byte array + */ + public Credential(final String password) { + if (password == null) { + throw new NullPointerException("Password cannot be null"); + } + bytes = LdapUtils.utf8Encode(password, false); + } + + + /** + * Creates a new credential. + * + * @param password converted from UTF-8 to a byte array + */ + public Credential(final char[] password) { + if (password == null) { + throw new NullPointerException("Password cannot be null"); + } + bytes = LdapUtils.utf8Encode(new String(password), false); + } + + + /** + * Creates a new credential. + * + * @param password to store + */ + public Credential(final byte[] password) { + if (password == null) { + throw new NullPointerException("Password cannot be null"); + } + bytes = password; + } + + + /** + * Returns this credential as a byte array. + * + * @return credential bytes + */ + public byte[] getBytes() { + return bytes; + } + + + /** + * Returns this credential as a string. + * + * @return credential string + */ + public String getString() { + return LdapUtils.utf8Encode(bytes, false); + } + + + /** + * Returns this credential as a character array. + * + * @return credential characters + */ + public char[] getChars() { + return getString().toCharArray(); + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "bytes=" + LdapUtils.utf8Encode(bytes) + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/DefaultConnectionFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/DefaultConnectionFactory.java new file mode 100644 index 0000000..0b1cab8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/DefaultConnectionFactory.java @@ -0,0 +1,181 @@ + +package org.xbib.net.ldap; + +import org.xbib.net.ldap.transport.Transport; +import org.xbib.net.ldap.transport.TransportFactory; + +/** + * Creates connections for performing ldap operations. + * + */ +public class DefaultConnectionFactory implements ConnectionFactory { + + /** + * Transport used by this factory. + */ + private final Transport transport; + + /** + * Connection configuration used by this factory. + */ + private ConnectionConfig config; + + + /** + * Default constructor. + */ + public DefaultConnectionFactory() { + this(TransportFactory.getTransport(DefaultConnectionFactory.class)); + } + + + /** + * Creates a new default connection factory. Be sure to invoke {@link #close()} if the supplied transport has + * resources to cleanup. + * + * @param t transport + */ + public DefaultConnectionFactory(final Transport t) { + transport = t; + } + + + /** + * Creates a new default connection factory. + * + * @param ldapUrl to connect to + */ + public DefaultConnectionFactory(final String ldapUrl) { + this(new ConnectionConfig(ldapUrl)); + } + + + /** + * Creates a new default connection factory. Be sure to invoke {@link #close()} if the supplied transport has + * resources to cleanup. + * + * @param ldapUrl to connect to + * @param t transport + */ + public DefaultConnectionFactory(final String ldapUrl, final Transport t) { + this(new ConnectionConfig(ldapUrl), t); + } + + + /** + * Creates a new default connection factory. + * + * @param cc connection configuration + */ + public DefaultConnectionFactory(final ConnectionConfig cc) { + this(cc, TransportFactory.getTransport(DefaultConnectionFactory.class)); + } + + + /** + * Creates a new default connection factory. Be sure to invoke {@link #close()} if the supplied transport has + * resources to cleanup. + * + * @param cc connection configuration + * @param t transport + */ + public DefaultConnectionFactory(final ConnectionConfig cc, final Transport t) { + transport = t; + setConnectionConfig(cc); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a builder for this class. + * + * @param t transport + * @return new builder + */ + public static Builder builder(final Transport t) { + return new Builder(t); + } + + @Override + public ConnectionConfig getConnectionConfig() { + return config; + } + + /** + * Sets the connection config. + * + * @param cc connection config + */ + public void setConnectionConfig(final ConnectionConfig cc) { + config = cc; + } + + /** + * Returns the ldap transport. + * + * @return ldap transport + */ + public Transport getTransport() { + return transport; + } + + /** + * Creates a new connection. Connections returned from this method must be opened before they can perform ldap + * operations. + * + * @return connection + */ + @Override + public Connection getConnection() { + return transport.create(config); + } + + @Override + public void close() { + transport.close(); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "transport=" + transport + ", " + + "config=" + config + "]"; + } + + // CheckStyle:OFF + public static class Builder { + + + private final DefaultConnectionFactory object; + + + protected Builder() { + object = new DefaultConnectionFactory(); + } + + + protected Builder(final Transport t) { + object = new DefaultConnectionFactory(t); + } + + + public Builder config(final ConnectionConfig cc) { + object.setConnectionConfig(cc); + return this; + } + + + public DefaultConnectionFactory build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/DeleteOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/DeleteOperation.java new file mode 100644 index 0000000..4c0c03b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/DeleteOperation.java @@ -0,0 +1,149 @@ + +package org.xbib.net.ldap; + +/** + * Executes an ldap delete operation. + * + */ +public class DeleteOperation extends AbstractOperation { + + + /** + * Default constructor. + */ + public DeleteOperation() { + } + + + /** + * Creates a new delete operation. + * + * @param factory connection factory + */ + public DeleteOperation(final ConnectionFactory factory) { + super(factory); + } + + /** + * Sends a delete request. See {@link OperationHandle#send()}. + * + * @param factory connection factory + * @param request delete request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + public static OperationHandle send( + final ConnectionFactory factory, + final DeleteRequest request) + throws LdapException { + final Connection conn = factory.getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return conn.operation(request).onComplete(conn::close).send(); + } + + /** + * Executes a delete request. See {@link OperationHandle#execute()}. + * + * @param factory connection factory + * @param request delete request + * @return delete result + * @throws LdapException if the connection cannot be opened + */ + public static DeleteResponse execute(final ConnectionFactory factory, final DeleteRequest request) + throws LdapException { + try (Connection conn = factory.getConnection()) { + conn.open(); + return conn.operation(request).execute(); + } + } + + /** + * Returns a new delete operation with the same properties as the supplied operation. + * + * @param operation to copy + * @return copy of the supplied delete operation + */ + public static DeleteOperation copy(final DeleteOperation operation) { + final DeleteOperation op = new DeleteOperation(); + op.setRequestHandlers(operation.getRequestHandlers()); + op.setResultHandlers(operation.getResultHandlers()); + op.setControlHandlers(operation.getControlHandlers()); + op.setReferralHandlers(operation.getReferralHandlers()); + op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers()); + op.setExceptionHandler(operation.getExceptionHandler()); + op.setThrowCondition(operation.getThrowCondition()); + op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers()); + op.setConnectionFactory(operation.getConnectionFactory()); + return op; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Sends a delete request. See {@link OperationHandle#send()}. + * + * @param request delete request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + @Override + public OperationHandle send(final DeleteRequest request) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send(); + } + + /** + * Executes a delete request. See {@link OperationHandle#execute()}. + * + * @param request delete request + * @return delete result + * @throws LdapException if the connection cannot be opened + */ + @Override + public DeleteResponse execute(final DeleteRequest request) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + return configureHandle(conn.operation(configureRequest(request))).execute(); + } + } + + /** + * Delete operation builder. + */ + public static class Builder extends AbstractOperation.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new DeleteOperation()); + } + + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/DeleteRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/DeleteRequest.java new file mode 100644 index 0000000..2c68e86 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/DeleteRequest.java @@ -0,0 +1,108 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP delete request defined as: + * + *
+ * DelRequest ::= [APPLICATION 10] LDAPDN
+ * 
+ * + */ +public class DeleteRequest extends AbstractRequestMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 10; + + /** + * LDAP DN to delete. + */ + private String ldapDn; + + + /** + * Default constructor. + */ + private DeleteRequest() { + } + + + /** + * Creates a new delete request. + * + * @param dn DN to delete + */ + public DeleteRequest(final String dn) { + ldapDn = dn; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the DN. + * + * @return DN + */ + public String getDn() { + return ldapDn; + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + return new DEREncoder[]{ + new IntegerType(id), + new OctetStringType(new ApplicationDERTag(PROTOCOL_OP, false), ldapDn), + }; + } + + @Override + public String toString() { + return super.toString() + ", " + "dn=" + ldapDn; + } + + /** + * Delete request builder. + */ + public static class Builder extends AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new DeleteRequest()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the ldap DN. + * + * @param dn ldap DN + * @return this builder + */ + public Builder dn(final String dn) { + object.ldapDn = dn; + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/DeleteResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/DeleteResponse.java new file mode 100644 index 0000000..c00982d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/DeleteResponse.java @@ -0,0 +1,117 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; + +/** + * LDAP delete response defined as: + * + *
+ * DelResponse ::= [APPLICATION 11] LDAPResult
+ * 
+ * + */ +public class DeleteResponse extends AbstractResult { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 11; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10253; + + /** + * DER path to result code. + */ + private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(11)/ENUM[0]"); + + /** + * DER path to matched DN. + */ + private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(11)/OCTSTR[1]"); + + /** + * DER path to diagnostic message. + */ + private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(11)/OCTSTR[2]"); + + /** + * DER path to referral. + */ + private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(11)/CTX(3)/OCTSTR[0]"); + + + /** + * Default constructor. + */ + private DeleteResponse() { + } + + + /** + * Creates a new delete response. + * + * @param buffer to decode + */ + public DeleteResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this)); + parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this)); + parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this)); + parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof DeleteResponse && super.equals(o); + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs()); + } + + // CheckStyle:OFF + public static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new DeleteResponse()); + } + + + @Override + protected Builder self() { + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/DerefAliases.java b/net-ldap/src/main/java/org/xbib/net/ldap/DerefAliases.java new file mode 100644 index 0000000..733d370 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/DerefAliases.java @@ -0,0 +1,29 @@ + +package org.xbib.net.ldap; + +/** + * Enum to define how aliases are dereferenced. + * + */ +public enum DerefAliases { + + /** + * never dereference aliases. + */ + NEVER, + + /** + * dereference when searching the entries beneath the starting point but not when searching for the starting entry. + */ + SEARCHING, + + /** + * dereference when searching for the starting entry but not when searching the entries beneath the starting point. + */ + FINDING, + + /** + * dereference when searching for the starting entry and when searching the entries beneath the starting point. + */ + ALWAYS +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/DnsResolverConnectionStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/DnsResolverConnectionStrategy.java new file mode 100644 index 0000000..40da4eb --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/DnsResolverConnectionStrategy.java @@ -0,0 +1,175 @@ + +package org.xbib.net.ldap; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Connection strategy that tries all IP addresses resolved from DNS. The order of IP addressees returned can be + * controlled via the java.net.preferIPv4Stack or java.net.preferIPv6Addresses system property flags. This strategy + * operates in an active/passive fashion. + * + */ +public class DnsResolverConnectionStrategy extends AbstractConnectionStrategy { + + /** + * Default time to live for DNS results. + */ + protected static final Duration DEFAULT_TTL = Duration.ofHours(6); + + /** + * Custom iterator function. + */ + private final Function, Iterator> iterFunction; + + /** + * Time to live for DNS records. + */ + private final Duration dnsTtl; + + /** + * Name resolver function. + */ + private Function resolverFunction = name -> { + try { + return InetAddress.getAllByName(name); + } catch (UnknownHostException e) { + throw new IllegalStateException("Could not resolve IP address for " + name, e); + } + }; + + /** + * LDAP URL string used to initialize this strategy. + */ + private String ldapUrls; + + /** + * DNS expiration time. + */ + private Instant expirationTime; + + + /** + * Default constructor. + */ + public DnsResolverConnectionStrategy() { + this(DEFAULT_TTL); + } + + + /** + * Creates a new DNS resolver connection strategy. + * + * @param ttl time to live for DNS records + */ + public DnsResolverConnectionStrategy(final Duration ttl) { + this(null, ttl); + } + + + /** + * Creates a new DNS connection strategy. + * + * @param function that produces a custom iterator + */ + public DnsResolverConnectionStrategy(final Function, Iterator> function) { + this(function, DEFAULT_TTL); + } + + + /** + * Creates a new DNS resolver connection strategy. + * + * @param function that produces a custom iterator + * @param ttl time to live for DNS records + */ + public DnsResolverConnectionStrategy(final Function, Iterator> function, final Duration ttl) { + iterFunction = function; + dnsTtl = ttl; + } + + + /** + * Returns the name resolution function. + * + * @return name resolution function + */ + public Function getResolverFunction() { + return resolverFunction; + } + + + /** + * Sets the function used to resolve names. + * + * @param func to set + */ + public void setResolverFunction(final Function func) { + resolverFunction = func; + } + + + @Override + public Iterator iterator() { + if (!isInitialized()) { + throw new IllegalStateException("Strategy is not initialized"); + } + if (Instant.now().isAfter(expirationTime)) { + populate(ldapUrls, ldapURLSet); + } + if (iterFunction != null) { + return iterFunction.apply(ldapURLSet.getUrls()); + } + return new DefaultLdapURLIterator(ldapURLSet.getUrls()); + } + + + @Override + public void populate(final String urls, final LdapURLSet urlSet) { + if (urls == null || urls.isEmpty()) { + throw new IllegalArgumentException("urls cannot be empty or null"); + } + ldapUrls = urls; + if (urls.contains(" ")) { + urlSet.populate(Stream.of(urls.split(" ")) + .flatMap(s -> { + final List l = new ArrayList<>(2); + final LdapURL parsedUrl = new LdapURL(s); + for (InetAddress address : resolverFunction.apply(parsedUrl.getHostname())) { + final LdapURL url = LdapURL.copy(parsedUrl); + url.setRetryMetadata(new LdapURLRetryMetadata(this)); + url.setInetAddress(address); + l.add(url); + } + return l.stream(); + }).collect(Collectors.toList())); + } else { + final LdapURL parsedUrl = new LdapURL(urls); + urlSet.populate(Stream.of(resolverFunction.apply(parsedUrl.getHostname())) + .map(ip -> { + final LdapURL url = LdapURL.copy(parsedUrl); + url.setRetryMetadata(new LdapURLRetryMetadata(this)); + url.setInetAddress(ip); + return url; + }).collect(Collectors.toList())); + } + expirationTime = Instant.now().plus(dnsTtl); + } + + + @Override + public DnsResolverConnectionStrategy newInstance() { + final DnsResolverConnectionStrategy strategy = new DnsResolverConnectionStrategy(iterFunction, dnsTtl); + strategy.setResolverFunction(resolverFunction); + strategy.setRetryCondition(getRetryCondition()); + return strategy; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/DnsSrvConnectionStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/DnsSrvConnectionStrategy.java new file mode 100644 index 0000000..2d5831e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/DnsSrvConnectionStrategy.java @@ -0,0 +1,253 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.xbib.net.ldap.dn.Dn; +import org.xbib.net.ldap.dns.DNSContextFactory; +import org.xbib.net.ldap.dns.DNSDomainFunction; +import org.xbib.net.ldap.dns.DefaultDNSContextFactory; +import org.xbib.net.ldap.dns.SRVDNSResolver; +import org.xbib.net.ldap.dns.SRVRecord; + +/** + * DNS SRV connection strategy. Queries a DNS server for SRV records and uses those records to construct a list of URLs. + * A time to live can be set to control how often the DNS server is consulted. See http://www.ietf.org/rfc/rfc2782.txt. + * + */ +public class DnsSrvConnectionStrategy extends AbstractConnectionStrategy { + + /** + * Default time to live for DNS results. + */ + protected static final Duration DEFAULT_TTL = Duration.ofHours(6); + + /** + * DNS context factory to override initialization parameters. + */ + private final DNSContextFactory dnsContextFactory; + + /** + * Time to live for SRV records. + */ + private final Duration srvTtl; + + /** + * Connect to LDAP using LDAPS. + */ + private final boolean useSSL; + + /** + * LDAP URL string used to initialize this strategy. + */ + private String ldapUrls; + + /** + * Resolver(s) for SRV DNS records. + */ + private Map dnsResolvers; + + /** + * SRV records expiration time. + */ + private Instant expirationTime; + + + /** + * Default constructor. + */ + public DnsSrvConnectionStrategy() { + this(DEFAULT_TTL); + } + + + /** + * Creates a new DNS SRV connection strategy. + * + * @param ttl time to live for SRV records + */ + public DnsSrvConnectionStrategy(final Duration ttl) { + this(null, ttl); + } + + + /** + * Creates a new DNS SRV connection strategy. + * + * @param factory DNS context factory + */ + public DnsSrvConnectionStrategy(final DNSContextFactory factory) { + this(factory, DEFAULT_TTL); + } + + + /** + * Creates a new DNS SRV connection strategy. + * + * @param factory DNS context factory + * @param ttl time to live for SRV records + */ + public DnsSrvConnectionStrategy(final DNSContextFactory factory, final Duration ttl) { + this(factory, ttl, false); + } + + + /** + * Creates a new DNS SRV connection strategy. + * + * @param factory DNS context factory + * @param ttl time to live for SRV records + * @param ssl whether SRV records should produce LDAPS URLs + */ + public DnsSrvConnectionStrategy(final DNSContextFactory factory, final Duration ttl, final boolean ssl) { + dnsContextFactory = factory; + srvTtl = ttl; + useSSL = ssl; + } + + + @Override + public void populate(final String urls, final LdapURLSet urlSet) { + ldapUrls = urls; + // SRV records are ordered by priority then weight. + // Thus LdapURLSet will be organized by decreasing precedence. + final List list = readSrvRecords(ldapUrls) + .stream() + .map(srv -> { + final LdapURL url = srv.getLdapURL(); + url.setRetryMetadata(new LdapURLRetryMetadata(this)); + return url; + }) + .collect(Collectors.toList()); + urlSet.populate(list); + } + + + /** + * Parses the supplied DNS URL string and reads SRV records from DNS. + * + * @param urls to parse + * @return Set of DNS SRV records ordered first by priority and then by weight. + */ + protected Set readSrvRecords(final String urls) { + if (urls == null) { + dnsResolvers = Collections.singletonMap( + new SRVDNSResolver( + Objects.requireNonNullElseGet(dnsContextFactory, DefaultDNSContextFactory::new), useSSL), null); + } else if (urls.contains(" ")) { + dnsResolvers = new HashMap<>(); + for (String url : urls.split(" ")) { + final String[] dnsUrl = parseUrl(url); + dnsResolvers.put( + new SRVDNSResolver( + Objects.requireNonNullElseGet(dnsContextFactory, () -> new DefaultDNSContextFactory(dnsUrl[0])), useSSL), + dnsUrl[1]); + } + } else { + final String[] dnsUrl = parseUrl(urls); + dnsResolvers = Collections.singletonMap( + new SRVDNSResolver( + Objects.requireNonNullElseGet( + dnsContextFactory, () -> new DefaultDNSContextFactory(dnsUrl[0])), useSSL), dnsUrl[1]); + } + final Set srvRecords = retrieveDNSRecords(); + if (srvRecords.isEmpty()) { + expirationTime = Instant.now(); + } else { + expirationTime = Instant.now().plus(srvTtl); + } + return srvRecords; + } + + + /** + * Parses the supplied URL. If the URL has an ldap scheme, it is inspected for a baseDN which will be used as the + * domain. Otherwise, the URL is assumed to have a dns scheme. + * + * @param url to parse + * @return array containing the DNS URL and the record name in that order + */ + protected String[] parseUrl(final String url) { + final LdapURL ldapURL; + try { + ldapURL = new LdapURL(url); + } catch (Exception e) { + return parseDnsUrl(url); + } + if (ldapURL.getBaseDn() == null || ldapURL.getBaseDn().isEmpty()) { + throw new IllegalArgumentException("LDAP URL " + url + " must contain a base DN"); + } + final String domain = new DNSDomainFunction().apply(new Dn(ldapURL.getBaseDn())); + if (domain.isEmpty()) { + throw new IllegalArgumentException("Base DN " + ldapURL.getBaseDn() + " could not be converted to a domain"); + } + return new String[]{null, "_ldap._tcp.".concat(domain)}; + } + + + /** + * Parses a DNS URL of the form dns://hostname/domain?record. Where record is the DNS record to retrieve. + * + * @param url to parse + * @return array containing the DNS URL and the record name in that order + */ + protected String[] parseDnsUrl(final String url) { + if (!url.contains("?")) { + return new String[]{url, null}; + } + return url.split("\\?"); + } + + + /** + * Returns a list of URLs retrieved from DNS SRV records. + * + * @return list of URLs to attempt connections to + */ + @Override + public synchronized Iterator iterator() { + if (!isInitialized()) { + throw new IllegalStateException("Strategy is not initialized"); + } + if (Instant.now().isAfter(expirationTime)) { + populate(ldapUrls, ldapURLSet); + } + return new DefaultLdapURLIterator(ldapURLSet.getUrls()); + } + + + /** + * Invoke {@link org.xbib.net.ldap.dns.DNSResolver#resolve(String)} for each resolver until results are found. + * + * @return set of LDAP URLs + */ + protected Set retrieveDNSRecords() { + for (Map.Entry entry : dnsResolvers.entrySet()) { + try { + final Set records = entry.getKey().resolve(entry.getValue()); + if (records != null && !records.isEmpty()) { + return records; + } + } catch (Exception e) { + // + } + } + return Collections.emptySet(); + } + + + @Override + public DnsSrvConnectionStrategy newInstance() { + final DnsSrvConnectionStrategy strategy = new DnsSrvConnectionStrategy(dnsContextFactory, srvTtl, useSSL); + strategy.setRetryCondition(getRetryCondition()); + return strategy; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/FilterTemplate.java b/net-ldap/src/main/java/org/xbib/net/ldap/FilterTemplate.java new file mode 100644 index 0000000..e7292d2 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/FilterTemplate.java @@ -0,0 +1,277 @@ + +package org.xbib.net.ldap; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.filter.FilterUtils; + +/** + * Class for producing an LDAP search filter from a filter template. Templates can use either index based parameters or + * name based parameters for substitutions. Parameters are encoded according to RFC 4515. + * + */ +public class FilterTemplate { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 311; + /** + * filter parameters. + */ + private final Map parameters = new HashMap<>(); + /** + * filter. + */ + private String searchFilter; + + + /** + * Default constructor. + */ + public FilterTemplate() { + } + + + /** + * Creates a new search filter with the supplied filter. + * + * @param filter to set + */ + public FilterTemplate(final String filter) { + setFilter(filter); + } + + + /** + * Creates a new search filter with the supplied filter and parameters. + * + * @param filter to set + * @param params to set + */ + public FilterTemplate(final String filter, final Object[] params) { + setFilter(filter); + setParameters(params); + } + + /** + * Hex encodes the supplied byte array for use in a search filter. + * + * @param value to encode + * @return encoded value or null if supplied value is null + */ + public static String encodeValue(final byte[] value) { + if (value == null) { + return null; + } + + final char[] c = LdapUtils.hexEncode(value); + final StringBuilder sb = new StringBuilder(c.length + c.length / 2); + for (int i = 0; i < c.length; i += 2) { + sb.append('\\').append(c[i]).append(c[i + 1]); + } + return sb.toString(); + } + + /** + * Encodes the supplied attribute value for use in a search filter. See {@link FilterUtils#escape(String)}. + * + * @param value to encode + * @return encoded value or null if supplied value is null + */ + public static String encodeValue(final String value) { + if (value == null) { + return null; + } + + return FilterUtils.escape(value); + } + + /** + * Hex encodes the supplied object if it is of type byte[], otherwise the string format of the object is escaped. See + * {@link FilterUtils#escape(String)}. + * + * @param obj to encode + * @return encoded object + */ + protected static String encode(final Object obj) { + if (obj == null) { + return null; + } + + final String str; + if (obj instanceof String) { + str = encodeValue((String) obj); + } else if (obj instanceof byte[]) { + str = encodeValue((byte[]) obj); + } else { + str = encodeValue(obj.toString()); + } + return str; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the filter. + * + * @return filter + */ + public String getFilter() { + return searchFilter; + } + + /** + * Sets the filter. + * + * @param filter to set + */ + public void setFilter(final String filter) { + searchFilter = filter; + } + + /** + * Gets the filter parameters. + * + * @return unmodifiable map of filter parameters + */ + public Map getParameters() { + return Collections.unmodifiableMap(parameters); + } + + /** + * Sets positional filter parameters. + * + * @param values to set + */ + public void setParameters(final Object[] values) { + int i = 0; + for (Object o : values) { + parameters.put(Integer.toString(i++), o); + } + } + + /** + * Sets a positional filter parameter. + * + * @param position of the parameter in the filter + * @param value to set + */ + public void setParameter(final int position, final Object value) { + parameters.put(Integer.toString(position), value); + } + + /** + * Sets a named filter parameter. + * + * @param name of the parameter in the filter + * @param value to set + */ + public void setParameter(final String name, final Object value) { + parameters.put(name, value); + } + + /** + * Returns this filter with its parameters encoded and replaced. See {@link #encode(Object)}. + * + * @return formatted and encoded filter + */ + public String format() { + String s = searchFilter; + if (!parameters.isEmpty()) { + for (Map.Entry e : parameters.entrySet()) { + final String encoded = encode(e.getValue()); + if (encoded != null) { + s = s.replace("{" + e.getKey() + "}", encoded); + } + } + } + return s; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof FilterTemplate v) { + return LdapUtils.areEqual(searchFilter, v.searchFilter) && + LdapUtils.areEqual(parameters, v.parameters); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, searchFilter, parameters); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "filter=" + searchFilter + ", " + + "parameters=" + parameters + "]"; + } + + // CheckStyle:OFF + public static class Builder { + + + private final FilterTemplate object = new FilterTemplate(); + + + protected Builder() { + } + + + public Builder filter(final String filter) { + object.setFilter(filter); + return this; + } + + + public Builder parameter(final String name, final String value) { + object.setParameter(name, value); + return this; + } + + + public Builder parameter(final String name, final Object value) { + object.setParameter(name, value); + return this; + } + + + public Builder parameter(final int pos, final String value) { + object.setParameter(pos, value); + return this; + } + + + public Builder parameter(final int pos, final Object value) { + object.setParameter(pos, value); + return this; + } + + + public Builder parameters(final Object... values) { + object.setParameters(values); + return this; + } + + + public FilterTemplate build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/InitialRetryMetadata.java b/net-ldap/src/main/java/org/xbib/net/ldap/InitialRetryMetadata.java new file mode 100644 index 0000000..f751228 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/InitialRetryMetadata.java @@ -0,0 +1,21 @@ + +package org.xbib.net.ldap; + +import java.time.Instant; + +/** + * Retry metadata used when a connection is opened. + * + */ +public class InitialRetryMetadata extends AbstractRetryMetadata { + + + /** + * Creates a new initial retry metadata. + * + * @param time of last successful connection + */ + public InitialRetryMetadata(final Instant time) { + successTime = time; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/LdapAttribute.java b/net-ldap/src/main/java/org/xbib/net/ldap/LdapAttribute.java new file mode 100644 index 0000000..0c870d8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/LdapAttribute.java @@ -0,0 +1,654 @@ + +package org.xbib.net.ldap; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * LDAP attribute defined as: + * + *
+ * Attribute ::= PartialAttribute(WITH COMPONENTS {
+ * ...,
+ * vals (SIZE(1..MAX))})
+ * 
+ * + */ +public class LdapAttribute { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10223; + + /** + * List of attribute names known to use binary syntax. + */ + private static final String[] DEFAULT_BINARY_ATTRIBUTES = new String[]{ + "photo", + "personalSignature", + "audio", + "jpegPhoto", + "javaSerializedData", + "thumbnailPhoto", + "thumbnailLogo", + "userCertificate", + "cACertificate", + "authorityRevocationList", + "certificateRevocationList", + "crossCertificatePair", + "x500UniqueIdentifier", + }; + + /** + * List of custom binary attribute names. + */ + private static final String[] BINARY_ATTRIBUTES; + + static { + // Configure custom binary attribute names + final String[] split = System.getProperty("org.xbib.net.ldap.attribute.binary", "").split(","); + BINARY_ATTRIBUTES = LdapUtils.concatArrays(DEFAULT_BINARY_ATTRIBUTES, split); + } + + /** + * Attribute name. + */ + private String attributeName; + /** + * Attribute values. + */ + private Set attributeValues = new LinkedHashSet<>(); + /** + * Whether this attribute is binary and string representations should be base64 encoded. + */ + private boolean binary; + + + /** + * Default constructor. + */ + public LdapAttribute() { + } + + + /** + * Creates a new attribute. + * + * @param type attribute description + */ + public LdapAttribute(final String type) { + setName(type); + } + + + /** + * Creates a new attribute. + * + * @param type attribute description + * @param value attribute values + */ + public LdapAttribute(final String type, final byte[]... value) { + setName(type); + addBinaryValues(value); + } + + + /** + * Creates a new attribute. + * + * @param type attribute description + * @param value attribute values + */ + public LdapAttribute(final String type, final String... value) { + setName(type); + addStringValues(value); + } + + /** + * Returns a new attribute whose values are sorted. String values are sorted naturally. Binary values are sorted using + * {@link ByteBuffer#compareTo(ByteBuffer)}. + * + * @param la attribute to sort + * @return sorted attribute + */ + public static LdapAttribute sort(final LdapAttribute la) { + final LdapAttribute sorted = new LdapAttribute(la.getName()); + if (la.isBinary()) { + sorted.setBinary(true); + final Set newValues = la.getBinaryValues().stream().sorted( + (o1, o2) -> { + final ByteBuffer bb1 = ByteBuffer.wrap(o1); + final ByteBuffer bb2 = ByteBuffer.wrap(o2); + return bb1.compareTo(bb2); + }).collect(Collectors.toCollection(LinkedHashSet::new)); + sorted.addBinaryValues(newValues); + } else { + final Set newValues = la.getStringValues().stream() + .sorted(Comparator.comparing(String::toString)).collect(Collectors.toCollection(LinkedHashSet::new)); + sorted.addStringValues(newValues); + } + return sorted; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns whether this ldap attribute is binary. + * + * @return whether this ldap attribute is binary + */ + public boolean isBinary() { + return binary; + } + + /** + * Sets whether this ldap attribute is binary. + * + * @param b whether this ldap attribute is binary + */ + public void setBinary(final boolean b) { + binary = b; + } + + /** + * Checks whether attrNames matches the name of this attribute. If a match is found this attribute is set as binary. + * + * @param attrNames custom binary attribute names + */ + public void configureBinary(final String... attrNames) { + if (binary) { + return; + } + if (attrNames != null && attrNames.length > 0) { + for (String s : attrNames) { + if (attributeName.equals(s)) { + binary = true; + break; + } + } + } + } + + /** + * Returns the attribute description with options. + * + * @return attribute description + */ + public String getName() { + return attributeName; + } + + /** + * Sets the name. This method has the side effect of setting this attribute as binary if the name has an option of + * 'binary' or the name matches one of {@link #BINARY_ATTRIBUTES}. + * + * @param type attribute name + */ + public void setName(final String type) { + attributeName = type; + if (getOptions().contains("binary") || Stream.of(BINARY_ATTRIBUTES).anyMatch(attributeName::equals)) { + setBinary(true); + } + } + + /** + * Returns the attribute description with or without options. + * + * @param withOptions whether the attribute description should include options + * @return attribute description + */ + public String getName(final boolean withOptions) { + if (withOptions) { + return attributeName; + } else { + final int optionIndex = attributeName.indexOf(";"); + return optionIndex > 0 ? attributeName.substring(0, optionIndex) : attributeName; + } + } + + /** + * Returns any options that may exist on the attribute description. + * + * @return attribute description options + */ + public List getOptions() { + if (attributeName.indexOf(";") > 0) { + final String[] split = attributeName.split(";"); + if (split.length > 1) { + return IntStream.range(1, split.length).mapToObj(i -> split[i]).collect(Collectors.toUnmodifiableList()); + } + } + return Collections.emptyList(); + } + + /** + * Returns a single byte array value of this attribute. + * + * @return single byte array attribute value or null if this attribute is empty + */ + public byte[] getBinaryValue() { + return attributeValues.isEmpty() ? null : attributeValues.iterator().next().array(); + } + + /** + * Returns the values of this attribute as byte arrays. The return collection cannot be modified. + * + * @return collection of string attribute values + */ + public Collection getBinaryValues() { + if (attributeValues.isEmpty()) { + return Collections.emptySet(); + } + return attributeValues.stream().map(ByteBuffer::array).collect(Collectors.toUnmodifiableList()); + } + + /** + * Returns a single string value of this attribute. + * + * @return single string attribute value or null if this attribute is empty + */ + public String getStringValue() { + if (attributeValues.isEmpty()) { + return null; + } + final ByteBuffer val = attributeValues.iterator().next(); + return binary ? LdapUtils.base64Encode(val.array()) : LdapUtils.utf8Encode(val.array()); + } + + /** + * Returns the values of this attribute as strings. Binary data is base64 encoded. The return collection cannot be + * modified. + * + * @return collection of string attribute values + */ + public Collection getStringValues() { + if (attributeValues.isEmpty()) { + return Collections.emptySet(); + } + return attributeValues.stream().map(v -> { + if (binary) { + return LdapUtils.base64Encode(v.array()); + } + return LdapUtils.utf8Encode(v.array(), false); + }).collect(Collectors.toUnmodifiableList()); + } + + /** + * Returns a single decoded value of this attribute. + * + * @param type of decoded attribute + * @param func to decode attribute value with + * @return single decoded attribute value or null if this attribute is empty + */ + public T getValue(final Function func) { + return attributeValues.isEmpty() ? null : func.apply(attributeValues.iterator().next().array()); + } + + /** + * Returns the values of this attribute decoded by the supplied function. + * + * @param type of decoded attributes + * @param func to decode attribute values with + * @return collection of decoded attribute values, null values are discarded + */ + public Collection getValues(final Function func) { + return attributeValues.stream() + .filter(Objects::nonNull) + .map(ByteBuffer::array) + .map(func).collect(Collectors.toUnmodifiableList()); + } + + /** + * Adds the supplied byte array as a value for this attribute. + * + * @param value to add, null values are discarded + */ + public void addBinaryValues(final byte[]... value) { + Stream.of(value).filter(Objects::nonNull).map(ByteBuffer::wrap).forEach(attributeValues::add); + } + + /** + * Adds all the byte arrays in the supplied collection as values for this attribute. + * + * @param values to add, null values are discarded + */ + public void addBinaryValues(final Collection values) { + values.stream().filter(Objects::nonNull).map(ByteBuffer::wrap).forEach(attributeValues::add); + } + + /** + * Adds the supplied string as a value for this attribute. + * + * @param value to add, null values are discarded + */ + public void addStringValues(final String... value) { + Stream.of(value) + .filter(Objects::nonNull) + .map(s -> toByteArray(s, true)) + .filter(Objects::nonNull) + .map(ByteBuffer::wrap) + .forEach(attributeValues::add); + } + + /** + * Adds all the strings in the supplied collection as values for this attribute. + * + * @param values to add, null values are discarded + */ + public void addStringValues(final Collection values) { + values.stream() + .filter(Objects::nonNull) + .map(s -> toByteArray(s, true)) + .filter(Objects::nonNull) + .map(ByteBuffer::wrap) + .forEach(attributeValues::add); + } + + /** + * Adds all the buffers in the supplied collection as values for this attribute. + * + * @param values to add, null values are discarded + */ + public void addBufferValues(final ByteBuffer... values) { + Stream.of(values).filter(Objects::nonNull).forEach(attributeValues::add); + } + + /** + * Adds all the buffers in the supplied collection as values for this attribute. + * + * @param values to add, null values are discarded + */ + public void addBufferValues(final Collection values) { + values.stream().filter(Objects::nonNull).forEach(attributeValues::add); + } + + /** + * Adds the supplied values for this attribute by encoding them with the supplied function. + * + * @param type attribute to encode + * @param func to encode value with + * @param value to encode and add, null values are discarded + */ + @SuppressWarnings("unchecked") + public void addValues(final Function func, final T... value) { + Stream.of(value) + .filter(Objects::nonNull) + .map(func) + .filter(Objects::nonNull) + .map(ByteBuffer::wrap) + .forEach(attributeValues::add); + } + + /** + * Adds all the values in the supplied collection for this attribute by encoding them with the supplied function. + * See {@link #addValues(Function, Object...)}. + * + * @param type attribute to encode + * @param func to encode value with + * @param values to encode and add, null values are discarded + */ + public void addValues(final Function func, final Collection values) { + values.stream() + .filter(Objects::nonNull) + .map(func) + .filter(Objects::nonNull) + .map(ByteBuffer::wrap) + .forEach(attributeValues::add); + } + + /** + * Removes the supplied byte array as a value from this attribute. + * + * @param value to remove, null values are discarded + */ + public void removeBinaryValues(final byte[]... value) { + Stream.of(value).filter(Objects::nonNull).map(ByteBuffer::wrap).forEach(attributeValues::remove); + } + + /** + * Removes all the byte arrays in the supplied collection as values from this attribute. + * + * @param values to remove, null values are discarded + */ + public void removeBinaryValues(final Collection values) { + values.stream().filter(Objects::nonNull).map(ByteBuffer::wrap).forEach(attributeValues::remove); + } + + /** + * Removes the supplied string as a value from this attribute. + * + * @param value to remove, null values are discarded + */ + public void removeStringValues(final String... value) { + Stream.of(value) + .filter(Objects::nonNull) + .map(s -> toByteArray(s, true)) + .filter(Objects::nonNull) + .map(ByteBuffer::wrap) + .forEach(attributeValues::remove); + } + + /** + * Removes all the strings in the supplied collection as values from this attribute. + * + * @param values to remove, null values are discarded + */ + public void removeStringValues(final Collection values) { + values.stream() + .filter(Objects::nonNull) + .map(s -> toByteArray(s, true)) + .filter(Objects::nonNull) + .map(ByteBuffer::wrap) + .forEach(attributeValues::remove); + } + + /** + * Removes all the buffers in the supplied collection as values from this attribute. + * + * @param values to remove, null values are discarded + */ + public void removeBufferValues(final ByteBuffer... values) { + Stream.of(values).filter(Objects::nonNull).forEach(attributeValues::remove); + } + + /** + * Removes all the buffers in the supplied collection as values from this attribute. + * + * @param values to remove, null values are discarded + */ + public void removeBufferValues(final Collection values) { + values.stream().filter(Objects::nonNull).forEach(attributeValues::remove); + } + + /** + * Returns whether the supplied value exists in this attribute. + * + * @param value to find + * @return whether value exists + */ + public boolean hasValue(final byte[] value) { + return attributeValues.stream().anyMatch(bb -> Arrays.equals(bb.array(), value)); + } + + /** + * Returns whether the supplied value exists in this attribute. + * + * @param value to find + * @return whether value exists + */ + public boolean hasValue(final String value) { + return attributeValues.stream().anyMatch(bb -> Arrays.equals(bb.array(), toByteArray(value, false))); + } + + /** + * Returns whether the supplied value exists in this attribute. + * + * @param type attribute to encode + * @param func to encode value with + * @param value to find + * @return whether value exists + */ + public boolean hasValue(final Function func, final T value) { + return attributeValues.stream().anyMatch(bb -> Arrays.equals(bb.array(), func.apply(value))); + } + + /** + * Returns the number of values in this ldap attribute. + * + * @return number of values in this ldap attribute + */ + public int size() { + return attributeValues.size(); + } + + /** + * Removes all the values in this ldap attribute. + */ + public void clear() { + attributeValues.clear(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof LdapAttribute) { + final LdapAttribute v = (LdapAttribute) o; + return LdapUtils.areEqual(LdapUtils.toLowerCase(attributeName), LdapUtils.toLowerCase(v.attributeName)) && + LdapUtils.areEqual(attributeValues, v.attributeValues); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + LdapUtils.toLowerCase(attributeName), + attributeValues); + } + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "name=" + attributeName + ", " + + "values=" + getStringValues() + ", " + + "binary=" + binary; + } + + /** + * Converts the supplied string value to a byte array respecting the {@link #binary} flag. If this attribute is + * binary, value is expected to be in base64 format. Otherwise, value is UTF-8 encoded. + * + * @param value to convert + * @param throwOnError whether to throw if a base64 decode error occurs + * @return binary value + * @throws IllegalArgumentException if attribute is binary, value cannot be base64 decoded and throwOnError is true + */ + private byte[] toByteArray(final String value, final boolean throwOnError) { + if (binary) { + try { + return LdapUtils.base64Decode(value); + } catch (IllegalArgumentException e) { + if (throwOnError) { + throw new IllegalArgumentException("Error decoding " + value + " for " + attributeName, e); + } + return null; + } + } + return LdapUtils.utf8Encode(value, false); + } + + // CheckStyle:OFF + public static class Builder { + + + private final LdapAttribute object = new LdapAttribute(); + + + protected Builder() { + } + + + public Builder name(final String name) { + object.setName(name); + return this; + } + + + @SuppressWarnings("unchecked") + public Builder values(final Function func, final T... value) { + object.addValues(func, value); + return this; + } + + + public Builder values(final byte[]... values) { + object.addBinaryValues(values); + return this; + } + + + public Builder binaryValues(final Collection values) { + object.addBinaryValues(values); + return this; + } + + + public Builder values(final String... values) { + object.addStringValues(values); + return this; + } + + + public Builder stringValues(final Collection values) { + object.addStringValues(values); + return this; + } + + + public Builder values(final ByteBuffer... values) { + object.addBufferValues(values); + return this; + } + + + public Builder bufferValues(final Collection values) { + object.addBufferValues(values); + return this; + } + + + public Builder binary(final boolean b) { + object.setBinary(b); + return this; + } + + + public LdapAttribute build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/LdapEntry.java b/net-ldap/src/main/java/org/xbib/net/ldap/LdapEntry.java new file mode 100644 index 0000000..89c457d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/LdapEntry.java @@ -0,0 +1,542 @@ + +package org.xbib.net.ldap; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.OctetStringType; +import org.xbib.net.ldap.dn.Dn; + +/** + * LDAP search result entry defined as: + * + *
+ * SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
+ * objectName      LDAPDN,
+ * attributes      PartialAttributeList }
+ *
+ * PartialAttributeList ::= SEQUENCE OF
+ * partialAttribute PartialAttribute
+ *
+ * PartialAttribute ::= SEQUENCE {
+ * type       AttributeDescription,
+ * vals       SET OF value AttributeValue }
+ * 
+ * + */ +public class LdapEntry extends AbstractMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 4; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10303; + + /** + * DER path to LDAP DN. + */ + private static final DERPath LDAP_DN_PATH = new DERPath("/SEQ/APP(4)/OCTSTR[0]"); + + /** + * DER path to attributes. + */ + private static final DERPath ATTRIBUTES_PATH = new DERPath("/SEQ/APP(4)/SEQ/SEQ"); + + /** + * LDAP DN of the entry. + */ + private String ldapDn; + + /** + * Parsed LDAP DN. + */ + private Dn parsedDn; + + /** + * Normalized LDAP DN. + */ + private String normalizedDn; + + /** + * LDAP attributes on the entry. + */ + private Map attributes = new LinkedHashMap<>(); + + + /** + * Default constructor. + */ + public LdapEntry() { + } + + + /** + * Creates a new search result entry. + * + * @param buffer to decode + */ + public LdapEntry(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(LDAP_DN_PATH, new LdapDnHandler(this)); + parser.registerHandler(ATTRIBUTES_PATH, new AttributesHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Returns a new entry whose attributes are sorted naturally by name without options. + * + * @param le entry to sort + * @return sorted entry + */ + public static LdapEntry sort(final LdapEntry le) { + final LdapEntry sorted = new LdapEntry(); + sorted.copyValues(le); + sorted.setDn(le.getDn()); + sorted.addAttributes( + le.getAttributes().stream() + .map(LdapAttribute::sort) + .sorted(Comparator.comparing(o -> o.getName(false))).collect(Collectors.toCollection(LinkedHashSet::new))); + return sorted; + } + + /** + * Returns the list of attribute modifications needed to change the supplied target entry into the supplied source + * entry. See {@link #computeModifications(LdapEntry, LdapEntry, boolean)}. + * + * @param source ldap entry containing new data + * @param target ldap entry containing existing data + * @return attribute modifications needed to change target into source or an empty array + */ + public static AttributeModification[] computeModifications(final LdapEntry source, final LdapEntry target) { + return computeModifications(source, target, true); + } + + /** + * Returns the list of attribute modifications needed to change the supplied target entry into the supplied source + * entry. This implementation performs a byte comparison on the attribute values to determine changes. + * + * @param source ldap entry containing new data + * @param target ldap entry containing existing data + * @param useReplace whether to use a single REPLACE modification or individual ADD/DELETE for attribute values + * @return attribute modifications needed to change target into source or an empty array + */ + public static AttributeModification[] computeModifications( + final LdapEntry source, final LdapEntry target, final boolean useReplace) { + final List mods = new ArrayList<>(); + for (LdapAttribute sourceAttr : source.getAttributes()) { + final LdapAttribute targetAttr = target.getAttribute(sourceAttr.getName()); + if (targetAttr == null) { + if (sourceAttr.size() > 0) { + mods.add(new AttributeModification(AttributeModification.Type.ADD, sourceAttr)); + } else { + // perform a replace if attribute has no values to avoid potential schema issues + mods.add(new AttributeModification(AttributeModification.Type.REPLACE, sourceAttr)); + } + } else if (!targetAttr.equals(sourceAttr)) { + if (useReplace) { + mods.add(new AttributeModification(AttributeModification.Type.REPLACE, sourceAttr)); + } else { + final LdapAttribute toAdd = new LdapAttribute(sourceAttr.getName()); + sourceAttr.getBinaryValues().stream() + .filter(sv -> !targetAttr.hasValue(sv)) + .forEach(toAdd::addBinaryValues); + if (toAdd.size() > 0) { + mods.add(new AttributeModification(AttributeModification.Type.ADD, toAdd)); + } + + final LdapAttribute toDelete = new LdapAttribute(sourceAttr.getName()); + targetAttr.getBinaryValues().stream() + .filter(tv -> !sourceAttr.hasValue(tv)) + .forEach(toDelete::addBinaryValues); + if (toDelete.size() > 0) { + mods.add(new AttributeModification(AttributeModification.Type.DELETE, toDelete)); + } + } + } + } + for (LdapAttribute targetAttr : target.getAttributes()) { + final LdapAttribute sourceAttr = source.getAttribute(targetAttr.getName()); + if (sourceAttr == null) { + mods.add( + new AttributeModification( + AttributeModification.Type.DELETE, + LdapAttribute.builder().name(targetAttr.getName()).build())); + } + } + return mods.toArray(AttributeModification[]::new); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the ldap DN. + * + * @return ldap DN + */ + public String getDn() { + return ldapDn; + } + + /** + * Sets the ldap DN. + * + * @param dn ldap DN + */ + public void setDn(final String dn) { + ldapDn = dn; + if (ldapDn != null) { + try { + parsedDn = new Dn(ldapDn); + } catch (Exception e) { + parsedDn = null; + } + if (parsedDn != null) { + normalizedDn = parsedDn.format(); + } + } + } + + /** + * Returns the parsed ldap DN. Parsing is performed using {@link org.xbib.net.ldap.dn.DefaultDnParser}. + * + * @return parsed ldap DN or null if {@link #ldapDn} is null or could not be parsed + */ + public Dn getParsedDn() { + return parsedDn; + } + + /** + * Returns the normalized ldap DN. Normalization is performed using {@link org.xbib.net.ldap.dn.DefaultRDnNormalizer}. + * + * @return normalized ldap DN or null if {@link #ldapDn} is null or could not be parsed + */ + public String getNormalizedDn() { + return normalizedDn; + } + + /** + * Returns the ldap attributes. + * + * @return ldap attributes + */ + public Collection getAttributes() { + return attributes.values(); + } + + /** + * Returns a single attribute of this entry. If multiple attributes exist the first attribute returned by the + * underlying iterator is used. If no attributes exist null is returned. + * + * @return single attribute + */ + public LdapAttribute getAttribute() { + if (attributes.isEmpty()) { + return null; + } + return attributes.values().iterator().next(); + } + + /** + * Returns the attribute with the supplied name. + * + * @param name of the attribute to return + * @return ldap attribute + */ + public LdapAttribute getAttribute(final String name) { + if (name != null) { + return attributes.get(LdapUtils.toLowerCase(name)); + } + return null; + } + + /** + * Returns the attribute names in this entry. + * + * @return string array of attribute names + */ + public String[] getAttributeNames() { + return attributes.values().stream().map(LdapAttribute::getName).toArray(String[]::new); + } + + /** + * Adds attributes to the entry. + * + * @param attrs attributes to add + */ + public void addAttributes(final LdapAttribute... attrs) { + for (LdapAttribute a : attrs) { + attributes.put(LdapUtils.toLowerCase(a.getName()), a); + } + } + + /** + * Adds attributes to the entry. + * + * @param attrs attributes to add + */ + public void addAttributes(final Collection attrs) { + attrs.forEach(a -> attributes.put(LdapUtils.toLowerCase(a.getName()), a)); + } + + /** + * Removes the attribute with the supplied name. + * + * @param name of attribute to remove + */ + public void removeAttribute(final String name) { + attributes.remove(LdapUtils.toLowerCase(name)); + } + + /** + * Removes an attribute from this ldap attributes. + * + * @param attrs attribute to remove + */ + public void removeAttributes(final LdapAttribute... attrs) { + for (LdapAttribute a : attrs) { + attributes.remove(LdapUtils.toLowerCase(a.getName())); + } + } + + /** + * Removes the attribute(s) from this ldap attributes. + * + * @param attrs collection of ldap attributes to remove + */ + public void removeAttributes(final Collection attrs) { + attrs.forEach(a -> attributes.remove(LdapUtils.toLowerCase(a.getName()))); + } + + /** + * Returns the number of attributes. + * + * @return number of attributes + */ + public int size() { + return attributes.size(); + } + + /** + * Removes all the attributes. + */ + public void clear() { + attributes.clear(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof LdapEntry && super.equals(o)) { + final LdapEntry v = (LdapEntry) o; + // compare normalizedDn if not null, else compare Dn + return LdapUtils.areEqual( + normalizedDn != null ? normalizedDn : ldapDn, + normalizedDn != null ? v.normalizedDn : v.normalizedDn != null ? v.normalizedDn : v.ldapDn) && + LdapUtils.areEqual(attributes, v.attributes); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + normalizedDn != null ? normalizedDn : ldapDn, + attributes); + } + + @Override + public String toString() { + return super.toString() + ", " + + "dn=" + ldapDn + ", " + + "attributes=" + (attributes != null ? attributes.values() : null); + } + + /** + * Parse handler implementation for the LDAP DN. + */ + protected static class LdapDnHandler extends AbstractParseHandler { + + + /** + * Creates a new ldap dn handler. + * + * @param response to configure + */ + LdapDnHandler(final LdapEntry response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setDn(OctetStringType.decode(encoded)); + } + } + + /** + * Parse handler implementation for the attributes. + */ + protected static class AttributesHandler extends AbstractParseHandler { + + + /** + * Creates a new attributes handler. + * + * @param response to configure + */ + AttributesHandler(final LdapEntry response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final AttributeParser p = new AttributeParser(); + p.parse(encoded); + + if (p.getName().isEmpty()) { + throw new IllegalArgumentException("Could not parse attribute"); + } + if (p.getValues().isEmpty()) { + getObject().addAttributes(LdapAttribute.builder().name(p.getName().get()).build()); + } else { + getObject().addAttributes( + LdapAttribute.builder().name(p.getName().get()).bufferValues(p.getValues().get()).build()); + } + } + } + + /** + * Parses a buffer containing an attribute name and its values. + */ + protected static class AttributeParser { + + /** + * DER path to name. + */ + private static final DERPath NAME_PATH = new DERPath("/OCTSTR"); + + /** + * DER path to values. + */ + private static final DERPath VALUES_PATH = new DERPath("/SET/OCTSTR"); + + /** + * Parser for decoding LDAP attributes. + */ + private final DERParser parser = new DERParser(); + + /** + * Attribute name. + */ + private String name; + + /** + * Attribute values. + */ + private final List values = new ArrayList<>(); + + + /** + * Creates a new attribute parser. + */ + public AttributeParser() { + parser.registerHandler(NAME_PATH, (p, e) -> name = OctetStringType.decode(e)); + parser.registerHandler(VALUES_PATH, (p, e) -> values.add(ByteBuffer.wrap(e.getRemainingBytes()))); + } + + + /** + * Examines the supplied buffer and parses an LDAP attribute if one is found. + * + * @param buffer to parse + */ + public void parse(final DERBuffer buffer) { + parser.parse(buffer); + } + + + /** + * Returns the attribute name. + * + * @return attribute name or empty + */ + public Optional getName() { + return Optional.ofNullable(name); + } + + + /** + * Returns the attribute values. + * + * @return attribute values or empty + */ + public Optional> getValues() { + return values.isEmpty() ? Optional.empty() : Optional.of(values); + } + } + + // CheckStyle:OFF + public static class Builder extends AbstractMessage.AbstractBuilder { + + + protected Builder() { + super(new LdapEntry()); + } + + + @Override + protected Builder self() { + return this; + } + + + public Builder dn(final String dn) { + object.setDn(dn); + return this; + } + + + public Builder attributes(final LdapAttribute... attrs) { + object.addAttributes(attrs); + return this; + } + + + public Builder attributes(final Collection attrs) { + object.addAttributes(attrs); + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/LdapException.java b/net-ldap/src/main/java/org/xbib/net/ldap/LdapException.java new file mode 100644 index 0000000..429369a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/LdapException.java @@ -0,0 +1,116 @@ + +package org.xbib.net.ldap; + +/** + * Base exception for all ldap related exceptions. + * + */ +public class LdapException extends Exception { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = 6812614366508784841L; + + /** + * Optional result code associated with this exception. + */ + private final ResultCode resultCode; + + + /** + * Creates a new ldap exception based on the supplied result. + * + * @param result that produced this exception + */ + public LdapException(final Result result) { + this(result.getResultCode(), formatResult(result)); + } + + + /** + * Creates a new ldap exception. + * + * @param msg describing this exception + */ + public LdapException(final String msg) { + this(null, msg); + } + + + /** + * Creates a new ldap exception. + * + * @param code result code describing this exception + * @param msg describing this exception + */ + public LdapException(final ResultCode code, final String msg) { + super(msg); + resultCode = code; + } + + + /** + * Creates a new ldap exception. + * + * @param e underlying exception + */ + public LdapException(final Throwable e) { + this((ResultCode) null, e); + } + + + /** + * Creates a new ldap exception. + * + * @param code result code describing this exception + * @param e underlying exception + */ + public LdapException(final ResultCode code, final Throwable e) { + super(e); + resultCode = code; + } + + + /** + * Creates a new ldap exception. + * + * @param msg describing this exception + * @param e underlying exception + */ + public LdapException(final String msg, final Throwable e) { + this(null, msg, e); + } + + + /** + * Creates a new ldap exception. + * + * @param code result code describing this exception + * @param msg describing this exception + * @param e underlying exception + */ + public LdapException(final ResultCode code, final String msg, final Throwable e) { + super(msg, e); + resultCode = code; + } + + /** + * Formats the supplied result for use as an exception message. + * + * @param result to format + * @return formatted result + */ + protected static String formatResult(final Result result) { + return "resultCode=" + result.getResultCode() + ", " + "diagnosticMessage=" + result.getEncodedDiagnosticMessage(); + } + + /** + * Returns the result code. + * + * @return result code or null + */ + public ResultCode getResultCode() { + return resultCode; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/LdapURL.java b/net-ldap/src/main/java/org/xbib/net/ldap/LdapURL.java new file mode 100644 index 0000000..26ffb09 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/LdapURL.java @@ -0,0 +1,516 @@ + +package org.xbib.net.ldap; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class for parsing LDAP URLs. See RFC 4516. Expects URLs of the form scheme://hostname:port/baseDn?attrs?scope?filter. + * This implementation does not support URL extensions. + * + */ +public class LdapURL { + + /** + * Pattern to match LDAP URL. + */ + protected static final Pattern URL_PATTERN = Pattern.compile( + "([lL][dD][aA][pP][sSiI]?)://(\\[[0-9A-Fa-f:]+\\]|[^:/]+)?" + + "(?::(\\d+))?" + + "(?:/(?:([^?]+))?" + + "(?:\\?([^?]*))?" + + "(?:\\?([^?]*))?" + + "(?:\\?(.*))?)?"); + + /** + * Default LDAP port, value is {@value}. + */ + protected static final int DEFAULT_LDAP_PORT = 389; + + /** + * Default LDAPS port, value is {@value}. + */ + protected static final int DEFAULT_LDAPS_PORT = 636; + + /** + * Default base DN, value is {@value}. + */ + protected static final String DEFAULT_BASE_DN = ""; + + /** + * Default search filter value is '(objectClass=*)'. + */ + protected static final String DEFAULT_FILTER = "(objectClass=*)"; + + /** + * Default scope, value is {@link SearchScope#OBJECT}. + */ + protected static final SearchScope DEFAULT_SCOPE = SearchScope.OBJECT; + + /** + * Default return attributes, value is all user attributes. + */ + protected static final String[] DEFAULT_ATTRIBUTES = ReturnAttributes.ALL_USER.value(); + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10333; + + /** + * Scheme of the ldap url. + */ + private String scheme; + + /** + * Hostname of the ldap url. + */ + private String hostname; + + /** + * Port of the ldap url. + */ + private int port; + + /** + * Base DN of the ldap url. + */ + private String baseDn; + + /** + * Attributes of the ldap url. + */ + private String[] attributes; + + /** + * Search scope of the ldap url. + */ + private SearchScope scope; + + /** + * Search filter of the ldap url. + */ + private String filter; + + /** + * Metadata that describes connection failures on this URL. + */ + private LdapURLRetryMetadata retryMetadata; + + /** + * False if the last connection attempt to this URL failed, which should result in updating {@link #retryMetadata}, + * otherwise true. + */ + private boolean active = true; + + /** + * IP address resolved for this URL. + */ + private InetAddress inetAddress; + + + /** + * Private constructor. + */ + private LdapURL() { + } + + + /** + * Creates a new ldap url. + * + * @param hostname LDAP server hostname + * @param port TCP port the LDAP server is listening on + */ + public LdapURL(final String hostname, final int port) { + this("ldap://" + hostname + ":" + port); + } + + + /** + * Creates a new ldap url. + * + * @param url LDAP url + */ + public LdapURL(final String url) { + parseURL(url); + } + + + /** + * Creates a new ldap url. + * + * @param scheme url scheme + * @param hostname url hostname + * @param port url port + * @param baseDn base DN + * @param attributes attributes + * @param scope search scope + * @param filter search filter + */ + protected LdapURL( + final String scheme, + final String hostname, + final int port, + final String baseDn, + final String[] attributes, + final SearchScope scope, + final String filter) { + if (scheme == null) { + throw new IllegalArgumentException("Scheme cannot be null"); + } + this.scheme = scheme; + this.hostname = hostname; + this.port = port; + this.baseDn = baseDn; + this.attributes = attributes; + this.scope = scope; + this.filter = filter; + } + + /** + * Returns a new ldap URL initialized with the supplied URL. + * + * @param ldapURL ldap URL to read properties from + * @return ldap URL + */ + public static LdapURL copy(final LdapURL ldapURL) { + final LdapURL url = new LdapURL(); + url.scheme = ldapURL.scheme; + url.hostname = ldapURL.hostname; + url.port = ldapURL.port; + url.baseDn = ldapURL.baseDn; + url.attributes = ldapURL.attributes; + url.scope = ldapURL.scope; + url.filter = ldapURL.filter; + url.retryMetadata = ldapURL.retryMetadata; + url.active = ldapURL.active; + url.inetAddress = ldapURL.inetAddress; + return url; + } + + /** + * Returns the scheme. + * + * @return scheme + */ + public String getScheme() { + return scheme; + } + + /** + * Returns the hostname. + * + * @return hostname + */ + public String getHostname() { + return hostname; + } + + /** + * Returns the port. If no port was supplied, returns the default port for the scheme. + * + * @return port + */ + public int getPort() { + if (port == -1) { + return "ldaps".equals(scheme) ? DEFAULT_LDAPS_PORT : DEFAULT_LDAP_PORT; + } + return port; + } + + /** + * Returns false if a port was supplied in this url. + * + * @return false if a port was supplied in this url + */ + public boolean isDefaultPort() { + return port == -1; + } + + /** + * Returns the base DN. + * + * @return baseDn + */ + public String getBaseDn() { + return baseDn == null ? DEFAULT_BASE_DN : baseDn; + } + + /** + * Returns whether a base DN was supplied in this url. + * + * @return whether a base DN was supplied in this url + */ + public boolean isDefaultBaseDn() { + return baseDn == null; + } + + /** + * Returns the attributes. + * + * @return attributes + */ + public String[] getAttributes() { + return attributes == null ? DEFAULT_ATTRIBUTES : attributes; + } + + /** + * Returns whether attributes were supplied in this url. + * + * @return whether an attributes were supplied in this url + */ + public boolean isDefaultAttributes() { + return attributes == null; + } + + /** + * Returns the scope. + * + * @return scope + */ + public SearchScope getScope() { + return scope == null ? DEFAULT_SCOPE : scope; + } + + /** + * Returns whether a scope was supplied in this url. + * + * @return whether a scope was supplied in this url + */ + public boolean isDefaultScope() { + return scope == null; + } + + /** + * Returns the filter. + * + * @return filter + */ + public String getFilter() { + return filter == null ? DEFAULT_FILTER : filter; + } + + /** + * Returns whether a filter was supplied in this url. + * + * @return whether a filter was supplied in this url + */ + public boolean isDefaultFilter() { + return filter == null; + } + + /** + * Returns the formatted URL as scheme://hostname:port/baseDn?attrs?scope?filter. + * + * @return url + */ + public String getUrl() { + + final StringBuilder sb = new StringBuilder(scheme).append("://"); + final String hostname = getHostname(); + if (hostname != null) { + // ipv6 address + if (hostname.contains(":")) { + sb.append("[").append(hostname).append("]"); + } else { + sb.append(hostname); + } + } + sb.append(":").append(getPort()); + sb.append("/").append(LdapUtils.percentEncode(getBaseDn())); + sb.append("?"); + + final String[] attrs = getAttributes(); + for (int i = 0; i < attrs.length; i++) { + sb.append(attrs[i]); + if (i + 1 < attrs.length) { + sb.append(","); + } + } + sb.append("?"); + + final SearchScope scope = getScope(); + if (SearchScope.OBJECT == scope) { + sb.append("base"); + } else if (SearchScope.ONELEVEL == scope) { + sb.append("one"); + } else if (SearchScope.SUBTREE == scope) { + sb.append("sub"); + } + sb.append("?").append(LdapUtils.percentEncode(getFilter())); + return sb.toString(); + } + + /** + * Returns the hostname:port. + * + * @return hostname:port + */ + public String getHostnameWithPort() { + return (getHostname() != null ? getHostname() : "null") + ":" + getPort(); + } + + /** + * Returns the scheme://hostname:port. + * + * @return scheme://hostname:port + */ + public String getHostnameWithSchemeAndPort() { + return getScheme() + "://" + (getHostname() != null ? getHostname() : "null") + ":" + getPort(); + } + + /** + * Returns the retry metadata. + * + * @return metadata describing retry attempts for connections made this URL. + */ + LdapURLRetryMetadata getRetryMetadata() { + return retryMetadata; + } + + /** + * Sets the retry metadata. + * + * @param metadata retry metadata + */ + void setRetryMetadata(final LdapURLRetryMetadata metadata) { + retryMetadata = metadata; + } + + /** + * Returns whether this URL is currently active. + * + * @return true if this URL can be connected to, false otherwise. + */ + boolean isActive() { + return active; + } + + /** + * Marks this URL as active. + */ + void activate() { + active = true; + } + + /** + * Marks this URL as inactive. + */ + void deactivate() { + active = false; + } + + /** + * Returns the resolved IP address. + * + * @return resolved IP address for this URL. + */ + public InetAddress getInetAddress() { + return inetAddress; + } + + /** + * Sets the resolved IP address. + * + * @param address IP address for this URL + */ + void setInetAddress(final InetAddress address) { + inetAddress = address; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof LdapURL) { + final LdapURL v = (LdapURL) o; + return LdapUtils.areEqual(scheme, v.scheme) && + LdapUtils.areEqual(hostname, v.hostname) && + LdapUtils.areEqual(port, v.port) && + LdapUtils.areEqual(baseDn, v.baseDn) && + LdapUtils.areEqual(attributes, v.attributes) && + LdapUtils.areEqual(scope, v.scope) && + LdapUtils.areEqual(filter, v.filter) && + LdapUtils.areEqual(inetAddress, v.inetAddress); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode( + HASH_CODE_SEED, + scheme, + hostname, + port, + baseDn, + attributes, + scope, + filter, + inetAddress); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "scheme=" + scheme + ", " + + "hostname=" + hostname + ", " + + "port=" + port + ", " + + "baseDn=" + baseDn + ", " + + "attributes=" + Arrays.toString(attributes) + ", " + + "scope=" + scope + ", " + + "filter=" + filter + ", " + + "inetAddress=" + inetAddress + "]"; + } + + + /** + * Matches the supplied url against a pattern and reads its components. + * + * @param url to parse + */ + protected void parseURL(final String url) { + final Matcher m = URL_PATTERN.matcher(url); + if (!m.matches()) { + throw new IllegalArgumentException("Invalid LDAP URL: " + url); + } + + // CheckStyle:MagicNumber OFF + scheme = LdapUtils.toLowerCaseAscii(m.group(1)); + hostname = m.group(2); + if (hostname != null) { + // check for ipv6 address + if (hostname.startsWith("[") && hostname.endsWith("]")) { + hostname = hostname.substring(1, hostname.length() - 1).trim(); + } + } + + port = m.group(3) != null ? Integer.parseInt(m.group(3)) : -1; + baseDn = m.group(4) != null ? LdapUtils.percentDecode(m.group(4)) : null; + attributes = m.group(5) != null ? m.group(5).length() > 0 ? m.group(5).split(",") : null : null; + final String scope = m.group(6); + if (scope != null && scope.length() > 0) { + if ("base".equalsIgnoreCase(scope)) { + this.scope = SearchScope.OBJECT; + } else if ("one".equalsIgnoreCase(scope)) { + this.scope = SearchScope.ONELEVEL; + } else if ("sub".equalsIgnoreCase(scope)) { + this.scope = SearchScope.SUBTREE; + } else { + throw new IllegalArgumentException("Invalid scope: " + scope); + } + } + + filter = m.group(7) != null + ? m.group(7).length() > 0 ? LdapUtils.percentDecode(m.group(7)) : null : null; + // CheckStyle:MagicNumber ON + } +} +// CheckStyle:HiddenField ON diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLActivatorService.java b/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLActivatorService.java new file mode 100644 index 0000000..49d35f9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLActivatorService.java @@ -0,0 +1,128 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Singleton which manages a single thread that periodically tests inactive LDAP URLs. + * + */ +public final class LdapURLActivatorService { + + /** + * Ldap activator period system property. + */ + private static final String ACTIVATOR_PERIOD_PROPERTY = "org.xbib.net.ldap.urlActivatorPeriod"; + + /** + * How often to test inactive connections. + */ + private static final Duration ACTIVATOR_PERIOD = Duration.ofMinutes( + Long.parseLong(System.getProperty(ACTIVATOR_PERIOD_PROPERTY, "5"))); + + /** + * Instance of this singleton. + */ + private static final LdapURLActivatorService INSTANCE = new LdapURLActivatorService(); + + /** + * List of inactive URLs to test. + */ + private final List inactiveUrls = new ArrayList<>(); + + /** + * Executor for testing inactive URLs. + */ + private final ScheduledThreadPoolExecutor executor; + + + /** + * Default constructor. + */ + private LdapURLActivatorService() { + executor = new ScheduledThreadPoolExecutor( + 1, + r -> { + final Thread t = new Thread(r, getClass().getSimpleName() + "@" + hashCode()); + t.setDaemon(true); + return t; + }); + executor.scheduleAtFixedRate( + this::testInactiveUrls, + ACTIVATOR_PERIOD.toMillis(), + ACTIVATOR_PERIOD.toMillis(), + TimeUnit.MILLISECONDS); + } + + + /** + * Returns the instance of this singleton. + * + * @return LDAP URL activator service + */ + public static LdapURLActivatorService getInstance() { + return INSTANCE; + } + + + /** + * Returns the activator period. + * + * @return activator period + */ + public static Duration getPeriod() { + return ACTIVATOR_PERIOD; + } + + + /** + * Registers an LDAP URL to be tested for activation. Once a URL becomes active it is automatically removed. + * + * @param url that is inactive and should be tested to become active + */ + public void registerUrl(final LdapURL url) { + inactiveUrls.add(url); + } + + + /** + * Returns the list of inactive urls. + * + * @return inactive urls + */ + public List getInactiveUrls() { + return Collections.unmodifiableList(inactiveUrls); + } + + + /** + * Tests each registered URL. Removes URLs that successfully activated. + */ + void testInactiveUrls() { + for (LdapURL url : inactiveUrls) { + if (!url.isActive() && url.getRetryMetadata().getConnectionStrategy().getRetryCondition().test(url)) { + // note that the activate condition may block + if (url.getRetryMetadata().getConnectionStrategy().getActivateCondition().test(url)) { + url.getRetryMetadata().getConnectionStrategy().success(url); + } else { + url.getRetryMetadata().recordFailure(Instant.now()); + } + } + } + inactiveUrls.removeIf(LdapURL::isActive); + } + + + /** + * Removes all registered inactive URLs. + */ + void clear() { + inactiveUrls.clear(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLRetryMetadata.java b/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLRetryMetadata.java new file mode 100644 index 0000000..85e74f8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLRetryMetadata.java @@ -0,0 +1,40 @@ + +package org.xbib.net.ldap; + +/** + * Retry metadata used by {@link LdapURL}. + * + */ +public class LdapURLRetryMetadata extends AbstractRetryMetadata { + + /** + * Connection strategy associated with this retry. + */ + private final ConnectionStrategy connectionStrategy; + + + /** + * Creates a new LDAP URL retry metadata. + * + * @param strategy connection strategy + */ + public LdapURLRetryMetadata(final ConnectionStrategy strategy) { + connectionStrategy = strategy; + } + + + /** + * Return the connection strategy. + * + * @return connection strategy + */ + public ConnectionStrategy getConnectionStrategy() { + return connectionStrategy; + } + + + @Override + public String toString() { + return super.toString() + ", " + "connectionStrategy=" + connectionStrategy; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLSet.java b/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLSet.java new file mode 100644 index 0000000..4467ab0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/LdapURLSet.java @@ -0,0 +1,121 @@ + +package org.xbib.net.ldap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A set of LDAP URLs with helper functions for common connection strategies. + * + */ +public class LdapURLSet { + + /** + * List of LDAP URLs to connect to in the order provided by the connection strategy. + */ + private final List urls = new ArrayList<>(); + + + /** + * Creates a new LDAP URL set. + * + * @param strategy Connection strategy. + * @param ldapUrls Space-delimited string of URLs describing the LDAP hosts to connect to. The URLs in the string + * are commonly {@code ldap://} or {@code ldaps://} URLs that directly describe the hosts to connect + * to, but may also describe a resource from which to obtain LDAP connection URLs as is the case for + * {@link DnsSrvConnectionStrategy} that use URLs with the scheme {@code dns:}. + */ + public LdapURLSet(final ConnectionStrategy strategy, final String ldapUrls) { + strategy.populate(ldapUrls, this); + } + + + public List getUrls() { + final List l = new ArrayList<>(getActiveUrls()); + if (hasInactiveUrls()) { + l.addAll(getInactiveUrls()); + } + return Collections.unmodifiableList(l); + } + + + /** + * Returns whether this set has any active URLs. + * + * @return whether there are any active LDAP URLs in the set, false otherwise. + */ + public boolean hasActiveUrls() { + return urls.stream().anyMatch(LdapURL::isActive); + } + + + /** + * Returns the active URLs. + * + * @return list of active URLs in order they were added. + */ + public List getActiveUrls() { + return urls.stream().filter(LdapURL::isActive).collect(Collectors.toList()); + } + + + /** + * Returns whether this set has any inactive URLs. + * + * @return whether there are any inactive LDAP URLs in the set, false otherwise. + */ + public boolean hasInactiveUrls() { + return urls.stream().anyMatch(u -> !u.isActive()); + } + + + /** + * Returns the inactive URLs. + * + * @return list of inactive URLs in order they were added. + */ + public List getInactiveUrls() { + return urls.stream().filter(u -> !u.isActive()).collect(Collectors.toList()); + } + + + /** + * Returns the number of URLs in this set. + * + * @return number of URLs in this set + */ + public int size() { + return urls.size(); + } + + + /** + * Populates this set with a list of URLs in the order produced by + * {@link ConnectionStrategy#populate(String, LdapURLSet)}. This method MUST be called before the set is used, but + * MAY be called subsequently periodically to refresh the set of LDAP URLs. + * + * @param ldapUrls LDAP URLs to add to this set. + */ + protected synchronized void populate(final List ldapUrls) { + // Copy activity state from any URLs currently in the set that match new entries + for (LdapURL url : urls) { + final LdapURL match = ldapUrls.stream().filter(u -> u.equals(url)).findFirst().orElse(null); + if (match != null && !url.isActive()) { + match.deactivate(); + } + } + urls.clear(); + urls.addAll(ldapUrls); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "active=" + getActiveUrls() + ", " + + "inactive=" + getInactiveUrls() + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/LdapUtils.java b/net-ldap/src/main/java/org/xbib/net/ldap/LdapUtils.java new file mode 100644 index 0000000..e3fe083 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/LdapUtils.java @@ -0,0 +1,742 @@ + +package org.xbib.net.ldap; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Queue; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.io.Hex; + +/** + * Provides utility methods for this package. + * + */ +public final class LdapUtils { + + /** + * Size of buffer in bytes to use when reading files. + */ + private static final int READ_BUFFER_SIZE = 128; + + /** + * Prime number to assist in calculating hash codes. + */ + private static final int HASH_CODE_PRIME = 113; + + /** + * Pattern to match ipv4 addresses. + */ + private static final Pattern IPV4_PATTERN = Pattern.compile( + "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)" + + "(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$"); + + /** + * Pattern to match ipv6 addresses. + */ + private static final Pattern IPV6_STD_PATTERN = Pattern.compile("^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$"); + + /** + * Pattern to match ipv6 hex compressed addresses. + */ + private static final Pattern IPV6_HEX_COMPRESSED_PATTERN = Pattern.compile( + "^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::" + + "((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$"); + + /** + * Pattern that matches control characters. + */ + private static final Pattern CNTRL_PATTERN = Pattern.compile("\\p{Cntrl}"); + + + /** + * Default constructor. + */ + private LdapUtils() { + } + + + /** + * This will convert the supplied value to a base64 encoded string. Returns null if the supplied byte array is null. + * + * @param value to base64 encode + * @return base64 encoded value + */ + public static String base64Encode(final byte... value) { + return value != null ? new String(Base64.getEncoder().encode(value), StandardCharsets.UTF_8) : null; + } + + + /** + * This will convert the supplied value to a base64 encoded string. Returns null if the supplied string is null. + * + * @param value to base64 encode + * @return base64 encoded value + */ + public static String base64Encode(final String value) { + return value != null ? base64Encode(value.getBytes(StandardCharsets.UTF_8)) : null; + } + + + /** + * This will convert the supplied value to a UTF-8 encoded string. Returns null if the supplied byte array is null. + * + * @param value to UTF-8 encode + * @return UTF-8 encoded value + */ + public static String utf8Encode(final byte[] value) { + return utf8Encode(value, true); + } + + + /** + * This will convert the supplied value to a UTF-8 encoded string. + * + * @param value to UTF-8 encode + * @param allowNull whether to throw {@link NullPointerException} if value is null + * @return UTF-8 encoded value + * @throws NullPointerException if allowNull is false and value is null + */ + public static String utf8Encode(final byte[] value, final boolean allowNull) { + if (!allowNull && value == null) { + throw new NullPointerException("Cannot UTF-8 encode null value"); + } + return value != null ? new String(value, StandardCharsets.UTF_8) : null; + } + + + /** + * This will convert the supplied value to a UTF-8 encoded byte array. Returns null if the supplied string is null. + * + * @param value to UTF-8 encode + * @return UTF-8 encoded value + */ + public static byte[] utf8Encode(final String value) { + return utf8Encode(value, true); + } + + + /** + * This will convert the supplied value to a UTF-8 encoded byte array. + * + * @param value to UTF-8 encode + * @param allowNull whether to throw {@link NullPointerException} if value is null + * @return UTF-8 encoded value + * @throws NullPointerException if allowNull is false and value is null + */ + public static byte[] utf8Encode(final String value, final boolean allowNull) { + if (!allowNull && value == null) { + throw new NullPointerException("Cannot UTF-8 encode null value"); + } + return value != null ? value.getBytes(StandardCharsets.UTF_8) : null; + } + + + /** + * This will convert the supplied value to a hex encoded string. Returns null if the supplied byte array is null. + * + * @param value to hex encode + * @return hex encoded value + */ + public static char[] hexEncode(final byte... value) { + return value != null ? Hex.encode(value) : null; + } + + + /** + * This will convert the supplied value to a hex encoded string. Returns null if the supplied char array is null. + * + * @param value to hex encode + * @return hex encoded value + */ + public static char[] hexEncode(final char... value) { + return value != null ? hexEncode(utf8Encode(String.valueOf(value))) : null; + } + + + /** + * Implementation of percent encoding as described in RFC 3986 section 2.1. + * + * @param value to encode + * @return percent encoded value + */ + public static String percentEncode(final String value) { + if (value == null) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + final char ch = value.charAt(i); + // uppercase + if (ch >= 'A' && ch <= 'Z') { + sb.append(ch); + // lowercase + } else if (ch >= 'a' && ch <= 'z') { + sb.append(ch); + // digit + } else if (ch >= '0' && ch <= '9') { + sb.append(ch); + } else { + // unreserved and reserved + switch (ch) { + + case '-': + case '.': + case '_': + case '~': + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + sb.append(ch); + break; + + default: + sb.append("%"); + // CheckStyle:MagicNumber OFF + if (ch <= 0x7F) { + sb.append(hexEncode((byte) (ch & 0x7F))); + } else { + sb.append(hexEncode(utf8Encode(String.valueOf(ch)))); + } + // CheckStyle:MagicNumber ON + } + } + } + return sb.toString(); + } + + + /** + * Converts all characters <= 0x1F and 0x7F to percent encoded hex. + * + * @param value to encode control characters in + * @return string with percent encoded hex characters + */ + public static String percentEncodeControlChars(final String value) { + if (value != null) { + final Matcher m = CNTRL_PATTERN.matcher(value); + if (m.find()) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + final char ch = value.charAt(i); + // CheckStyle:MagicNumber OFF + if (ch <= 0x1F || ch == 0x7F) { + sb.append("%"); + sb.append(hexEncode((byte) (ch & 0x7F))); + } else { + sb.append(ch); + } + // CheckStyle:MagicNumber ON + } + return sb.toString(); + } + } + return value; + } + + + /** + * Removes the space character from both the beginning and end of the supplied value. + * + * @param value to trim space character from + * @return trimmed value or same value if no trim was performed + */ + public static String trimSpace(final String value) { + if (value == null || value.isEmpty()) { + return value; + } + + int startIndex = 0; + int endIndex = value.length(); + while (startIndex < endIndex && value.charAt(startIndex) == ' ') { + startIndex++; + } + while (startIndex < endIndex && value.charAt(endIndex - 1) == ' ') { + endIndex--; + } + if (startIndex == 0 && endIndex == value.length()) { + return value; + } + return value.substring(startIndex, endIndex); + } + + + /** + * Changes the supplied value by replacing multiple spaces with a single space. + * + * @param value to compress spaces + * @param trim whether to remove any leading or trailing space characters + * @return normalized value or value if no compress was performed + */ + public static String compressSpace(final String value, final boolean trim) { + if (value == null || value.isEmpty()) { + return value; + } + + final StringBuilder sb = new StringBuilder(); + boolean foundSpace = false; + for (int i = 0; i < value.length(); i++) { + final char ch = value.charAt(i); + if (ch == ' ') { + if (i == value.length() - 1) { + // last char is a space + sb.append(ch); + } + foundSpace = true; + } else { + if (foundSpace) { + sb.append(' '); + } + sb.append(ch); + foundSpace = false; + } + } + + if (sb.length() == 0 && foundSpace) { + return trim ? "" : " "; + } + if (trim) { + if (sb.length() > 0 && sb.charAt(0) == ' ') { + sb.deleteCharAt(0); + } + if (sb.length() > 0 && sb.charAt(sb.length() - 1) == ' ') { + sb.deleteCharAt(sb.length() - 1); + } + } + return sb.toString(); + } + + + /** + * This will decode the supplied value as a base64 encoded string to a byte[]. Returns null if the supplied string is + * null. + * + * @param value to base64 decode + * @return base64 decoded value + */ + public static byte[] base64Decode(final String value) { + try { + return value != null ? Base64.getDecoder().decode(value.getBytes(StandardCharsets.UTF_8)) : null; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error decoding value: " + value, e); + } + } + + + /** + * This will decode the supplied value as a hex encoded string to a byte[]. Returns null if the supplied character + * array is null. + * + * @param value to hex decode + * @return hex decoded value + */ + public static byte[] hexDecode(final char[] value) { + return value != null ? Hex.decode(value) : null; + } + + + /** + * Implementation of percent decoding as described in RFC 3986 section 2.1. + * + * @param value to decode + * @return percent decoded value + */ + public static String percentDecode(final String value) { + if (value == null || !value.contains("%")) { + return value; + } + + final StringBuilder sb = new StringBuilder(); + int pos = 0; + while (pos < value.length()) { + final char c = value.charAt(pos++); + if (c == '%') { + final char[] hex = new char[]{ + value.charAt(pos++), + value.charAt(pos++), + }; + sb.append(utf8Encode(hexDecode(hex))); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + + /** + * See {@link #shouldBase64Encode(byte[])}. + * + * @param value to inspect + * @return whether the value should be base64 encoded + */ + public static boolean shouldBase64Encode(final String value) { + return shouldBase64Encode(value.getBytes(StandardCharsets.UTF_8)); + } + + + /** + * Determines whether the supplied value should be base64 encoded. See http://www.faqs.org/rfcs/rfc2849.html for more + * details. + * + * @param value to inspect + * @return whether the value should be base64 encoded + */ + public static boolean shouldBase64Encode(final byte[] value) { + if (value == null || value.length == 0) { + return false; + } + + boolean encode = false; + + // CheckStyle:MagicNumber OFF + // check first byte in value + switch (value[0] & 0xFF) { + // check for SP + case 0x20: + // check for colon(:) + case 0x3A: + // check for left arrow(<) + case 0x3C: + encode = true; + break; + default: + break; + } + + if (!encode) { + // check for SP at last byte in value + if ((value[value.length - 1] & 0xFF) == 0x20) { + encode = true; + } else { + // check remaining bytes in the value + for (final byte b : value) { + switch (b & 0xFF) { + // check for NUL + case 0x00: + // check for LF + case 0x0A: + // check for CR + case 0x0D: + encode = true; + break; + + default: + // check for any character above 127 + if ((b & 0x80) != 0x00) { + encode = true; + } + break; + } + if (encode) { + break; + } + } + } + } + // CheckStyle:MagicNumber ON + + return encode; + } + + + /** + * Converts the supplied string to lower case. If the string contains non-ascii characters, {@link Locale#ROOT} is + * used. + * + * @param s to lower case + * @return new lower case string + */ + public static String toLowerCase(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + // CheckStyle:MagicNumber OFF + // if string contains non-ascii, use locale specific lowercase + if (s.chars().anyMatch(c -> c > 0x7F)) { + return s.toLowerCase(Locale.ROOT); + } + return toLowerCaseAscii(s); + } + + + /** + * Converts the characters A-Z to a-z. + * + * @param s to lower case + * @return new string with lower case alphabetical characters + * @throws IllegalArgumentException if the supplied string contains non-ascii characters + */ + public static String toLowerCaseAscii(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + // mutate A-Z to a-z + // CheckStyle:MagicNumber OFF + final char[] chars = s.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (chars[i] > 0x7F) { + throw new IllegalArgumentException("String contains non-ascii characters: " + s); + } else if (chars[i] >= 'A' && chars[i] <= 'Z') { + chars[i] = (char) (chars[i] + 32); + } + } + // CheckStyle:MagicNumber ON + return new String(chars); + } + + + /** + * Converts the supplied string to upper case. If the string contains non-ascii characters, {@link Locale#ROOT} is + * used. + * + * @param s to upper case + * @return new upper case string + */ + public static String toUpperCase(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + // CheckStyle:MagicNumber OFF + // if string contains non-ascii, use locale specific uppercase + if (s.chars().anyMatch(c -> c > 0x7F)) { + return s.toUpperCase(Locale.ROOT); + } + return toUpperCaseAscii(s); + } + + + /** + * Converts the characters a-z to A-Z. + * + * @param s to upper case + * @return new string with upper case alphabetical characters + * @throws IllegalArgumentException if the supplied string contains non-ascii characters + */ + public static String toUpperCaseAscii(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + // mutate a-z to A-Z + // CheckStyle:MagicNumber OFF + final char[] chars = s.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (chars[i] > 0x7F) { + throw new IllegalArgumentException("String contains non-ascii characters: " + s); + } else if (chars[i] >= 'a' && chars[i] <= 'z') { + chars[i] = (char) (chars[i] - 32); + } + } + // CheckStyle:MagicNumber ON + return new String(chars); + } + + + /** + * Reads the data in the supplied stream and returns it as a byte array. + * + * @param is stream to read + * @return bytes read from the stream + * @throws IOException if an error occurs reading data + */ + public static byte[] readInputStream(final InputStream is) + throws IOException { + final ByteArrayOutputStream data = new ByteArrayOutputStream(); + try (is; data) { + final byte[] buffer = new byte[READ_BUFFER_SIZE]; + int length; + while ((length = is.read(buffer)) != -1) { + data.write(buffer, 0, length); + } + } + return data.toByteArray(); + } + + + /** + * Concatenates multiple arrays together. + * + * @param type of array + * @param first array to concatenate. Cannot be null. + * @param rest of the arrays to concatenate. May be null. + * @return array containing the concatenation of all parameters + */ + @SuppressWarnings("unchecked") + public static T[] concatArrays(final T[] first, final T[]... rest) { + int totalLength = first.length; + for (T[] array : rest) { + if (array != null) { + totalLength += array.length; + } + } + + final T[] result = Arrays.copyOf(first, totalLength); + + int offset = first.length; + for (T[] array : rest) { + if (array != null) { + System.arraycopy(array, 0, result, offset, array.length); + offset += array.length; + } + } + return result; + } + + + /** + * Determines equality of the supplied objects. Array types are automatically detected. + * + * @param o1 to test equality of + * @param o2 to test equality of + * @return whether o1 equals o2 + */ + public static boolean areEqual(final Object o1, final Object o2) { + if (o1 == o2) { + return true; + } + final boolean areEqual; + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + areEqual = Arrays.equals((boolean[]) o1, (boolean[]) o2); + } else if (o1 instanceof byte[] && o2 instanceof byte[]) { + areEqual = Arrays.equals((byte[]) o1, (byte[]) o2); + } else if (o1 instanceof char[] && o2 instanceof char[]) { + areEqual = Arrays.equals((char[]) o1, (char[]) o2); + } else if (o1 instanceof double[] && o2 instanceof double[]) { + areEqual = Arrays.equals((double[]) o1, (double[]) o2); + } else if (o1 instanceof float[] && o2 instanceof float[]) { + areEqual = Arrays.equals((float[]) o1, (float[]) o2); + } else if (o1 instanceof int[] && o2 instanceof int[]) { + areEqual = Arrays.equals((int[]) o1, (int[]) o2); + } else if (o1 instanceof long[] && o2 instanceof long[]) { + areEqual = Arrays.equals((long[]) o1, (long[]) o2); + } else if (o1 instanceof short[] && o2 instanceof short[]) { + areEqual = Arrays.equals((short[]) o1, (short[]) o2); + } else if (o1 instanceof Object[] && o2 instanceof Object[]) { + areEqual = Arrays.deepEquals((Object[]) o1, (Object[]) o2); + } else { + areEqual = o1 != null && o1.equals(o2); + } + return areEqual; + } + + + /** + * Computes a hash code for the supplied objects using the supplied seed. If a Collection type is found it is iterated + * over. + * + * @param seed odd/prime number + * @param objects to calculate hashCode for + * @return hash code for the supplied objects + */ + public static int computeHashCode(final int seed, final Object... objects) { + if (objects == null || objects.length == 0) { + return seed * HASH_CODE_PRIME; + } + + int hc = seed; + for (Object object : objects) { + hc = HASH_CODE_PRIME * hc; + if (object != null) { + if (object instanceof List || object instanceof Queue) { + int index = 1; + for (Object o : (Collection) object) { + hc += computeHashCode(o) * index++; + } + } else if (object instanceof Collection) { + for (Object o : (Collection) object) { + hc += computeHashCode(o); + } + } else { + hc += computeHashCode(object); + } + } + } + return hc; + } + + + /** + * Computes a hash code for the supplied object. Checks for arrays of primitives and Objects then delegates to the + * {@link Arrays} class. Otherwise {@link Object#hashCode()} is invoked. + * + * @param object to calculate hash code for + * @return hash code + */ + private static int computeHashCode(final Object object) { + int hc = 0; + if (object instanceof boolean[]) { + hc += Arrays.hashCode((boolean[]) object); + } else if (object instanceof byte[]) { + hc += Arrays.hashCode((byte[]) object); + } else if (object instanceof char[]) { + hc += Arrays.hashCode((char[]) object); + } else if (object instanceof double[]) { + hc += Arrays.hashCode((double[]) object); + } else if (object instanceof float[]) { + hc += Arrays.hashCode((float[]) object); + } else if (object instanceof int[]) { + hc += Arrays.hashCode((int[]) object); + } else if (object instanceof long[]) { + hc += Arrays.hashCode((long[]) object); + } else if (object instanceof short[]) { + hc += Arrays.hashCode((short[]) object); + } else if (object instanceof Object[]) { + hc += Arrays.deepHashCode((Object[]) object); + } else { + hc += object.hashCode(); + } + return hc; + } + + + /** + * Returns whether the supplied string represents an IP address. Matches both IPv4 and IPv6 addresses. + * + * @param s to match + * @return whether the supplied string represents an IP address + */ + public static boolean isIPAddress(final String s) { + return + s != null && + (IPV4_PATTERN.matcher(s).matches() || IPV6_STD_PATTERN.matcher(s).matches() || + IPV6_HEX_COMPRESSED_PATTERN.matcher(s).matches()); + } + + + /** + * Looks for the supplied system property value and loads a class with that name. The default constructor for that + * class is then returned. + * + * @param property whose value is a class + * @return class constructor or null if no system property was found + * @throws IllegalArgumentException if an error occurs instantiating the constructor + */ + public static Constructor createConstructorFromProperty(final String property) { + final String clazz = System.getProperty(property); + if (clazz != null) { + try { + return Class.forName(clazz).getDeclaredConstructor(); + } catch (Exception e) { + throw new IllegalArgumentException("Error getting declared constructor for " + clazz, e); + } + } + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/Message.java b/net-ldap/src/main/java/org/xbib/net/ldap/Message.java new file mode 100644 index 0000000..78da2dd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/Message.java @@ -0,0 +1,45 @@ + +package org.xbib.net.ldap; + +import org.xbib.net.ldap.control.ResponseControl; + +/** + * LDAP protocol response. + * + */ +public interface Message { + + + /** + * Returns the ID for this message. + * + * @return message ID + */ + int getMessageID(); + + + /** + * Returns the response controls for this message. + * + * @return response controls + */ + ResponseControl[] getControls(); + + + /** + * Returns the first response control with the supplied OID. + * + * @param oid of the response control to return + * @return response control or null if control could not be found + */ + default ResponseControl getControl(final String oid) { + if (getControls() != null) { + for (ResponseControl c : getControls()) { + if (c != null && c.getOID().equals(oid)) { + return c; + } + } + } + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnOperation.java new file mode 100644 index 0000000..857eb9f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnOperation.java @@ -0,0 +1,148 @@ + +package org.xbib.net.ldap; + +/** + * Executes an ldap modify DN operation. + * + */ +public class ModifyDnOperation extends AbstractOperation { + + + /** + * Default constructor. + */ + public ModifyDnOperation() { + } + + + /** + * Creates a new modify DN operation. + * + * @param factory connection factory + */ + public ModifyDnOperation(final ConnectionFactory factory) { + super(factory); + } + + /** + * Sends a modify DN request. See {@link OperationHandle#send()}. + * + * @param factory connection factory + * @param request modify DN request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + public static OperationHandle send( + final ConnectionFactory factory, + final ModifyDnRequest request) + throws LdapException { + final Connection conn = factory.getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return conn.operation(request).onComplete(conn::close).send(); + } + + /** + * Executes a modify DN request. See {@link OperationHandle#execute()}. + * + * @param factory connection factory + * @param request modify dn request + * @return modify dn result + * @throws LdapException if the connection cannot be opened + */ + public static ModifyDnResponse execute(final ConnectionFactory factory, final ModifyDnRequest request) + throws LdapException { + try (Connection conn = factory.getConnection()) { + conn.open(); + return conn.operation(request).execute(); + } + } + + /** + * Returns a new modify dn operation with the same properties as the supplied operation. + * + * @param operation to copy + * @return copy of the supplied modify dn operation + */ + public static ModifyDnOperation copy(final ModifyDnOperation operation) { + final ModifyDnOperation op = new ModifyDnOperation(); + op.setRequestHandlers(operation.getRequestHandlers()); + op.setResultHandlers(operation.getResultHandlers()); + op.setControlHandlers(operation.getControlHandlers()); + op.setReferralHandlers(operation.getReferralHandlers()); + op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers()); + op.setExceptionHandler(operation.getExceptionHandler()); + op.setThrowCondition(operation.getThrowCondition()); + op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers()); + op.setConnectionFactory(operation.getConnectionFactory()); + return op; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Sends a modify DN request. See {@link OperationHandle#send()}. + * + * @param request modify DN request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + @Override + public OperationHandle send(final ModifyDnRequest request) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send(); + } + + /** + * Executes a modify DN request. See {@link OperationHandle#execute()}. + * + * @param request modify DN request + * @return modify DN result + * @throws LdapException if the connection cannot be opened + */ + public ModifyDnResponse execute(final ModifyDnRequest request) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + return configureHandle(conn.operation(configureRequest(request))).execute(); + } + } + + /** + * Modify DN operation builder. + */ + public static class Builder extends AbstractOperation.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new ModifyDnOperation()); + } + + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnRequest.java new file mode 100644 index 0000000..e5cc85b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnRequest.java @@ -0,0 +1,231 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP modify DN request defined as: + * + *
+ * ModifyDNRequest ::= [APPLICATION 12] SEQUENCE {
+ * entry           LDAPDN,
+ * newrdn          RelativeLDAPDN,
+ * deleteoldrdn    BOOLEAN,
+ * newSuperior     [0] LDAPDN OPTIONAL }
+ * 
+ * + */ +public class ModifyDnRequest extends AbstractRequestMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 12; + + /** + * DN to modify. + */ + private String oldModifyDn; + + /** + * New DN. + */ + private String newModifyRDn; + + /** + * Whether to delete the old RDN attribute. + */ + private boolean deleteOldRDn; + + /** + * New superior DN. + */ + private String newSuperiorDn; + + + /** + * Default constructor. + */ + private ModifyDnRequest() { + } + + + /** + * Creates a new modify DN request. + * + * @param oldDN old modify DN + * @param newRDN new modify DN + * @param delete whether to delete the old RDN attribute + */ + public ModifyDnRequest(final String oldDN, final String newRDN, final boolean delete) { + this(oldDN, newRDN, delete, null); + } + + + /** + * Creates a new modify DN request. + * + * @param oldDN old modify DN + * @param newRDN new modify DN + * @param delete whether to delete the old RDN attribute + * @param newSuperior new superior DN + */ + public ModifyDnRequest(final String oldDN, final String newRDN, final boolean delete, final String newSuperior) { + oldModifyDn = oldDN; + newModifyRDn = newRDN; + deleteOldRDn = delete; + newSuperiorDn = newSuperior; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the old DN. + * + * @return old DN + */ + public String getOldDn() { + return oldModifyDn; + } + + /** + * Returns the new RDN. + * + * @return new RDN + */ + public String getNewRDn() { + return newModifyRDn; + } + + /** + * Whether to delete the old RDN. + * + * @return whether to delete the old RDN + */ + public boolean isDeleteOldRDn() { + return deleteOldRDn; + } + + /** + * Returns the new superior DN. + * + * @return new superior DN + */ + public String getNewSuperiorDn() { + return newSuperiorDn; + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + if (newSuperiorDn == null) { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new OctetStringType(oldModifyDn), + new OctetStringType(newModifyRDn), + new BooleanType(deleteOldRDn)), + }; + } else { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new OctetStringType(oldModifyDn), + new OctetStringType(newModifyRDn), + new BooleanType(deleteOldRDn), + new OctetStringType(new ContextDERTag(0, false), newSuperiorDn)), + }; + } + } + + @Override + public String toString() { + return super.toString() + ", " + + "oldModifyDn=" + oldModifyDn + ", " + + "newModifyRDn=" + newModifyRDn + ", " + + "delete=" + deleteOldRDn + ", " + + "superior=" + newSuperiorDn; + } + + /** + * Modify DN request builder. + */ + public static class Builder extends AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new ModifyDnRequest()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the old modify ldap DN. + * + * @param dn ldap DN + * @return this builder + */ + public Builder oldDN(final String dn) { + object.oldModifyDn = dn; + return self(); + } + + + /** + * Sets the new modify ldap DN. + * + * @param dn ldap DN + * @return this builder + */ + public Builder newRDN(final String dn) { + object.newModifyRDn = dn; + return self(); + } + + + /** + * Sets whether to delete the old RDN. + * + * @param delete whether to delete the old RDN + * @return this builder + */ + public Builder delete(final boolean delete) { + object.deleteOldRDn = delete; + return self(); + } + + + /** + * Sets the new superior ldap DN. + * + * @param dn ldap DN + * @return this builder + */ + public Builder superior(final String dn) { + object.newSuperiorDn = dn; + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnResponse.java new file mode 100644 index 0000000..b2be5b5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyDnResponse.java @@ -0,0 +1,117 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; + +/** + * LDAP modify DN response defined as: + * + *
+ * ModifyDNResponse ::= [APPLICATION 13] LDAPResult
+ * 
+ * + */ +public class ModifyDnResponse extends AbstractResult { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 13; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10271; + + /** + * DER path to result code. + */ + private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(13)/ENUM[0]"); + + /** + * DER path to matched DN. + */ + private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(13)/OCTSTR[1]"); + + /** + * DER path to diagnostic message. + */ + private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(13)/OCTSTR[2]"); + + /** + * DER path to referral. + */ + private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(13)/CTX(3)/OCTSTR[0]"); + + + /** + * Default constructor. + */ + private ModifyDnResponse() { + } + + + /** + * Creates a new modify DN response. + * + * @param buffer to decode + */ + public ModifyDnResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this)); + parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this)); + parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this)); + parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ModifyDnResponse && super.equals(o); + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs()); + } + + // CheckStyle:OFF + public static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new ModifyDnResponse()); + } + + + @Override + protected Builder self() { + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ModifyOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyOperation.java new file mode 100644 index 0000000..0e076bd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyOperation.java @@ -0,0 +1,148 @@ + +package org.xbib.net.ldap; + +/** + * Executes an ldap modify operation. + * + */ +public class ModifyOperation extends AbstractOperation { + + + /** + * Default constructor. + */ + public ModifyOperation() { + } + + + /** + * Creates a new modify operation. + * + * @param factory connection factory + */ + public ModifyOperation(final ConnectionFactory factory) { + super(factory); + } + + /** + * Sends a modify request. See {@link OperationHandle#send()}. + * + * @param factory connection factory + * @param request modify request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + public static OperationHandle send( + final ConnectionFactory factory, + final ModifyRequest request) + throws LdapException { + final Connection conn = factory.getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return conn.operation(request).onComplete(conn::close).send(); + } + + /** + * Executes a modify request. See {@link OperationHandle#execute()}. + * + * @param factory connection factory + * @param request modify request + * @return modify result + * @throws LdapException if the connection cannot be opened + */ + public static ModifyResponse execute(final ConnectionFactory factory, final ModifyRequest request) + throws LdapException { + try (Connection conn = factory.getConnection()) { + conn.open(); + return conn.operation(request).execute(); + } + } + + /** + * Returns a new modify operation with the same properties as the supplied operation. + * + * @param operation to copy + * @return copy of the supplied modify operation + */ + public static ModifyOperation copy(final ModifyOperation operation) { + final ModifyOperation op = new ModifyOperation(); + op.setRequestHandlers(operation.getRequestHandlers()); + op.setResultHandlers(operation.getResultHandlers()); + op.setControlHandlers(operation.getControlHandlers()); + op.setReferralHandlers(operation.getReferralHandlers()); + op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers()); + op.setExceptionHandler(operation.getExceptionHandler()); + op.setThrowCondition(operation.getThrowCondition()); + op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers()); + op.setConnectionFactory(operation.getConnectionFactory()); + return op; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Sends a modify request. See {@link OperationHandle#send()}. + * + * @param request modify request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + @Override + public OperationHandle send(final ModifyRequest request) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send(); + } + + /** + * Executes a modify request. See {@link OperationHandle#execute()}. + * + * @param request modify request + * @return modify result + * @throws LdapException if the connection cannot be opened + */ + public ModifyResponse execute(final ModifyRequest request) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + return configureHandle(conn.operation(configureRequest(request))).execute(); + } + } + + /** + * Modify operation builder. + */ + public static class Builder extends AbstractOperation.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new ModifyOperation()); + } + + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ModifyRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyRequest.java new file mode 100644 index 0000000..daf24c9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyRequest.java @@ -0,0 +1,202 @@ + +package org.xbib.net.ldap; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * LDAP modify request defined as: + * + *
+ * ModifyRequest ::= [APPLICATION 6] SEQUENCE {
+ * object          LDAPDN,
+ * changes         SEQUENCE OF change SEQUENCE {
+ * operation       ENUMERATED {
+ * add     (0),
+ * delete  (1),
+ * replace (2),
+ * ...  },
+ * modification    PartialAttribute } }
+ *
+ * PartialAttribute ::= SEQUENCE {
+ * type       AttributeDescription,
+ * vals       SET OF value AttributeValue }
+ *
+ * Attribute ::= PartialAttribute(WITH COMPONENTS {
+ * ...,
+ * vals (SIZE(1..MAX))})
+ * 
+ * + */ +public class ModifyRequest extends AbstractRequestMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 6; + + /** + * Empty byte. + */ + private static final byte[] EMPTY_BYTE = new byte[0]; + + /** + * LDAP DN to modify. + */ + private String ldapDn; + + /** + * Modifications to perform. + */ + private AttributeModification[] modifications; + + + /** + * Default constructor. + */ + private ModifyRequest() { + } + + + /** + * Creates a new modify request. + * + * @param entry DN to modify + * @param mod to make on the object + */ + public ModifyRequest(final String entry, final AttributeModification... mod) { + ldapDn = entry; + modifications = mod; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the DN. + * + * @return DN + */ + public String getDn() { + return ldapDn; + } + + /** + * Returns the attribute modifications. + * + * @return attributes modifications + */ + public AttributeModification[] getModifications() { + return modifications; + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new OctetStringType(ldapDn), + new ConstructedDEREncoder( + UniversalDERTag.SEQ, + Stream.of(modifications).map(m -> + new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(UniversalDERTag.ENUM, m.getOperation().ordinal()), + new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new OctetStringType(m.getAttribute().getName()), + new ConstructedDEREncoder( + UniversalDERTag.SET, + getAttributeValueEncoders(m.getAttribute().getBinaryValues()))))) + .toArray(DEREncoder[]::new))), + }; + } + + /** + * Returns attribute value encoders for the supplied values. + * + * @param values to create encoders for + * @return attribute value encoders + */ + private DEREncoder[] getAttributeValueEncoders(final Collection values) { + if (values == null || values.size() == 0) { + return new DEREncoder[]{() -> EMPTY_BYTE}; + } + return values.stream().map(OctetStringType::new).toArray(DEREncoder[]::new); + } + + @Override + public String toString() { + return super.toString() + ", " + "dn=" + ldapDn + ", " + "modifications=" + Arrays.toString(modifications); + } + + /** + * Modify request builder. + */ + public static class Builder extends AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new ModifyRequest()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the ldap DN. + * + * @param dn ldap DN + * @return this builder + */ + public Builder dn(final String dn) { + object.ldapDn = dn; + return self(); + } + + + /** + * Sets the modifications. + * + * @param mod modifications + * @return this builder + */ + public Builder modifications(final AttributeModification... mod) { + object.modifications = mod; + return self(); + } + + + /** + * Sets the modifications. + * + * @param mod modifications + * @return this builder + */ + public Builder modifications(final Collection mod) { + object.modifications = mod.toArray(AttributeModification[]::new); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ModifyResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyResponse.java new file mode 100644 index 0000000..74bcbd1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ModifyResponse.java @@ -0,0 +1,117 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; + +/** + * LDAP modify response defined as: + * + *
+ * ModifyResponse ::= [APPLICATION 7] LDAPResult
+ * 
+ * + */ +public class ModifyResponse extends AbstractResult { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 7; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10273; + + /** + * DER path to result code. + */ + private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(7)/ENUM[0]"); + + /** + * DER path to matched DN. + */ + private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(7)/OCTSTR[1]"); + + /** + * DER path to diagnostic message. + */ + private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(7)/OCTSTR[2]"); + + /** + * DER path to referral. + */ + private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(7)/CTX(3)/OCTSTR[0]"); + + + /** + * Default constructor. + */ + private ModifyResponse() { + } + + + /** + * Creates a new modify response. + * + * @param buffer to decode + */ + public ModifyResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this)); + parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this)); + parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this)); + parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ModifyResponse && super.equals(o); + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs()); + } + + // CheckStyle:OFF + public static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new ModifyResponse()); + } + + + @Override + protected Builder self() { + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/Operation.java b/net-ldap/src/main/java/org/xbib/net/ldap/Operation.java new file mode 100644 index 0000000..fd9157f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/Operation.java @@ -0,0 +1,31 @@ + +package org.xbib.net.ldap; + +/** + * Operation interface. + * + * @param type of request + * @param type of result + */ +public interface Operation { + + + /** + * Sends an asynchronous request and does not wait for a response. + * + * @param request operation request + * @return operation result + * @throws LdapException if the operation fails + */ + OperationHandle send(Q request) throws LdapException; + + + /** + * Sends an asynchronous request and waits for the response. + * + * @param request operation request + * @return operation result + * @throws LdapException if the operation fails + */ + S execute(Q request) throws LdapException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/OperationHandle.java b/net-ldap/src/main/java/org/xbib/net/ldap/OperationHandle.java new file mode 100644 index 0000000..88e08f5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/OperationHandle.java @@ -0,0 +1,158 @@ + +package org.xbib.net.ldap; + +import java.time.Instant; +import org.xbib.net.ldap.extended.ExtendedOperationHandle; +import org.xbib.net.ldap.handler.CompleteHandler; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Handle that notifies on the components of an LDAP operation request. + * + * @param type of request + * @param type of result + */ +public interface OperationHandle { + + + /** + * Sends this request to the server. + * + * @return this handle + * @throws IllegalStateException if this request has already been sent + */ + OperationHandle send(); + + + /** + * Waits for a result or reports a timeout exception. + * + * @return result of the operation or empty if the operation is abandoned + * @throws LdapException if an error occurs executing the request + */ + S await() throws LdapException; + + + /** + * Convenience method that invokes {@link #send()} followed by {@link #await()}. Provides a single method to make a + * synchronous request. + * + * @return result of the operation or empty if the operation is abandoned + * @throws LdapException if an error occurs executing the request + */ + default S execute() + throws LdapException { + return send().await(); + } + + + /** + * Sets the functions to execute when a result is received. + * + * @param function to execute on a result + * @return this handle + */ + OperationHandle onResult(ResultHandler... function); + + + /** + * Sets the functions to execute when a control is received. + * + * @param function to execute on a control + * @return this handle + */ + OperationHandle onControl(ResponseControlHandler... function); + + + /** + * Sets the functions to execute when a referral is received. + * + * @param function to execute on a referral + * @return this handle + */ + OperationHandle onReferral(ReferralHandler... function); + + + /** + * Sets the functions to execute when an intermediate response is received. + * + * @param function to execute on an intermediate response + * @return this handle + */ + OperationHandle onIntermediate(IntermediateResponseHandler... function); + + + /** + * Sets the functions to execute when an unsolicited notification is received. + * + * @param function to execute on an unsolicited notification + * @return this handle + */ + OperationHandle onUnsolicitedNotification(UnsolicitedNotificationHandler... function); + + + /** + * Sets the function to execute when an exception occurs. + * + * @param function to execute when an exception occurs + * @return this handle + */ + OperationHandle onException(ExceptionHandler function); + + + /** + * Sets the function to execute when the operation completes. + * + * @param function to execute on completion + * @return this handle + */ + OperationHandle onComplete(CompleteHandler function); + + + /** + * Sets the function to determine whether an exception should be raised by a particular result. + * + * @param function to determine whether to throw an exception + * @return this handle + */ + OperationHandle throwIf(ResultPredicate function); + + + /** + * Abandons this operation. + * + * @throws IllegalStateException if the request has not been sent to the server + */ + void abandon(); + + + /** + * Cancels this operation. See {@link org.xbib.net.ldap.extended.CancelRequest}. + * + * @return extended operation handle + * @throws IllegalStateException if the request has not been sent to the server + */ + ExtendedOperationHandle cancel(); + + + /** + * Returns the time this operation sent a request. + * + * @return sent time + */ + Instant getSentTime(); + + + /** + * Returns the time this operation received a result or encountered an exception. + * + * @return received time + */ + Instant getReceivedTime(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/PooledConnectionFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/PooledConnectionFactory.java new file mode 100644 index 0000000..4b482e1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/PooledConnectionFactory.java @@ -0,0 +1,337 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; +import java.time.Instant; +import java.util.function.BiPredicate; +import org.xbib.net.ldap.pool.BlockingConnectionPool; +import org.xbib.net.ldap.pool.ConnectionActivator; +import org.xbib.net.ldap.pool.ConnectionPassivator; +import org.xbib.net.ldap.pool.PoolException; +import org.xbib.net.ldap.pool.PruneStrategy; +import org.xbib.net.ldap.pool.ValidationException; +import org.xbib.net.ldap.pool.ValidationExceptionHandler; +import org.xbib.net.ldap.transport.Transport; +import org.xbib.net.ldap.transport.TransportFactory; + +/** + * Creates connections for performing ldap operations and manages those connections as a pool. + * + */ +public class PooledConnectionFactory extends BlockingConnectionPool implements ConnectionFactory { + + /** + * Validation exception handler. Default implementation retries once. + */ + private ValidationExceptionHandler validationExceptionHandler = new RetryValidationExceptionHandler(); + + + /** + * Default constructor. + */ + public PooledConnectionFactory() { + setDefaultConnectionFactory( + new DefaultConnectionFactory(TransportFactory.getTransport(PooledConnectionFactory.class))); + } + + + /** + * Creates a new pooled connection factory. + * + * @param t transport + */ + public PooledConnectionFactory(final Transport t) { + setDefaultConnectionFactory(new DefaultConnectionFactory(t)); + } + + + /** + * Creates a new pooled connection factory. + * + * @param ldapUrl to connect to + */ + public PooledConnectionFactory(final String ldapUrl) { + setDefaultConnectionFactory( + new DefaultConnectionFactory(ldapUrl, TransportFactory.getTransport(PooledConnectionFactory.class))); + } + + + /** + * Creates a new pooled connection factory. + * + * @param ldapUrl to connect to + * @param t transport + */ + public PooledConnectionFactory(final String ldapUrl, final Transport t) { + setDefaultConnectionFactory(new DefaultConnectionFactory(ldapUrl, t)); + } + + + /** + * Creates a new pooled connection factory. + * + * @param cc connection configuration + */ + public PooledConnectionFactory(final ConnectionConfig cc) { + setDefaultConnectionFactory( + new DefaultConnectionFactory(cc, TransportFactory.getTransport(PooledConnectionFactory.class))); + } + + + /** + * Creates a new pooled connection factory. + * + * @param cc connection configuration + * @param t transport + */ + public PooledConnectionFactory(final ConnectionConfig cc, final Transport t) { + setDefaultConnectionFactory(new DefaultConnectionFactory(cc, t)); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a builder for this class. + * + * @param t transport + * @return new builder + */ + public static Builder builder(final Transport t) { + return new Builder(t); + } + + @Override + public ConnectionConfig getConnectionConfig() { + return getDefaultConnectionFactory().getConnectionConfig(); + } + + /** + * Sets the connection config. + * + * @param cc connection config + */ + public void setConnectionConfig(final ConnectionConfig cc) { + getDefaultConnectionFactory().setConnectionConfig(cc); + } + + /** + * Returns the validation exception handler. + * + * @return validation exception handler + */ + public ValidationExceptionHandler getValidationExceptionHandler() { + return validationExceptionHandler; + } + + /** + * Sets the validation exception handler. + * + * @param handler validation exception handler + */ + public void setValidationExceptionHandler(final ValidationExceptionHandler handler) { + validationExceptionHandler = handler; + } + + /** + * Returns the ldap transport. + * + * @return ldap transport + */ + public Transport getTransport() { + return getDefaultConnectionFactory().getTransport(); + } + + @Override + public Connection getConnection() + throws PoolException { + try { + return super.getConnection(); + } catch (ValidationException e) { + if (validationExceptionHandler != null) { + final Connection conn = validationExceptionHandler.apply(e); + if (conn != null) { + return conn; + } + } + throw e; + } + } + + @Override + public void close() { + super.close(); + getDefaultConnectionFactory().close(); + } + + @Override + public String toString() { + return "[" + super.toString() + ", " + "validationExceptionHandler=" + validationExceptionHandler + "]"; + } + + // CheckStyle:OFF + public static class Builder { + + private final PooledConnectionFactory object; + + + protected Builder() { + object = new PooledConnectionFactory(); + } + + + protected Builder(final Transport t) { + object = new PooledConnectionFactory(t); + } + + + public Builder config(final ConnectionConfig cc) { + object.setConnectionConfig(cc); + return this; + } + + + public Builder min(final int size) { + object.setMinPoolSize(size); + return this; + } + + + public Builder max(final int size) { + object.setMaxPoolSize(size); + return this; + } + + + public Builder validateOnCheckIn(final boolean b) { + object.setValidateOnCheckIn(b); + return this; + } + + + public Builder validateOnCheckOut(final boolean b) { + object.setValidateOnCheckOut(b); + return this; + } + + + public Builder validatePeriodically(final boolean b) { + object.setValidatePeriodically(b); + return this; + } + + + public Builder blockWaitTime(final Duration time) { + object.setBlockWaitTime(time); + return this; + } + + + public Builder connectOnCreate(final boolean connect) { + object.setConnectOnCreate(connect); + return this; + } + + + public Builder failFastInitialize(final boolean failFast) { + object.setFailFastInitialize(failFast); + return this; + } + + + public Builder activator(final ConnectionActivator activator) { + object.setActivator(activator); + return this; + } + + + public Builder passivator(final ConnectionPassivator passivator) { + object.setPassivator(passivator); + return this; + } + + + public Builder validator(final ConnectionValidator validator) { + object.setValidator(validator); + return this; + } + + + public Builder validationExceptionHandler(final ValidationExceptionHandler handler) { + object.setValidationExceptionHandler(handler); + return this; + } + + + public Builder pruneStrategy(final PruneStrategy strategy) { + object.setPruneStrategy(strategy); + return this; + } + + + public Builder name(final String name) { + object.setName(name); + return this; + } + + + public PooledConnectionFactory build() { + return object; + } + } + // CheckStyle:ON + + + /** + * Validation exception handler that attempts to retrieve another connection. By default, this implementation makes + * {@link #getMaxPoolSize()} attempts or waits {@link #getBlockWaitTime()} whichever occurs first. + */ + public class RetryValidationExceptionHandler implements ValidationExceptionHandler { + + /** + * Condition on which to continue retry. First parameter is the count, the second is the time the retry started. + */ + private final BiPredicate continueCondition; + + + /** + * Creates a new retry validation exception handler. + */ + public RetryValidationExceptionHandler() { + this((count, time) -> + count <= getMaxPoolSize() + 1 && !getBlockWaitTime().minus(Duration.between(time, Instant.now())).isNegative()); + } + + + /** + * Creates a new retry validation exception handler. + * + * @param condition on which to retry + */ + public RetryValidationExceptionHandler(final BiPredicate condition) { + continueCondition = condition; + } + + + @Override + public Connection apply(final ValidationException e) { + int count = 1; + final Instant time = Instant.now(); + while (continueCondition.test(count++, time)) { + try { + return PooledConnectionFactory.super.getConnection(); + } catch (ValidationException ignored) { + } catch (Exception ex) { + break; + } + } + return null; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/RandomConnectionStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/RandomConnectionStrategy.java new file mode 100644 index 0000000..890c91c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/RandomConnectionStrategy.java @@ -0,0 +1,70 @@ + +package org.xbib.net.ldap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Connection strategy that randomizes the list of configured URLs. A random URL ordering will be created for each + * connection attempt. + * + */ +public class RandomConnectionStrategy extends AbstractConnectionStrategy { + + + @Override + public Iterator iterator() { + if (!isInitialized()) { + throw new IllegalStateException("Strategy is not initialized"); + } + // CheckStyle:AnonInnerLength OFF + return new Iterator<>() { + private final List active = ldapURLSet.getActiveUrls().stream().collect( + Collectors.collectingAndThen( + Collectors.toCollection(ArrayList::new), + list -> { + Collections.shuffle(list); + return list; + })); + private final List inactive = ldapURLSet.getInactiveUrls().stream().collect( + Collectors.collectingAndThen( + Collectors.toCollection(ArrayList::new), + list -> { + Collections.shuffle(list); + return list; + })); + private int i; + + + @Override + public boolean hasNext() { + return i < active.size() + inactive.size(); + } + + + @Override + public LdapURL next() { + final LdapURL url; + if (i < active.size()) { + url = active.get(i); + } else { + url = inactive.get(i - active.size()); + } + i++; + return url; + } + }; + // CheckStyle:AnonInnerLength ON + } + + + @Override + public RandomConnectionStrategy newInstance() { + final RandomConnectionStrategy strategy = new RandomConnectionStrategy(); + strategy.setRetryCondition(getRetryCondition()); + return strategy; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/Request.java b/net-ldap/src/main/java/org/xbib/net/ldap/Request.java new file mode 100644 index 0000000..a2ae073 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/Request.java @@ -0,0 +1,18 @@ + +package org.xbib.net.ldap; + +/** + * LDAP protocol request. + * + */ +public interface Request { + + + /** + * Encode this request as asn.1. + * + * @param id message id of this request + * @return asn.1 encoded request + */ + byte[] encode(int id); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/Result.java b/net-ldap/src/main/java/org/xbib/net/ldap/Result.java new file mode 100644 index 0000000..c32e50f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/Result.java @@ -0,0 +1,74 @@ + +package org.xbib.net.ldap; + +/** + * LDAP protocol result. + * + */ +public interface Result extends Message { + + /** + * Whether to encode control characters. + */ + boolean ENCODE_CNTRL_CHARS = Boolean.parseBoolean( + System.getProperty("org.xbib.net.ldap.response.ENCODE_CNTRL_CHARS", "false")); + + + /** + * Returns the result code. + * + * @return result code + */ + ResultCode getResultCode(); + + + /** + * Returns the matched DN. + * + * @return matched DN + */ + String getMatchedDN(); + + + /** + * Returns the diagnostic message. + * + * @return diagnostic message + */ + String getDiagnosticMessage(); + + + /** + * Returns the referral URLs. + * + * @return referral URLs + */ + String[] getReferralURLs(); + + + /** + * Returns whether the result code in this result is {@link ResultCode#SUCCESS}. + * + * @return whether this result is success + */ + default boolean isSuccess() { + return ResultCode.SUCCESS == getResultCode(); + } + + + /** + * Returns the diagnostic message percent encoded if {@link #ENCODE_CNTRL_CHARS} is true. See {@link + * LdapUtils#percentEncodeControlChars(String)}. + * + * @return encoded message + */ + default String getEncodedDiagnosticMessage() { + if (getDiagnosticMessage() != null && + !"".equals(getDiagnosticMessage()) && + Result.ENCODE_CNTRL_CHARS) { + return LdapUtils.percentEncodeControlChars(getDiagnosticMessage()); + } else { + return getDiagnosticMessage(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ResultCode.java b/net-ldap/src/main/java/org/xbib/net/ldap/ResultCode.java new file mode 100644 index 0000000..56c3ab0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ResultCode.java @@ -0,0 +1,403 @@ + +package org.xbib.net.ldap; + +/** + * Enum to define ldap result codes. + * + */ +public enum ResultCode { + + /** + * success. + */ + SUCCESS(0), + + /** + * operations error. + */ + OPERATIONS_ERROR(1), + + /** + * protocol error. + */ + PROTOCOL_ERROR(2), + + /** + * time limit exceeded. + */ + TIME_LIMIT_EXCEEDED(3), + + /** + * size limit exceeded. + */ + SIZE_LIMIT_EXCEEDED(4), + + /** + * compare false. + */ + COMPARE_FALSE(5), + + /** + * compare true. + */ + COMPARE_TRUE(6), + + /** + * authentication method not supported. + */ + AUTH_METHOD_NOT_SUPPORTED(7), + + /** + * strong authentication required. + */ + STRONG_AUTH_REQUIRED(8), + + /** + * partial results. + */ + PARTIAL_RESULTS(9), + + /** + * referral. + */ + REFERRAL(10), + + /** + * admin limit exceeded. + */ + ADMIN_LIMIT_EXCEEDED(11), + + /** + * unavailable critical extension. + */ + UNAVAILABLE_CRITICAL_EXTENSION(12), + + /** + * confidentiality required. + */ + CONFIDENTIALITY_REQUIRED(13), + + /** + * sasl bind in progress. + */ + SASL_BIND_IN_PROGRESS(14), + + /** + * no such attribute. + */ + NO_SUCH_ATTRIBUTE(16), + + /** + * undefined attribute type. + */ + UNDEFINED_ATTRIBUTE_TYPE(17), + + /** + * inappropriate matching. + */ + INAPPROPRIATE_MATCHING(18), + + /** + * constraint violation. + */ + CONSTRAINT_VIOLATION(19), + + /** + * attribute or value exists. + */ + ATTRIBUTE_OR_VALUE_EXISTS(20), + + /** + * invalid attribute syntax. + */ + INVALID_ATTRIBUTE_SYNTAX(21), + + /** + * no such object. + */ + NO_SUCH_OBJECT(32), + + /** + * alias problem. + */ + ALIAS_PROBLEM(33), + + /** + * invalid dn syntax. + */ + INVALID_DN_SYNTAX(34), + + /** + * is leaf. + */ + IS_LEAF(35), + + /** + * alias dereferencing problem. + */ + ALIAS_DEREFERENCING_PROBLEM(36), + + /** + * inappropriate authentication. + */ + INAPPROPRIATE_AUTHENTICATION(48), + + /** + * invalid credentials. + */ + INVALID_CREDENTIALS(49), + + /** + * insufficient access rights. + */ + INSUFFICIENT_ACCESS_RIGHTS(50), + + /** + * busy. + */ + BUSY(51), + + /** + * unavailable. + */ + UNAVAILABLE(52), + + /** + * unwilling to perform. + */ + UNWILLING_TO_PERFORM(53), + + /** + * loop detect. + */ + LOOP_DETECT(54), + + /** + * sort control missing, See draft-ietf-ldapext-ldapv3-vlv. + */ + SORT_CONTROL_MISSING(60), + + /** + * offset range error, See draft-ietf-ldapext-ldapv3-vlv. + */ + OFFSET_RANGE_ERROR(61), + + /** + * naming violation. + */ + NAMING_VIOLATION(64), + + /** + * object class violation. + */ + OBJECT_CLASS_VIOLATION(65), + + /** + * not allowed on nonleaf. + */ + NOT_ALLOWED_ON_NONLEAF(66), + + /** + * not allowed on rdn. + */ + NOT_ALLOWED_ON_RDN(67), + + /** + * entry already exists. + */ + ENTRY_ALREADY_EXISTS(68), + + /** + * object class mods prohibited. + */ + OBJECT_CLASS_MODS_PROHIBITED(69), + + /** + * affected multiple dsas. + */ + AFFECTS_MULTIPLE_DSAS(71), + + /** + * virtual list view error, See draft-ietf-ldapext-ldapv3-vlv. + */ + VIRTUAL_LIST_VIEW_ERROR(76), + + /** + * other. + */ + OTHER(80), + + /** + * server down. + */ + SERVER_DOWN(81), + + /** + * local error. + */ + LOCAL_ERROR(82), + + /** + * encoding error. + */ + ENCODING_ERROR(83), + + /** + * decoding error. + */ + DECODING_ERROR(84), + + /** + * ldap timeout. + */ + LDAP_TIMEOUT(85), + + /** + * auth unknown. + */ + AUTH_UNKNOWN(86), + + /** + * filter error. + */ + FILTER_ERROR(87), + + /** + * user cancelled. + */ + USER_CANCELLED(88), + + /** + * param error. + */ + PARAM_ERROR(89), + + /** + * no memory. + */ + NO_MEMORY(90), + + /** + * connect error. + */ + CONNECT_ERROR(91), + + /** + * ldap not supported. + */ + LDAP_NOT_SUPPORTED(92), + + /** + * control not found. + */ + CONTROL_NOT_FOUND(93), + + /** + * no results returned. + */ + NO_RESULTS_RETURNED(94), + + /** + * more results to return. + */ + MORE_RESULTS_TO_RETURN(95), + + /** + * client loop. + */ + CLIENT_LOOP(96), + + /** + * referral limit exceeded. + */ + REFERRAL_LIMIT_EXCEEDED(97), + + /** + * invalid response. + */ + INVALID_RESPONSE(100), + + /** + * ambiguous response. + */ + AMBIGUOUS_RESPONSE(101), + + /** + * tls not supported. + */ + TLS_NOT_SUPPORTED(112), + + /** + * operation canceled, See RFC 3909. + */ + CANCELED(118), + + /** + * no such operation, See RFC 3909. + */ + NO_SUCH_OPERATION(119), + + /** + * too late, See RFC 3909. + */ + TOO_LATE(120), + + /** + * cannot cancel, See RFC 3909. + */ + CANNOT_CANCEL(121), + + /** + * assertion failed, See RFC 4528. + */ + ASSERTION_FAILED(122), + + /** + * authorization denied, See RFC 4370. + */ + AUTHORIZATION_DENIED(123), + + /** + * e-syncRefreshRequired, See RFC 4533. + */ + E_SYNC_REFRESH_REQUIRED(4096); + + /** + * underlying error code. + */ + private final int code; + + + /** + * Creates a new result code. + * + * @param i error code + */ + ResultCode(final int i) { + code = i; + } + + /** + * Returns the result code for the supplied integer constant. + * + * @param code to find result code for + * @return result code + */ + public static ResultCode valueOf(final int code) { + for (ResultCode rc : ResultCode.values()) { + if (rc.value() == code) { + return rc; + } + } + return null; + } + + /** + * Returns the result code value. + * + * @return ldap result code + */ + public int value() { + return code; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/RetryMetadata.java b/net-ldap/src/main/java/org/xbib/net/ldap/RetryMetadata.java new file mode 100644 index 0000000..db3a43e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/RetryMetadata.java @@ -0,0 +1,51 @@ + +package org.xbib.net.ldap; + +import java.time.Instant; + +/** + * Contains properties related to retries. + * + */ +public interface RetryMetadata { + + + /** + * Returns the success time. + * + * @return time that the success occurred + */ + Instant getSuccessTime(); + + + /** + * Returns the failure time. + * + * @return time that the failure occurred + */ + Instant getFailureTime(); + + + /** + * Number of attempts for this retry. + * + * @return retry attempts + */ + int getAttempts(); + + + /** + * Records a connection success at the given instant. + * + * @param time Point in time when connection was opened. + */ + void recordSuccess(Instant time); + + + /** + * Records a connection failure at the given instant. + * + * @param time Point in time when connection failed. + */ + void recordFailure(Instant time); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ReturnAttributes.java b/net-ldap/src/main/java/org/xbib/net/ldap/ReturnAttributes.java new file mode 100644 index 0000000..9ac5306 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ReturnAttributes.java @@ -0,0 +1,93 @@ + +package org.xbib.net.ldap; + +import java.util.Arrays; + +/** + * Enum to define constants specific to ldap return attributes. + * + */ +public enum ReturnAttributes { + + /** + * all user and operational attributes. + */ + ALL(new String[]{"*", "+"}), + + /** + * all user attributes. + */ + ALL_USER(new String[]{"*"}), + + /** + * all operational attributes. + */ + ALL_OPERATIONAL(new String[]{"+"}), + + /** + * no attributes. + */ + NONE(new String[]{"1.1"}); + + /** + * underlying value. + */ + private final String[] value; + + + /** + * Creates a new return attributes. + * + * @param s value + */ + ReturnAttributes(final String[] s) { + value = s; + } + + /** + * Parses the supplied return attributes and applies the following convention: + * + *
    + *
  • null == {@link ReturnAttributes#ALL_USER}
  • + *
  • empty == {@link ReturnAttributes#ALL_USER}
  • + *
+ * + * @param attrs to parse + * @return parsed attributes according to convention + */ + public static String[] parse(final String... attrs) { + if (attrs == null || attrs.length == 0) { + return ReturnAttributes.ALL_USER.value(); + } + return attrs; + } + + /** + * Returns the value(s). + * + * @return ldap return attribute + */ + public String[] value() { + return value; + } + + /** + * Returns whether the supplied attributes matches the value of this return attributes. + * + * @param attrs to compare + * @return whether attrs contains only this return attributes + */ + public boolean equalsAttributes(final String... attrs) { + return Arrays.equals(value, attrs); + } + + /** + * Combines the supplied attributes with the value of this return attributes. + * + * @param attrs to combine + * @return combined attributes + */ + public String[] add(final String... attrs) { + return LdapUtils.concatArrays(value, attrs); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/RoundRobinConnectionStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/RoundRobinConnectionStrategy.java new file mode 100644 index 0000000..fd484d4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/RoundRobinConnectionStrategy.java @@ -0,0 +1,71 @@ + +package org.xbib.net.ldap; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +/** + * Connection strategy that reorders its URLs based on the number of times it's been invoked. + * + */ +public class RoundRobinConnectionStrategy extends AbstractConnectionStrategy { + + /** + * Usage counter. + */ + private final AtomicInteger counter = new AtomicInteger(); + + /** + * Custom iterator function. + */ + private final Function, Iterator> iterFunction; + + + /** + * Default constructor. + */ + public RoundRobinConnectionStrategy() { + this(null); + } + + + /** + * Creates a new round robin connection strategy. + * + * @param function that produces a custom iterator + */ + public RoundRobinConnectionStrategy(final Function, Iterator> function) { + iterFunction = function; + } + + + @Override + public synchronized Iterator iterator() { + if (!isInitialized()) { + throw new IllegalStateException("Strategy is not initialized"); + } + final List urls = new ArrayList<>(ldapURLSet.getActiveUrls()); + if (urls.size() > 1) { + for (int i = 0; i < counter.get(); i++) { + urls.add(urls.remove(0)); + } + } + urls.addAll(ldapURLSet.getInactiveUrls()); + counter.incrementAndGet(); + if (iterFunction != null) { + return iterFunction.apply(ldapURLSet.getUrls()); + } + return new DefaultLdapURLIterator(urls); + } + + + @Override + public RoundRobinConnectionStrategy newInstance() { + final RoundRobinConnectionStrategy strategy = new RoundRobinConnectionStrategy(iterFunction); + strategy.setRetryCondition(getRetryCondition()); + return strategy; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SearchConnectionValidator.java b/net-ldap/src/main/java/org/xbib/net/ldap/SearchConnectionValidator.java new file mode 100644 index 0000000..36cdaad --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SearchConnectionValidator.java @@ -0,0 +1,108 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; + +/** + * Validates a connection is healthy by performing a search operation. Unless {@link + * #setValidResultCodes(ResultCode...)} is set, validation is considered successful if the search result contains any + * result code. + * + */ +public class SearchConnectionValidator extends AbstractOperationConnectionValidator { + + + /** + * Creates a new search validator. + */ + public SearchConnectionValidator() { + this(SearchRequest.objectScopeSearchRequest("", ReturnAttributes.NONE.value())); + } + + + /** + * Creates a new search validator. + * + * @param sr to use for searches + */ + public SearchConnectionValidator(final SearchRequest sr) { + this(DEFAULT_VALIDATE_PERIOD, DEFAULT_VALIDATE_TIMEOUT, sr); + } + + + /** + * Creates a new search validator. + * + * @param period execution period + * @param timeout execution timeout + * @param request to use for searches + */ + public SearchConnectionValidator(final Duration period, final Duration timeout, final SearchRequest request) { + setValidatePeriod(period); + setValidateTimeout(timeout); + setRequest(request); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the search request. + * + * @return search request + * @deprecated use {@link AbstractOperationConnectionValidator#getRequest()} + */ + @Deprecated + public SearchRequest getSearchRequest() { + return getRequest(); + } + + /** + * Sets the search request. + * + * @param sr search request + * @deprecated use {@link AbstractOperationConnectionValidator#setRequest(Request)} + */ + @Deprecated + public void setSearchRequest(final SearchRequest sr) { + setRequest(sr); + } + + @Override + protected OperationHandle performOperation(final Connection conn) { + return conn.operation(getRequest()); + } + + @Override + public String toString() { + return "[" + super.toString() + "]"; + } + + /** + * Search validator builder. + */ + public static class Builder extends + AbstractOperationConnectionValidator.AbstractBuilder< + SearchRequest, SearchResponse, SearchConnectionValidator.Builder, SearchConnectionValidator> { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new SearchConnectionValidator()); + } + + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SearchOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/SearchOperation.java new file mode 100644 index 0000000..f9daae7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SearchOperation.java @@ -0,0 +1,796 @@ + +package org.xbib.net.ldap; + +import java.util.Arrays; +import org.xbib.net.ldap.filter.Filter; +import org.xbib.net.ldap.filter.FilterParser; +import org.xbib.net.ldap.handler.LdapEntryHandler; +import org.xbib.net.ldap.handler.SearchReferenceHandler; +import org.xbib.net.ldap.handler.SearchResultHandler; + +/** + * Executes an ldap search operation. + * + */ +public class SearchOperation extends AbstractOperation { + + /** + * Search request to execute. + */ + private SearchRequest request; + + /** + * Filter template. + */ + private FilterTemplate filterTemplate; + + /** + * Functions to handle response entries. + */ + private LdapEntryHandler[] entryHandlers; + + /** + * Functions to handle response references. + */ + private SearchReferenceHandler[] referenceHandlers; + + /** + * Functions to handle response results. + */ + private SearchResultHandler[] searchResultHandlers; + + + /** + * Default constructor. + */ + public SearchOperation() { + setRequest(new SearchRequest()); + } + + + /** + * Creates a new search operation. + * + * @param factory connection factory + */ + public SearchOperation(final ConnectionFactory factory) { + super(factory); + setRequest(new SearchRequest()); + } + + + /** + * Creates a new search operation. + * + * @param factory connection factory + * @param req search request + */ + public SearchOperation(final ConnectionFactory factory, final SearchRequest req) { + super(factory); + setRequest(req); + } + + + /** + * Creates a new search operation. + * + * @param factory connection factory + * @param baseDN to search from + */ + public SearchOperation(final ConnectionFactory factory, final String baseDN) { + super(factory); + setRequest(new SearchRequest(baseDN)); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param factory connection factory + * @param req search request + * @return search operation handle + * @throws LdapException if the connection cannot be opened + */ + public static SearchOperationHandle send(final ConnectionFactory factory, final SearchRequest req) + throws LdapException { + final Connection conn = factory.getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return conn.operation(req).onComplete(conn::close).send(); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param factory connection factory + * @param req search request + * @return search result + * @throws LdapException if the connection cannot be opened + */ + public static SearchResponse execute(final ConnectionFactory factory, final SearchRequest req) + throws LdapException { + try (Connection conn = factory.getConnection()) { + conn.open(); + return conn.operation(req).execute(); + } + } + + /** + * Returns a new search operation with the same properties as the supplied operation. + * + * @param operation to copy + * @return copy of the supplied search operation + */ + public static SearchOperation copy(final SearchOperation operation) { + final SearchOperation op = new SearchOperation(); + op.setRequestHandlers(operation.getRequestHandlers()); + op.setResultHandlers(operation.getResultHandlers()); + op.setControlHandlers(operation.getControlHandlers()); + op.setReferralHandlers(operation.getReferralHandlers()); + op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers()); + op.setExceptionHandler(operation.getExceptionHandler()); + op.setThrowCondition(operation.getThrowCondition()); + op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers()); + op.setConnectionFactory(operation.getConnectionFactory()); + op.setEntryHandlers(operation.getEntryHandlers()); + op.setReferenceHandlers(operation.getReferenceHandlers()); + op.setSearchResultHandlers(operation.getSearchResultHandlers()); + op.setRequest(operation.getRequest()); + op.setTemplate(operation.getTemplate()); + return op; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + public SearchRequest getRequest() { + return request; + } + + public void setRequest(final SearchRequest req) { + request = req; + } + + public FilterTemplate getTemplate() { + return filterTemplate; + } + + public void setTemplate(final FilterTemplate template) { + filterTemplate = template; + } + + public LdapEntryHandler[] getEntryHandlers() { + return entryHandlers; + } + + public void setEntryHandlers(final LdapEntryHandler... handlers) { + entryHandlers = handlers; + } + + public SearchReferenceHandler[] getReferenceHandlers() { + return referenceHandlers; + } + + public void setReferenceHandlers(final SearchReferenceHandler... handlers) { + referenceHandlers = handlers; + } + + public SearchResultHandler[] getSearchResultHandlers() { + return searchResultHandlers; + } + + public void setSearchResultHandlers(final SearchResultHandler... handlers) { + searchResultHandlers = handlers; + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param filter search filter + * @return search operation handle + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send(final String filter) + throws LdapException { + return send(null, FilterParser.parse(filter), null, (LdapEntryHandler[]) null); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param template filter template + * @return search operation handle + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send(final FilterTemplate template) + throws LdapException { + return send(null, FilterParser.parse(template.format()), null, (LdapEntryHandler[]) null); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param filter search filter + * @return search operation handle + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send(final Filter filter) + throws LdapException { + return send(null, filter, null, (LdapEntryHandler[]) null); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param filter search filter + * @param returnAttributes attributes to return + * @return search operation handle + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send(final String filter, final String... returnAttributes) + throws LdapException { + return send(null, FilterParser.parse(filter), returnAttributes, (LdapEntryHandler[]) null); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param template filter template + * @param returnAttributes attributes to return + * @return search operation handle + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send(final FilterTemplate template, final String... returnAttributes) + throws LdapException { + return send(null, FilterParser.parse(template.format()), returnAttributes, (LdapEntryHandler[]) null); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param filter search filter + * @param returnAttributes attributes to return + * @return search operation handle + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send(final Filter filter, final String... returnAttributes) + throws LdapException { + return send(null, filter, returnAttributes, (LdapEntryHandler[]) null); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param filter search filter + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search operation handle + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send( + final String filter, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return send(null, FilterParser.parse(filter), returnAttributes, handlers); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param template filter template + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search operation handle + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send( + final FilterTemplate template, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return send(null, FilterParser.parse(template.format()), returnAttributes, handlers); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param filter search filter + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search operation handle + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send( + final Filter filter, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return send(null, filter, returnAttributes, handlers); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param baseDN base DN + * @param filter search filter + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search operation handle + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send( + final String baseDN, + final String filter, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return send(baseDN, FilterParser.parse(filter), returnAttributes, handlers); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param baseDN base DN + * @param template filter template + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search operation handle + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send( + final String baseDN, + final FilterTemplate template, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return send(baseDN, FilterParser.parse(template.format()), returnAttributes, handlers); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @param baseDN base DN + * @param filter search filter + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search operation handle + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send( + final String baseDN, + final Filter filter, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + final SearchRequest req = configureRequest(baseDN, filter, returnAttributes); + if (handlers != null) { + return configureHandle(conn.operation(req)).onEntry(handlers).onComplete(conn::close).send(); + } else { + return configureHandle(conn.operation(req)).onComplete(conn::close).send(); + } + } + + /** + * Sends the supplied search request. + * + * @param req search request to send + * @return search operation handle + * @throws LdapException if the connection cannot be opened + */ + @Override + public SearchOperationHandle send(final SearchRequest req) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return configureHandle(conn.operation(configureRequest(req))).onComplete(conn::close).send(); + } + + /** + * Sends a search request. See {@link SearchOperationHandle#send()}. + * + * @return search operation handle + * @throws LdapException if the connection cannot be opened + */ + public SearchOperationHandle send() + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + final SearchRequest req = configureRequest(null, null, null); + return configureHandle(conn.operation(req)).onComplete(conn::close).send(); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param filter search filter + * @return search result + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute(final String filter) + throws LdapException { + return execute(FilterParser.parse(filter), null, (LdapEntryHandler[]) null); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param template filter template + * @return search result + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute(final FilterTemplate template) + throws LdapException { + return execute(FilterParser.parse(template.format()), null, (LdapEntryHandler[]) null); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param filter search filter + * @return search result + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute(final Filter filter) + throws LdapException { + return execute(null, filter, null, (LdapEntryHandler[]) null); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param filter search filter + * @param returnAttributes attributes to return + * @return search result + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute(final String filter, final String... returnAttributes) + throws LdapException { + return execute(FilterParser.parse(filter), returnAttributes, (LdapEntryHandler[]) null); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param template filter template + * @param returnAttributes attributes to return + * @return search result + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute(final FilterTemplate template, final String... returnAttributes) + throws LdapException { + return execute(FilterParser.parse(template.format()), returnAttributes, (LdapEntryHandler[]) null); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param filter search filter + * @param returnAttributes attributes to return + * @return search result + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute(final Filter filter, final String... returnAttributes) + throws LdapException { + return execute(null, filter, returnAttributes, (LdapEntryHandler[]) null); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param filter search filter + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search result + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute( + final String filter, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return execute(FilterParser.parse(filter), returnAttributes, handlers); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param template filter template + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search result + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute( + final FilterTemplate template, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return execute(FilterParser.parse(template.format()), returnAttributes, handlers); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param filter search filter + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search result + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute( + final Filter filter, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return execute(null, filter, returnAttributes, handlers); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param baseDN base DN + * @param filter search filter + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search result + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute( + final String baseDN, + final String filter, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return execute(baseDN, FilterParser.parse(filter), returnAttributes, handlers); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param baseDN base DN + * @param template filter template + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search result + * @throws org.xbib.net.ldap.filter.FilterParseException if the filter cannot be parsed + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute( + final String baseDN, + final FilterTemplate template, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + return execute(baseDN, FilterParser.parse(template.format()), returnAttributes, handlers); + } + + /** + * Executes a search request. See {@link SearchOperationHandle#execute()}. + * + * @param baseDN base DN + * @param filter search filter + * @param returnAttributes attributes to return + * @param handlers entry handlers + * @return search result + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute( + final String baseDN, + final Filter filter, + final String[] returnAttributes, + final LdapEntryHandler... handlers) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + final SearchRequest req = configureRequest(baseDN, filter, returnAttributes); + if (handlers != null) { + return configureHandle(conn.operation(req)).onEntry(handlers).execute(); + } else { + return configureHandle(conn.operation(req)).execute(); + } + } + } + + @Override + public SearchResponse execute(final SearchRequest req) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + return configureHandle(conn.operation(configureRequest(req))).execute(); + } + } + + /** + * Executes a search request using {@link #getRequest()}. See {@link SearchOperationHandle#execute()}. + * + * @return search result + * @throws LdapException if the connection cannot be opened + */ + public SearchResponse execute() + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + final SearchRequest req = configureRequest(null, null, null); + return configureHandle(conn.operation(req)).execute(); + } + } + + /** + * Creates a new request from {@link #getRequest()} and applies any non-null supplied properties. + * + * @param baseDN base DN + * @param filter search filter + * @param returnAttributes attributes to return + * @return configured search request + */ + private SearchRequest configureRequest( + final String baseDN, + final Filter filter, + final String[] returnAttributes) { + final SearchRequest.Builder builder = SearchRequest.builder(SearchRequest.copy(getRequest())); + if (baseDN != null) { + builder.dn(baseDN); + } + if (filter != null) { + builder.filter(filter); + } else if (getTemplate() != null) { + builder.filter(getTemplate()); + } + if (returnAttributes != null) { + builder.returnAttributes(returnAttributes); + } + return configureRequest(builder.build()); + } + + /** + * Adds configured functions to the supplied handle. + * + * @param handle to configure + * @return configured handle + */ + protected SearchOperationHandle configureHandle(final SearchOperationHandle handle) { + return handle + .onEntry(getEntryHandlers()) + .onReference(getReferenceHandlers()) + .onResult(getResultHandlers()) + .onControl(getControlHandlers()) + .onReferral(getReferralHandlers()) + .onIntermediate(getIntermediateResponseHandlers()) + .onException(getExceptionHandler()) + .throwIf(getThrowCondition()) + .onUnsolicitedNotification(getUnsolicitedNotificationHandlers()) + .onSearchResult(getSearchResultHandlers()); + } + + @Override + public String toString() { + return super.toString() + ", " + + "request=" + request + ", " + + "template=" + filterTemplate + ", " + + "entryHandlers=" + Arrays.toString(entryHandlers) + ", " + + "referenceHandlers=" + Arrays.toString(referenceHandlers) + ", " + + "searchResultHandlers=" + Arrays.toString(searchResultHandlers); + } + + /** + * Search operation builder. + */ + public static class Builder extends AbstractOperation.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new SearchOperation()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the search request. + * + * @param request to set + * @return this builder + */ + public Builder request(final SearchRequest request) { + object.setRequest(request); + return self(); + } + + + /** + * Sets the filter template. + * + * @param template to set + * @return this builder + */ + public Builder template(final FilterTemplate template) { + object.setTemplate(template); + return self(); + } + + + /** + * Sets the functions to execute when a search result entry is received. + * + * @param handlers to execute on a search result entry + * @return this builder + */ + public Builder onEntry(final LdapEntryHandler... handlers) { + object.setEntryHandlers(handlers); + return self(); + } + + + /** + * Sets the functions to execute when a search result reference is received. + * + * @param handlers to execute on a search result reference + * @return this builder + */ + public Builder onReference(final SearchReferenceHandler... handlers) { + object.setReferenceHandlers(handlers); + return self(); + } + + + /** + * Sets the functions to execute when a search result is complete. + * + * @param handlers to execute on a search result + * @return this builder + */ + public Builder onSearchResult(final SearchResultHandler... handlers) { + object.setSearchResultHandlers(handlers); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SearchOperationHandle.java b/net-ldap/src/main/java/org/xbib/net/ldap/SearchOperationHandle.java new file mode 100644 index 0000000..d29e5d1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SearchOperationHandle.java @@ -0,0 +1,95 @@ + +package org.xbib.net.ldap; + +import org.xbib.net.ldap.handler.CompleteHandler; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.LdapEntryHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.SearchReferenceHandler; +import org.xbib.net.ldap.handler.SearchResultHandler; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Handle that notifies on the components of a search request. + * + */ +public interface SearchOperationHandle extends OperationHandle { + + + @Override + SearchOperationHandle send(); + + + @Override + SearchResponse await() throws LdapException; + + + @Override + default SearchResponse execute() + throws LdapException { + return send().await(); + } + + + @Override + SearchOperationHandle onResult(ResultHandler... function); + + + @Override + SearchOperationHandle onControl(ResponseControlHandler... function); + + + @Override + SearchOperationHandle onReferral(ReferralHandler... function); + + + @Override + SearchOperationHandle onIntermediate(IntermediateResponseHandler... function); + + + @Override + SearchOperationHandle onUnsolicitedNotification(UnsolicitedNotificationHandler... function); + + + @Override + SearchOperationHandle onException(ExceptionHandler function); + + + @Override + SearchOperationHandle onComplete(CompleteHandler function); + + + @Override + SearchOperationHandle throwIf(ResultPredicate function); + + + /** + * Sets the functions to execute when a search result entry is received. + * + * @param function to execute on a search result entry + * @return this handle + */ + SearchOperationHandle onEntry(LdapEntryHandler... function); + + + /** + * Sets the functions to execute when a search result reference is received. + * + * @param function to execute on a search result reference + * @return this handle + */ + SearchOperationHandle onReference(SearchReferenceHandler... function); + + + /** + * Sets the functions to execute when a search result is complete. + * + * @param function to execute on a search result + * @return this handle + */ + SearchOperationHandle onSearchResult(SearchResultHandler... function); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SearchRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/SearchRequest.java new file mode 100644 index 0000000..fca9404 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SearchRequest.java @@ -0,0 +1,821 @@ + +package org.xbib.net.ldap; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; +import org.xbib.net.ldap.filter.Filter; +import org.xbib.net.ldap.filter.FilterParseException; +import org.xbib.net.ldap.filter.FilterParser; +import org.xbib.net.ldap.filter.PresenceFilter; + +/** + * LDAP search request defined as: + * + *
+ * SearchRequest ::= [APPLICATION 3] SEQUENCE {
+ * baseObject      LDAPDN,
+ * scope           ENUMERATED {
+ * baseObject              (0),
+ * singleLevel             (1),
+ * wholeSubtree            (2),
+ * ...  },
+ * aliases    ENUMERATED {
+ * neverDerefAliases       (0),
+ * derefInSearching        (1),
+ * derefFindingBaseObj     (2),
+ * derefAlways             (3) },
+ * sizeLimit       INTEGER (0 ..  maxInt),
+ * timeLimit       INTEGER (0 ..  maxInt),
+ * typesOnly       BOOLEAN,
+ * filter          Filter,
+ * attributes      AttributeSelection }
+ * 
+ * + */ +public class SearchRequest extends AbstractRequestMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 3; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 307; + + /** + * Base DN. + */ + private String baseDn = ""; + + /** + * Search scope. + */ + private SearchScope searchScope = SearchScope.SUBTREE; + + /** + * Deref aliases. + */ + private DerefAliases derefAliases = DerefAliases.NEVER; + + /** + * Size limit. + */ + private int sizeLimit; + + /** + * Time limit. + */ + private Duration timeLimit = Duration.ZERO; + + /** + * Types only. + */ + private boolean typesOnly; + + /** + * Search filter. + */ + private Filter searchFilter; + + /** + * Return attributes. + */ + private String[] returnAttributes = ReturnAttributes.ALL_USER.value(); + + /** + * Binary attribute names used to convey attributes that should be treated as binary when a response is received for + * this request. This property is not part of the request specification. See {@link LdapAttribute#isBinary()}. + */ + private String[] binaryAttributes; + + + /** + * Default constructor. + */ + public SearchRequest() { + } + + + /** + * Creates a new search request. + * + * @param dn base DN + */ + public SearchRequest(final String dn) { + setBaseDn(dn); + } + + + /** + * Creates a new search request. + * + * @param dn base DN + * @param filter search filter + */ + public SearchRequest( + final String dn, + final String filter) { + setBaseDn(dn); + setFilter(filter); + } + + + /** + * Creates a new search request. + * + * @param dn base DN + * @param filter search filter + * @param attributes return attributes + */ + public SearchRequest( + final String dn, + final String filter, + final String... attributes) { + setBaseDn(dn); + setFilter(filter); + setReturnAttributes(attributes); + } + + + /** + * Creates a new search request. + * + * @param dn base DN + * @param template filter template + * @param attributes return attributes + */ + public SearchRequest( + final String dn, + final FilterTemplate template, + final String... attributes) { + setBaseDn(dn); + setFilter(template); + setReturnAttributes(attributes); + } + + + /** + * Creates a new search request. + * + * @param dn base DN + * @param filter search filter + * @param attributes return attributes + */ + public SearchRequest( + final String dn, + final Filter filter, + final String... attributes) { + setBaseDn(dn); + setFilter(filter); + setReturnAttributes(attributes); + } + + + /** + * Creates a new search request. + * + * @param dn base DN + * @param scope search scope + * @param aliases deref aliases + * @param size size limit + * @param time time limit + * @param types types only + * @param filter search filter + * @param attributes return attributes + */ + // CheckStyle:ParameterNumber OFF + public SearchRequest( + final String dn, + final SearchScope scope, + final DerefAliases aliases, + final int size, + final Duration time, + final boolean types, + final Filter filter, + final String... attributes) { + setBaseDn(dn); + setSearchScope(scope); + setDerefAliases(aliases); + setSizeLimit(size); + setTimeLimit(time); + setTypesOnly(types); + setFilter(filter); + setReturnAttributes(attributes); + } + // CheckStyle:ParameterNumber ON + + /** + * Returns a search request initialized for use with an object level search scope. + * + * @param dn of an ldap entry + * @return search request + */ + public static SearchRequest objectScopeSearchRequest(final String dn) { + return objectScopeSearchRequest(dn, null); + } + + /** + * Returns a search request initialized for use with an object level search scope. + * + * @param dn of an ldap entry + * @param attrs to return + * @return search request + */ + public static SearchRequest objectScopeSearchRequest(final String dn, final String[] attrs) { + return objectScopeSearchRequest(dn, attrs, new PresenceFilter("objectClass")); + } + + /** + * Returns a search request initialized for use with an object level search scope. + * + * @param dn of an ldap entry + * @param attrs to return + * @param filter to execute on the ldap entry + * @return search request + * @throws IllegalArgumentException if the filter cannot be parsed + */ + public static SearchRequest objectScopeSearchRequest( + final String dn, + final String[] attrs, + final String filter) { + try { + return objectScopeSearchRequest(dn, attrs, FilterParser.parse(filter)); + } catch (FilterParseException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Returns a search request initialized for use with an object level search scope. + * + * @param dn of an ldap entry + * @param attrs to return + * @param template to execute on the ldap entry + * @return search request + * @throws IllegalArgumentException if the filter cannot be parsed + */ + public static SearchRequest objectScopeSearchRequest( + final String dn, + final String[] attrs, + final FilterTemplate template) { + try { + return objectScopeSearchRequest(dn, attrs, FilterParser.parse(template.format())); + } catch (FilterParseException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Returns a search request initialized for use with an object level search scope. + * + * @param dn of an ldap entry + * @param attrs to return + * @param filter to execute on the ldap entry + * @return search request + */ + public static SearchRequest objectScopeSearchRequest( + final String dn, + final String[] attrs, + final Filter filter) { + final SearchRequest request = new SearchRequest(); + request.setBaseDn(dn); + request.setFilter(filter); + request.setReturnAttributes(attrs); + request.setSearchScope(SearchScope.OBJECT); + return request; + } + + /** + * Returns a new search request with the same properties as the supplied request. + * + * @param request to copy + * @return copy of the supplied search request + */ + public static SearchRequest copy(final SearchRequest request) { + final SearchRequest sr = new SearchRequest(); + sr.setBaseDn(request.getBaseDn()); + sr.setSearchScope(request.getSearchScope()); + sr.setDerefAliases(request.getDerefAliases()); + sr.setSizeLimit(request.getSizeLimit()); + sr.setTimeLimit(request.getTimeLimit()); + sr.setTypesOnly(request.isTypesOnly()); + sr.setFilter(request.getFilter()); + sr.setReturnAttributes(request.getReturnAttributes()); + sr.setBinaryAttributes(request.getBinaryAttributes()); + sr.setControls(request.getControls()); + return sr; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a builder for this class. + * + * @param request search request to initialize the builder with + * @return new builder + */ + public static Builder builder(final SearchRequest request) { + return new Builder(request); + } + + /** + * Returns the base DN. + * + * @return base DN + */ + public String getBaseDn() { + return baseDn; + } + + /** + * Sets the base DN. + * + * @param dn base DN + */ + public void setBaseDn(final String dn) { + baseDn = dn; + } + + /** + * Gets the search scope. + * + * @return search scope + */ + public SearchScope getSearchScope() { + return searchScope; + } + + /** + * Sets the search scope. + * + * @param scope search scope + */ + public void setSearchScope(final SearchScope scope) { + if (scope == null) { + throw new IllegalArgumentException("Scope cannot be null"); + } + searchScope = scope; + } + + /** + * Returns how to dereference aliases. + * + * @return how to dereference aliases + */ + public DerefAliases getDerefAliases() { + return derefAliases; + } + + /** + * Sets how to dereference aliases. + * + * @param aliases how to dereference aliases + */ + public void setDerefAliases(final DerefAliases aliases) { + if (aliases == null) { + throw new IllegalArgumentException("Aliases cannot be null"); + } + derefAliases = aliases; + } + + /** + * Returns the size limit. + * + * @return size limit + */ + public int getSizeLimit() { + return sizeLimit; + } + + /** + * Sets the size limit. + * + * @param limit size limit + * @throws IllegalArgumentException if limit is negative + */ + public void setSizeLimit(final int limit) { + if (limit < 0) { + throw new IllegalArgumentException("Size limit cannot be negative"); + } + sizeLimit = limit; + } + + /** + * Returns the time limit. + * + * @return time limit + */ + public Duration getTimeLimit() { + return timeLimit; + } + + /** + * Sets the time limit. + * + * @param limit time limit + * @throws IllegalArgumentException if limit is null or negative + */ + public void setTimeLimit(final Duration limit) { + if (limit == null || limit.isNegative()) { + throw new IllegalArgumentException("Time limit cannot be null or negative"); + } + timeLimit = limit; + } + + /** + * Returns whether to return only attribute types. + * + * @return whether to return only attribute types + */ + public boolean isTypesOnly() { + return typesOnly; + } + + /** + * Sets whether to return only attribute types. + * + * @param types whether to return only attribute types + */ + public void setTypesOnly(final boolean types) { + typesOnly = types; + } + + /** + * Returns the search filter. + * + * @return search filter + */ + public Filter getFilter() { + return searchFilter; + } + + /** + * Sets the search filter. + * + * @param filter search filter + */ + public void setFilter(final Filter filter) { + searchFilter = filter; + } + + /** + * Sets the search filter. See {@link FilterParser#parse(String)}. + * + * @param filter search filter + * @throws IllegalArgumentException if the filter cannot be parsed + */ + public void setFilter(final String filter) { + try { + searchFilter = FilterParser.parse(filter); + } catch (FilterParseException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Sets the search filter. See {@link FilterTemplate} and {@link FilterParser#parse(String)}. + * + * @param template filter template + * @throws IllegalArgumentException if the filter cannot be parsed + */ + public void setFilter(final FilterTemplate template) { + try { + searchFilter = FilterParser.parse(template.format()); + } catch (FilterParseException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Returns the search return attributes. + * + * @return search return attributes + */ + public String[] getReturnAttributes() { + return returnAttributes; + } + + /** + * Sets the search return attributes. + * + * @param attributes search return attributes + */ + public void setReturnAttributes(final String... attributes) { + returnAttributes = ReturnAttributes.parse(attributes); + } + + /** + * Returns names of binary attributes. + * + * @return binary attribute names + */ + public String[] getBinaryAttributes() { + return binaryAttributes; + } + + /** + * Sets names of binary attributes. + * + * @param attrs binary attribute names + */ + public void setBinaryAttributes(final String... attrs) { + binaryAttributes = attrs; + } + + /** + * Invokes {@link LdapAttribute#configureBinary(String...)} for each attribute in the supplied entry using {@link + * #binaryAttributes}. + * + * @param entry to configure binary attributes for + */ + public void configureBinaryAttributes(final LdapEntry entry) { + if (binaryAttributes != null && binaryAttributes.length > 0) { + for (LdapAttribute a : entry.getAttributes()) { + a.configureBinary(binaryAttributes); + } + } + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + if (baseDn == null) { + throw new NullPointerException("No baseDn defined in " + this); + } + if (searchFilter == null) { + throw new NullPointerException("No search filter defined in " + this); + } + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new OctetStringType(baseDn), + new IntegerType(UniversalDERTag.ENUM, searchScope.ordinal()), + new IntegerType(UniversalDERTag.ENUM, derefAliases.ordinal()), + new IntegerType(sizeLimit), + new IntegerType((int) timeLimit.getSeconds()), + new BooleanType(typesOnly), + searchFilter.getEncoder(), + new ConstructedDEREncoder( + UniversalDERTag.SEQ, + Stream.of(returnAttributes).map(OctetStringType::new).toArray(DEREncoder[]::new))), + }; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SearchRequest) { + final SearchRequest v = (SearchRequest) o; + return LdapUtils.areEqual(baseDn, v.baseDn) && + LdapUtils.areEqual(searchScope, v.searchScope) && + LdapUtils.areEqual(derefAliases, v.derefAliases) && + LdapUtils.areEqual(sizeLimit, v.sizeLimit) && + LdapUtils.areEqual(timeLimit, v.timeLimit) && + LdapUtils.areEqual(typesOnly, v.typesOnly) && + LdapUtils.areEqual(searchFilter, v.searchFilter) && + LdapUtils.areEqual(returnAttributes, v.returnAttributes) && + LdapUtils.areEqual(binaryAttributes, v.binaryAttributes) && + LdapUtils.areEqual(getControls(), v.getControls()) && + LdapUtils.areEqual(getResponseTimeout(), v.getResponseTimeout()); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + baseDn, + searchScope, + derefAliases, + sizeLimit, + timeLimit, + typesOnly, + searchFilter, + returnAttributes, + binaryAttributes, + getControls(), + getResponseTimeout()); + } + + @Override + public String toString() { + return super.toString() + ", " + + "dn=" + baseDn + ", " + + "scope=" + searchScope + ", " + + "aliases=" + derefAliases + ", " + + "sizeLimit=" + sizeLimit + ", " + + "timeLimit=" + timeLimit + ", " + + "typesOnly=" + typesOnly + ", " + + "filter=" + searchFilter + ", " + + "returnAttributes=" + Arrays.toString(returnAttributes) + ", " + + "binaryAttributes=" + Arrays.toString(binaryAttributes); + } + + /** + * Search request builder. + */ + public static class Builder extends AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new SearchRequest()); + } + + + /** + * Creates a new builder. + * + * @param req search request to build + */ + protected Builder(final SearchRequest req) { + super(req); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the base DN. + * + * @param dn base DN + * @return this builder + */ + public Builder dn(final String dn) { + object.setBaseDn(dn); + return self(); + } + + + /** + * Sets the search scope. + * + * @param scope search scope + * @return this builder + */ + public Builder scope(final SearchScope scope) { + object.setSearchScope(scope); + return self(); + } + + + /** + * Sets the deref aliases flag. + * + * @param aliases deref aliases + * @return this builder + */ + public Builder aliases(final DerefAliases aliases) { + object.setDerefAliases(aliases); + return self(); + } + + + /** + * Sets the size limit. + * + * @param size size limit + * @return this builder + */ + public Builder sizeLimit(final int size) { + object.setSizeLimit(size); + return self(); + } + + + /** + * Sets the time limit. + * + * @param time time limit + * @return this builder + */ + public Builder timeLimit(final Duration time) { + object.setTimeLimit(time); + return self(); + } + + + /** + * Sets the types only. + * + * @param types whether to return only types + * @return this builder + */ + public Builder typesOnly(final boolean types) { + object.setTypesOnly(types); + return self(); + } + + + /** + * Sets the search filter. + * + * @param filter search filter + * @return this builder + */ + public Builder filter(final Filter filter) { + object.setFilter(filter); + return self(); + } + + + /** + * Sets the search filter. + * + * @param filter search filter + * @return this builder + */ + public Builder filter(final String filter) { + object.setFilter(filter); + return self(); + } + + + /** + * Sets the search filter. + * + * @param template filter template + * @return this builder + */ + public Builder filter(final FilterTemplate template) { + object.setFilter(template); + return self(); + } + + + /** + * Sets the return attributes. + * + * @param attributes return attributes + * @return this builder + */ + public Builder returnAttributes(final String... attributes) { + object.setReturnAttributes(attributes); + return self(); + } + + + /** + * Sets the return attributes. + * + * @param attributes return attributes + * @return this builder + */ + public Builder returnAttributes(final Collection attributes) { + object.setReturnAttributes(attributes.toArray(String[]::new)); + return self(); + } + + + /** + * Sets the binary attributes. + * + * @param attributes binary attributes + * @return this builder + */ + public Builder binaryAttributes(final String... attributes) { + object.setBinaryAttributes(attributes); + return self(); + } + + + /** + * Sets the binary attributes. + * + * @param attributes binary attributes + * @return this builder + */ + public Builder binaryAttributes(final Collection attributes) { + object.setBinaryAttributes(attributes.toArray(String[]::new)); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SearchResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/SearchResponse.java new file mode 100644 index 0000000..33530a6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SearchResponse.java @@ -0,0 +1,378 @@ + +package org.xbib.net.ldap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.net.ldap.dn.Dn; + +/** + * Response that encapsulates the result elements of a search request. This class formally decodes the SearchResultDone + * LDAP message defined as: + * + *
+ * SearchResultDone ::= [APPLICATION 5] LDAPResult
+ * 
+ * + */ +public class SearchResponse extends AbstractResult { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 5; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10301; + + /** + * DER path to result code. + */ + private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(5)/ENUM[0]"); + + /** + * DER path to matched DN. + */ + private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(5)/OCTSTR[1]"); + + /** + * DER path to diagnostic message. + */ + private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(5)/OCTSTR[2]"); + + /** + * DER path to referral. + */ + private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(5)/CTX(3)/OCTSTR[0]"); + + /** + * Entries contained in this result. + */ + private final List resultEntries = new ArrayList<>(); + + /** + * Search result references contained in this result. + */ + private final List resultReferences = new ArrayList<>(); + + + /** + * Default constructor. + */ + public SearchResponse() { + } + + + /** + * Creates a new search result done. + * + * @param buffer to decode + */ + public SearchResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this)); + parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this)); + parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this)); + parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Returns a new response whose entries are sorted naturally by DN. Each attribute and each attribute value are also + * sorted. See {@link LdapEntry#sort(LdapEntry)} and {@link LdapAttribute#sort(LdapAttribute)}. + * + * @param sr response to sort + * @return sorted response + */ + public static SearchResponse sort(final SearchResponse sr) { + final SearchResponse sorted = new SearchResponse(); + sorted.copyValues(sr); + sorted.addEntries(sr.getEntries().stream() + .map(LdapEntry::sort) + .sorted(Comparator.comparing(LdapEntry::getDn, String.CASE_INSENSITIVE_ORDER)) + .collect(Collectors.toCollection(LinkedHashSet::new))); + sorted.addReferences(sr.getReferences().stream() + .map(SearchResultReference::sort) + .sorted(Comparator.comparing(SearchResultReference::hashCode)) + .collect(Collectors.toCollection(LinkedHashSet::new))); + return sorted; + } + + /** + * Merges the entries in the supplied result into a single entry. This method always returns a search result of size + * zero or one. + * + * @param result search result containing entries to merge + * @return search result containing a single merged entry + */ + public static SearchResponse merge(final SearchResponse result) { + LdapEntry mergedEntry = null; + if (result != null) { + for (LdapEntry entry : result.getEntries()) { + if (mergedEntry == null) { + mergedEntry = entry; + } else { + for (LdapAttribute la : entry.getAttributes()) { + final LdapAttribute oldAttr = mergedEntry.getAttribute(la.getName()); + if (oldAttr == null) { + mergedEntry.addAttributes(la); + } else { + if (oldAttr.isBinary()) { + oldAttr.addBinaryValues(la.getBinaryValues()); + } else { + oldAttr.addStringValues(la.getStringValues()); + } + } + } + } + } + } + return mergedEntry != null ? + builder() + .entry( + LdapEntry.builder().dn(mergedEntry.getDn()).attributes(mergedEntry.getAttributes()).build()) + .build() : + new SearchResponse(); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Copies the values of the supplied search result done to this synthetic result. + * + * @param result of values to copy + */ + public void initialize(final SearchResponse result) { + copyValues(result); + } + + /** + * Returns a collection of ldap entry. + * + * @return collection of ldap entry + */ + public Collection getEntries() { + return resultEntries; + } + + /** + * Returns a single entry of this result. If multiple entries exist the first entry returned by the underlying + * iterator is used. If no entries exist null is returned. + * + * @return search result entry + */ + public LdapEntry getEntry() { + if (resultEntries.isEmpty()) { + return null; + } + return resultEntries.iterator().next(); + } + + /** + * Returns the ldap entry in this result with the supplied DN. DN comparison is attempted with a normalized string + * comparison. + * + * @param dn of the entry to return + * @return search result entry or null if no entry matching the dn could be found + * @throws IllegalArgumentException if the supplied dn cannot be normalized + */ + public LdapEntry getEntry(final String dn) { + final String compareDn = new Dn(dn).format(); + return resultEntries.stream().filter(e -> compareDn.equals(e.getNormalizedDn())).findAny().orElse(null); + } + + /** + * Returns the entry DNs in this result. + * + * @return string array of entry DNs + */ + public Set getEntryDns() { + return resultEntries.stream().map(LdapEntry::getDn).collect(Collectors.toUnmodifiableSet()); + } + + /** + * Adds an entry to this search result. + * + * @param entry entry to add + */ + public void addEntries(final LdapEntry... entry) { + Stream.of(entry).forEach(resultEntries::add); + } + + /** + * Adds entry(s) to this search result. + * + * @param entries collection of entries to add + */ + public void addEntries(final Collection entries) { + entries.forEach(resultEntries::add); + } + + /** + * Returns the number of entries in this search result. + * + * @return number of entries in this search result + */ + public int entrySize() { + return resultEntries.size(); + } + + /** + * Returns a collection of ldap entry. + * + * @return collection of ldap entry + */ + public Collection getReferences() { + return resultReferences; + } + + /** + * Returns a single search reference of this result. If multiple references exist the first references returned by the + * underlying iterator is used. If no references exist null is returned. + * + * @return search result references + */ + public SearchResultReference getReference() { + if (resultReferences.isEmpty()) { + return null; + } + return resultReferences.iterator().next(); + } + + /** + * Adds a reference to this search result. + * + * @param reference reference to add + */ + public void addReferences(final SearchResultReference... reference) { + Collections.addAll(resultReferences, reference); + } + + /** + * Adds references(s) to this search result. + * + * @param references collection of references to add + */ + public void addReferences(final Collection references) { + resultReferences.addAll(references); + } + + /** + * Returns the number of references in this search result. + * + * @return number of references in this search result + */ + public int referenceSize() { + return resultReferences.size(); + } + + /** + * Returns a portion of this result between the specified fromIndex, inclusive, and toIndex, exclusive. If fromIndex + * and toIndex are equal, the return result is empty. The result of this method is undefined for unordered results. + * + * @param fromIndex low endpoint of the search result (inclusive) + * @param toIndex high endpoint of the search result (exclusive) + * @return portion of this search result + * @throws IndexOutOfBoundsException for illegal index values + */ + public SearchResponse subResult(final int fromIndex, final int toIndex) { + if (fromIndex < 0 || toIndex > resultEntries.size() || fromIndex > toIndex) { + throw new IndexOutOfBoundsException("Illegal index value"); + } + + final SearchResponse result = new SearchResponse(); + if (resultEntries.isEmpty() || fromIndex == toIndex) { + return result; + } + + int i = 0; + for (LdapEntry e : resultEntries) { + if (i >= fromIndex && i < toIndex) { + result.addEntries(e); + } + i++; + } + return result; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SearchResponse && super.equals(o)) { + final SearchResponse v = (SearchResponse) o; + return LdapUtils.areEqual(resultEntries, v.resultEntries) && + LdapUtils.areEqual(resultReferences, v.resultReferences); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs(), + resultEntries, + resultReferences); + } + + @Override + public String toString() { + return super.toString() + ", entries=" + resultEntries + ", references=" + resultReferences; + } + + // CheckStyle:OFF + public static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new SearchResponse()); + } + + + @Override + protected Builder self() { + return this; + } + + + public Builder entry(final LdapEntry... e) { + object.addEntries(e); + return this; + } + + + public Builder reference(final SearchResultReference... r) { + object.addReferences(r); + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SearchResultReference.java b/net-ldap/src/main/java/org/xbib/net/ldap/SearchResultReference.java new file mode 100644 index 0000000..51bd855 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SearchResultReference.java @@ -0,0 +1,181 @@ + +package org.xbib.net.ldap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP search result entry defined as: + * + *
+ * SearchResultReference ::= [APPLICATION 19] SEQUENCE
+ * SIZE (1..MAX) OF uri URI
+ * 
+ * + */ +public class SearchResultReference extends AbstractMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 19; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10313; + + /** + * DER path to referral URI. + */ + private static final DERPath REFERRAL_URI_PATH = new DERPath("/SEQ/APP(19)/OCTSTR"); + + /** + * List of references. + */ + private final List references = new ArrayList<>(); + + + /** + * Default constructor. + */ + public SearchResultReference() { + } + + + /** + * Creates a new search result reference. + * + * @param buffer to decode + */ + public SearchResultReference(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(REFERRAL_URI_PATH, new ReferralUriHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Returns a new reference whose URIs are sorted naturally. + * + * @param ref reference to sort + * @return sorted reference + */ + public static SearchResultReference sort(final SearchResultReference ref) { + final SearchResultReference sorted = new SearchResultReference(); + sorted.copyValues(ref); + sorted.addUris(Stream.of(ref.getUris()).sorted().collect(Collectors.toList())); + return sorted; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + public String[] getUris() { + return references.toArray(new String[0]); + } + + /** + * Adds a new URI to this reference. + * + * @param uri to add + */ + public void addUris(final String... uri) { + Collections.addAll(references, uri); + } + + /** + * Adds a new URI to this reference. + * + * @param uris to add + */ + public void addUris(final Collection uris) { + references.addAll(uris); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SearchResultReference v && super.equals(o)) { + return LdapUtils.areEqual(references, v.references); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + references); + } + + @Override + public String toString() { + return super.toString() + ", " + "URIs=" + references; + } + + /** + * Parse handler implementation for the referral URL. + */ + protected static class ReferralUriHandler extends AbstractParseHandler { + + + /** + * Creates a new referral URI handler. + * + * @param response to configure + */ + ReferralUriHandler(final SearchResultReference response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().addUris(OctetStringType.decode(encoded)); + } + } + + // CheckStyle:OFF + public static class Builder extends AbstractMessage.AbstractBuilder { + + + protected Builder() { + super(new SearchResultReference()); + } + + + @Override + protected Builder self() { + return this; + } + + + public Builder uris(final String... uri) { + object.addUris(uri); + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SearchScope.java b/net-ldap/src/main/java/org/xbib/net/ldap/SearchScope.java new file mode 100644 index 0000000..98c4067 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SearchScope.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap; + +/** + * Enum to define the type of search scope. + * + *
+ * scope           ENUMERATED {
+ * baseObject              (0),
+ * singleLevel             (1),
+ * wholeSubtree            (2),
+ * subordinateSubtree      (3),
+ * ...  }
+ * 
+ * + */ +public enum SearchScope { + + /** + * base object search. + */ + OBJECT, + + /** + * single level search. + */ + ONELEVEL, + + /** + * whole subtree search. + */ + SUBTREE, + + /** + * subordinate subtree search. See draft-sermersheim-ldap-subordinate-scope. + */ + SUBORDINATE +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SimpleBindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/SimpleBindRequest.java new file mode 100644 index 0000000..dd01d4a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SimpleBindRequest.java @@ -0,0 +1,166 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextType; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP simple bind request. + * + */ +public class SimpleBindRequest extends AbstractRequestMessage implements BindRequest { + + /** + * LDAP DN to bind as. + */ + private String ldapDN; + + /** + * Password for the LDAP DN. + */ + private String password; + + + /** + * Default constructor. + */ + private SimpleBindRequest() { + } + + + /** + * Creates a new simple bind request. + * + * @param name to bind as + * @param pass to bind with + */ + public SimpleBindRequest(final String name, final String pass) { + setLdapDN(name); + setPassword(pass); + } + + + /** + * Creates a new simple bind request. + * + * @param name to bind as + * @param cred to bind with + */ + public SimpleBindRequest(final String name, final Credential cred) { + setLdapDN(name); + setPassword(cred.getString()); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Sets the LDAP DN. + * + * @param name LDAP DN to set + * @throws IllegalArgumentException if name is null or empty + */ + protected void setLdapDN(final String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("bind request name cannot be null or empty"); + } + ldapDN = name; + } + + /** + * Sets the password. + * + * @param pass password to set + * @throws IllegalArgumentException if pass is null or empty + */ + protected void setPassword(final String pass) { + if (pass == null || pass.isEmpty()) { + throw new IllegalArgumentException("bind request password cannot be null or empty"); + } + password = pass; + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new IntegerType(VERSION), + new OctetStringType(ldapDN), + new ContextType(0, LdapUtils.utf8Encode(password, false))), + }; + } + + @Override + public String toString() { + return super.toString() + ", " + "dn=" + ldapDN; + } + + /** + * Simple bind request builder. + */ + public static class Builder extends + AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new SimpleBindRequest()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the bind DN. + * + * @param dn ldap DN + * @return this builder + */ + public Builder dn(final String dn) { + object.setLdapDN(dn); + return self(); + } + + + /** + * Sets the bind password. + * + * @param password associated with the DN + * @return this builder + */ + public Builder password(final String password) { + object.setPassword(password); + return self(); + } + + + /** + * Sets the bind password. + * + * @param credential associated with the DN + * @return this builder + */ + public Builder password(final Credential credential) { + object.setPassword(credential.getString()); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/SingleConnectionFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/SingleConnectionFactory.java new file mode 100644 index 0000000..0bb2e5c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/SingleConnectionFactory.java @@ -0,0 +1,538 @@ + +package org.xbib.net.ldap; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import org.xbib.net.ldap.transport.Transport; +import org.xbib.net.ldap.transport.TransportFactory; + +/** + * Creates a single connection which is proxied for LDAP operations. + * + */ +public class SingleConnectionFactory extends DefaultConnectionFactory { + + /** + * The proxy used by this factory. + */ + private ConnectionProxy proxy; + + /** + * Whether {@link #initialize()} has been successfully invoked. + */ + private boolean initialized; + + /** + * Whether {@link #initialize()} should throw if the connection cannot be opened. + */ + private boolean failFastInitialize = true; + + /** + * Whether {@link #initialize()} should occur on a separate thread. + */ + private boolean nonBlockingInitialize; + + /** + * To run when a connection is opened. + */ + private Function onOpen; + + /** + * To run when a connection is closed. + */ + private Function onClose; + + /** + * For validating the connection. + */ + private ConnectionValidator validator; + + /** + * Executor for scheduling factory tasks. + */ + private ExecutorService factoryExecutor; + + + /** + * Default constructor. + */ + public SingleConnectionFactory() { + super(TransportFactory.getTransport(SingleConnectionFactory.class)); + } + + + /** + * Creates a new single connection factory. + * + * @param t transport + */ + public SingleConnectionFactory(final Transport t) { + super(t); + } + + + /** + * Creates a new single connection factory. + * + * @param ldapUrl to connect to + */ + public SingleConnectionFactory(final String ldapUrl) { + super(ldapUrl, TransportFactory.getTransport(SingleConnectionFactory.class)); + } + + + /** + * Creates a new single connection factory. + * + * @param ldapUrl to connect to + * @param t transport + */ + public SingleConnectionFactory(final String ldapUrl, final Transport t) { + super(ldapUrl, t); + } + + + /** + * Creates a new single connection factory. + * + * @param cc connection configuration + */ + public SingleConnectionFactory(final ConnectionConfig cc) { + super(cc, TransportFactory.getTransport(SingleConnectionFactory.class)); + } + + + /** + * Creates a new single connection factory. + * + * @param cc connection configuration + * @param t transport + */ + public SingleConnectionFactory(final ConnectionConfig cc, final Transport t) { + super(cc, t); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a builder for this class. + * + * @param t transport + * @return new builder + */ + public static Builder builder(final Transport t) { + return new Builder(t); + } + + /** + * Returns whether {@link #initialize()} should throw if the connection cannot be opened. + * + * @return whether {@link #initialize()} should throw + */ + public boolean getFailFastInitialize() { + return failFastInitialize; + } + + /** + * Sets whether {@link #initialize()} should throw if the connection cannot be opened. + * + * @param b whether {@link #initialize()} should throw + */ + public void setFailFastInitialize(final boolean b) { + failFastInitialize = b; + } + + /** + * Returns whether {@link #initialize()} should execute on a separate thread. + * + * @return whether {@link #initialize()} should block + */ + public boolean getNonBlockingInitialize() { + return nonBlockingInitialize; + } + + /** + * Sets whether {@link #initialize()} should execute on a separate thread. + * + * @param b whether {@link #initialize()} should block + */ + public void setNonBlockingInitialize(final boolean b) { + nonBlockingInitialize = b; + } + + /** + * Returns the function to run when the connection is opened. + * + * @return on open function + */ + public Function getOnOpen() { + return onOpen; + } + + /** + * Sets the function to run when the connection is opened. + * + * @param function to run on connection open + */ + public void setOnOpen(final Function function) { + onOpen = function; + } + + /** + * Returns the function to run when the connection is closed. + * + * @return on close function + */ + public Function getOnClose() { + return onClose; + } + + /** + * Sets the function to run when the connection is closed. + * + * @param function to run on connection close + */ + public void setOnClose(final Function function) { + onClose = function; + } + + /** + * Returns the connection validator for this factory. + * + * @return connection validator + */ + public ConnectionValidator getValidator() { + return validator; + } + + /** + * Sets the connection validator for this factory. + * + * @param cv connection validator + */ + public void setValidator(final ConnectionValidator cv) { + validator = cv; + } + + /** + * Returns whether this factory has been initialized. + * + * @return whether this factory has been initialized + */ + public boolean isInitialized() { + return initialized; + } + + /** + * Prepares this factory for use. + * + * @throws LdapException if the connection cannot be opened + */ + public synchronized void initialize() + throws LdapException { + if (initialized) { + throw new IllegalStateException("Connection factory is already initialized for " + this); + } + if (nonBlockingInitialize) { + if (factoryExecutor == null) { + if (validator != null) { + factoryExecutor = Executors.newSingleThreadScheduledExecutor( + r -> { + final Thread t = new Thread(r, getClass().getSimpleName() + "@" + hashCode()); + t.setDaemon(true); + return t; + }); + } else { + factoryExecutor = Executors.newCachedThreadPool( + r -> { + final Thread t = new Thread(r, getClass().getSimpleName() + "@" + hashCode()); + t.setDaemon(true); + return t; + }); + } + } + factoryExecutor.execute( + () -> { + try { + initializeInternal(); + } catch (LdapException e) { + // + } + }); + } else { + if (validator != null) { + factoryExecutor = Executors.newSingleThreadScheduledExecutor( + r -> { + final Thread t = new Thread(r, getClass().getSimpleName() + "@" + hashCode()); + t.setDaemon(true); + return t; + }); + } + initializeInternal(); + } + } + + /** + * Attempts to open the connection and establish the proxy. + * + * @throws LdapException if {@link Connection#open()} fails and {@link #failFastInitialize} is true + */ + private synchronized void initializeInternal() + throws LdapException { + LdapException initializeEx = null; + try { + initializeConnectionProxy(); + } catch (LdapException e) { + initializeEx = e; + } + if (validator != null) { + ((ScheduledExecutorService) factoryExecutor).scheduleAtFixedRate( + () -> { + try { + validator.apply(proxy != null ? proxy.getConnection() : null); + } catch (Exception e) { + // + } + }, + validator.getValidatePeriod().toMillis(), + validator.getValidatePeriod().toMillis(), + TimeUnit.MILLISECONDS); + } + if (initializeEx != null && failFastInitialize) { + throw initializeEx; + } + } + + /** + * Opens the connection and creates the connection proxy. Invokes {@link #onOpen} and will tear down the connection if + * that function returns false. + * + * @throws LdapException if connection open fails + */ + private void initializeConnectionProxy() + throws LdapException { + final Connection connection = super.getConnection(); + connection.open(); + proxy = new ConnectionProxy(connection); + initialized = true; + if (onOpen != null && !onOpen.apply(proxy.getConnection())) { + connection.close(); + proxy = null; + initialized = false; + throw new LdapException("On open function failed for " + this); + } + } + + /** + * Closes the connection and sets the proxy to null. Invokes {@link #onClose} prior to closing the connection. + */ + private void destroyConnectionProxy() { + if (proxy != null) { + if (onClose != null && !onClose.apply(proxy.getConnection())) { + } + proxy.getConnection().close(); + } + proxy = null; + initialized = false; + } + + @Override + public Connection getConnection() { + if (!initialized) { + throw new IllegalStateException("Connection factory is not initialized"); + } + return (Connection) Proxy.newProxyInstance( + Connection.class.getClassLoader(), + new Class[]{Connection.class}, + proxy); + } + + @Override + public synchronized void close() { + destroyConnectionProxy(); + super.close(); + if (factoryExecutor != null) { + try { + factoryExecutor.shutdown(); + } finally { + factoryExecutor = null; + } + } + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "transport=" + getTransport() + ", " + + "connection=" + (proxy != null ? proxy.getConnection() : null) + ", " + + "failFastInitialize=" + failFastInitialize + ", " + + "nonBlockingInitialize=" + nonBlockingInitialize + ", " + + "onOpen=" + onOpen + ", " + + "onClose=" + onClose + ", " + + "validator=" + validator + ", " + + "initialized=" + initialized + "]"; + } + + /** + * Contains the connection used by this factory. + */ + protected static class ConnectionProxy implements InvocationHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 509; + + /** + * Underlying connection. + */ + private final Connection conn; + + + /** + * Creates a new connection proxy. + * + * @param c connection to proxy + */ + public ConnectionProxy(final Connection c) { + conn = c; + } + + + /** + * Returns the connection that is being proxied. + * + * @return underlying connection + */ + public Connection getConnection() { + return conn; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof ConnectionProxy v) { + return LdapUtils.areEqual(conn, v.conn); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, conn); + } + + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) + throws Throwable { + Object retValue = null; + if (!"open".equals(method.getName()) && !"close".equals(method.getName())) { + try { + retValue = method.invoke(conn, args); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } + return retValue; + } + } + + // CheckStyle:OFF + public static class Builder extends DefaultConnectionFactory.Builder { + + private final SingleConnectionFactory object; + + + protected Builder() { + object = new SingleConnectionFactory(); + } + + + protected Builder(final Transport transport) { + object = new SingleConnectionFactory(transport); + } + + + public Builder config(final ConnectionConfig config) { + object.setConnectionConfig(config); + return this; + } + + + public Builder onOpen(final Function function) { + object.setOnOpen(function); + return this; + } + + + public Builder onClose(final Function function) { + object.setOnClose(function); + return this; + } + + + public Builder validator(final ConnectionValidator validator) { + object.setValidator(validator); + return this; + } + + + public Builder failFastInitialize(final boolean failFast) { + object.setFailFastInitialize(failFast); + return this; + } + + + public Builder nonBlockingInitialize(final boolean nonBlocking) { + object.setNonBlockingInitialize(nonBlocking); + return this; + } + + + public SingleConnectionFactory build() { + return object; + } + } + // CheckStyle:ON + + + /** + * Invokes {@link #destroyConnectionProxy()} followed by {@link #initializeConnectionProxy()}. + */ + public class ReinitializeConnectionConsumer implements Consumer { + + @Override + public void accept(final Connection conn) { + if (proxy != null && !proxy.getConnection().equals(conn)) { + throw new IllegalArgumentException("Connection not managed by this factory: " + conn); + } + try { + destroyConnectionProxy(); + initializeConnectionProxy(); + } catch (Exception e) { + // + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/UnbindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/UnbindRequest.java new file mode 100644 index 0000000..0c857a7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/UnbindRequest.java @@ -0,0 +1,31 @@ + +package org.xbib.net.ldap; + +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.NullType; + +/** + * LDAP unbind request defined as: + * + *
+ * UnbindRequest ::= [APPLICATION 2] NULL
+ * 
+ */ +public class UnbindRequest extends AbstractRequestMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 2; + + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + return new DEREncoder[]{ + new IntegerType(id), + new NullType(new ApplicationDERTag(PROTOCOL_OP, false)), + }; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/GlobalIdentifier.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/GlobalIdentifier.java new file mode 100644 index 0000000..2777f0d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/GlobalIdentifier.java @@ -0,0 +1,142 @@ + +package org.xbib.net.ldap.ad; + +import java.nio.ByteBuffer; +import java.util.StringTokenizer; +import org.xbib.net.ldap.LdapUtils; + +/** + * Class to represent an active directory GUID. Provides conversion from binary to string and vice versa. + * + */ +public final class GlobalIdentifier { + + + /** + * Default constructor. + */ + private GlobalIdentifier() { + } + + + /** + * Converts the supplied GUID to its string format. + * + * @param guid to convert + * @return string format of the GUID + */ + public static String toString(final byte[] guid) { + // CheckStyle:MagicNumber OFF + + // create a byte buffer for reading the guid + final ByteBuffer guidBuffer = ByteBuffer.wrap(guid); + + // string identifier + final StringBuilder sb = new StringBuilder("{"); + // encode the first 4 bytes, big endian + guidBuffer.limit(4); + sb.append(LdapUtils.hexEncode(getBytes(guidBuffer, true))); + + // encode the next 2 bytes, big endian + guidBuffer.limit(6); + sb.append("-").append(LdapUtils.hexEncode(getBytes(guidBuffer, true))); + + // encode the next 2 bytes, big endian + guidBuffer.limit(8); + sb.append("-").append(LdapUtils.hexEncode(getBytes(guidBuffer, true))); + + // encode the next 2 bytes, little endian + guidBuffer.limit(10); + sb.append("-").append(LdapUtils.hexEncode(getBytes(guidBuffer, false))); + + // encode the last 6 bytes, little endian + guidBuffer.limit(guidBuffer.capacity()); + sb.append("-").append(LdapUtils.hexEncode(getBytes(guidBuffer, false))); + sb.append("}"); + + return sb.toString(); + // CheckStyle:MagicNumber ON + } + + + /** + * Converts the supplied GUID to its binary format. + * + * @param guid to convert + * @return binary format of the GUID + */ + public static byte[] toBytes(final String guid) { + // CheckStyle:MagicNumber OFF + + // remove the enclosing brackets {...} + final StringTokenizer st = new StringTokenizer(guid.substring(1, guid.length() - 1), "-"); + // first token is 4 bytes, big endian + final String data1 = st.nextToken(); + // second token is 2 bytes, big endian + final String data2 = st.nextToken(); + // third token is 2 bytes, big endian + final String data3 = st.nextToken(); + // fourth token is 2 bytes, little endian + final String data4 = st.nextToken(); + // fifth token is 6 bytes, little endian + final String data5 = st.nextToken(); + + final ByteBuffer guidBuffer = ByteBuffer.allocate(16); + putBytes(guidBuffer, LdapUtils.hexDecode(data1.toCharArray()), true); + putBytes(guidBuffer, LdapUtils.hexDecode(data2.toCharArray()), true); + putBytes(guidBuffer, LdapUtils.hexDecode(data3.toCharArray()), true); + putBytes(guidBuffer, LdapUtils.hexDecode(data4.toCharArray()), false); + putBytes(guidBuffer, LdapUtils.hexDecode(data5.toCharArray()), false); + + return guidBuffer.array(); + // CheckStyle:MagicNumber ON + } + + + /** + * Reads bytes from the supplied byte buffer. The byte buffer limit must be set appropriately by the caller. + * + * @param buffer to read bytes from + * @param bigEndian whether to return the bytes as big endian + * @return long value + */ + private static byte[] getBytes(final ByteBuffer buffer, final boolean bigEndian) { + // CheckStyle:MagicNumber OFF + final byte[] bytes = new byte[buffer.limit() - buffer.position()]; + if (bigEndian) { + int offset = bytes.length - 1; + while (buffer.hasRemaining()) { + bytes[offset--] = (byte) (buffer.get() & 0xFF); + } + } else { + int offset = 0; + while (buffer.hasRemaining()) { + bytes[offset++] = (byte) (buffer.get() & 0xFF); + } + } + return bytes; + // CheckStyle:MagicNumber ON + } + + + /** + * Writes a long into the supplied byte buffer. The byte buffer limit must be set appropriately by the caller. + * + * @param buffer to write long to + * @param bytes to write + * @param bigEndian whether to write the bytes as big endian + */ + private static void putBytes(final ByteBuffer buffer, final byte[] bytes, final boolean bigEndian) { + // CheckStyle:MagicNumber OFF + if (bigEndian) { + for (int i = bytes.length - 1; i >= 0; i--) { + buffer.put((byte) (bytes[i] & 0xFF)); + } + } else { + for (byte b : bytes) { + buffer.put((byte) (b & 0xFF)); + } + } + // CheckStyle:MagicNumber ON + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/SecurityIdentifier.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/SecurityIdentifier.java new file mode 100644 index 0000000..6ec03a5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/SecurityIdentifier.java @@ -0,0 +1,171 @@ + +package org.xbib.net.ldap.ad; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +/** + * Class to represent an active directory SID. Provides conversion from binary to string and vice versa. + * + */ +public final class SecurityIdentifier { + + + /** + * Default constructor. + */ + private SecurityIdentifier() { + } + + + /** + * Converts the supplied SID to its string format. + * + * @param sid to convert + * @return string format of the SID + */ + public static String toString(final byte[] sid) { + // CheckStyle:MagicNumber OFF + + // format of SID: S-R-X-Y1-Y2...-Yn + // S: static 'S', indicating string + // R: revision + // X: authority + // Yn: sub-authority + + // create a byte buffer for reading the sid + final ByteBuffer sidBuffer = ByteBuffer.wrap(sid); + + // string identifier + final StringBuilder sb = new StringBuilder("S"); + + // byte[0] is the revision + sb.append("-").append(sidBuffer.get() & 0xFF); + + // byte[1] is the count of sub-authorities + final int countSubAuth = sidBuffer.get() & 0xFF; + + // byte[2] - byte[7] is the authority (48 bits) + sidBuffer.limit(8); + sb.append("-").append(getLong(sidBuffer, true)); + + // byte[8] - ? is the sub-authorities, + // (32 bits per authority, little endian) + for (int i = 0; i < countSubAuth; i++) { + // values are unsigned, so get 4 bytes as a long + sidBuffer.limit(sidBuffer.position() + 4); + sb.append("-").append(getLong(sidBuffer, false)); + } + + return sb.toString(); + // CheckStyle:MagicNumber ON + } + + + /** + * Converts the supplied SID to its binary format. + * + * @param sid to convert + * @return binary format of the SID + */ + public static byte[] toBytes(final String sid) { + // CheckStyle:MagicNumber OFF + + // format of SID: S-R-X-Y1-Y2...-Yn + // S: static 'S', indicating string + // R: revision + // X: authority + // Yn: sub-authority + + final StringTokenizer st = new StringTokenizer(sid, "-"); + // first token is the 'S' + st.nextToken(); + + // second token is the revision + final int revision = Integer.parseInt(st.nextToken()); + // third token is the authority + final long authority = Long.parseLong(st.nextToken()); + // remaining token are the sub authorities + final List subAuthorities = new ArrayList<>(); + while (st.hasMoreTokens()) { + subAuthorities.add(st.nextToken()); + } + + // revision is 1 byte + // sub-authorities count is 1 byte + // authority is 6 bytes + // 4 bytes for each sub-authority + final int size = 8 + (4 * subAuthorities.size()); + final ByteBuffer sidBuffer = ByteBuffer.allocate(size); + sidBuffer.put((byte) (revision & 0xFF)); + sidBuffer.put((byte) (subAuthorities.size() & 0xFF)); + sidBuffer.limit(8); + putLong(sidBuffer, authority, true); + for (String subAuthority : subAuthorities) { + sidBuffer.limit(sidBuffer.position() + 4); + putLong(sidBuffer, Long.parseLong(subAuthority), false); + } + + return sidBuffer.array(); + // CheckStyle:MagicNumber ON + } + + + /** + * Reads a long from the supplied byte buffer. The byte buffer limit must be set appropriately by the caller. + * + * @param buffer to read long from + * @param bigEndian whether to read the bytes as big endian + * @return long value + */ + private static long getLong(final ByteBuffer buffer, final boolean bigEndian) { + // CheckStyle:MagicNumber OFF + long value = buffer.get() & 0xFF; + if (bigEndian) { + // shift the value to the right, or with the next byte + while (buffer.hasRemaining()) { + value <<= Byte.SIZE; + value |= buffer.get() & 0xFF; + } + } else { + // shift the next byte to the right, or with the value + int offset = Byte.SIZE; + while (buffer.hasRemaining()) { + value |= (long) (buffer.get() & 0xFF) << offset; + offset += Byte.SIZE; + } + } + return value & 0xFFFFFFFFL; + // CheckStyle:MagicNumber ON + } + + + /** + * Writes a long into the supplied byte buffer. The byte buffer limit must be set appropriately by the caller. + * + * @param buffer to write long to + * @param value to write + * @param bigEndian whether to write the bytes as big endian + */ + private static void putLong(final ByteBuffer buffer, final long value, final boolean bigEndian) { + // CheckStyle:MagicNumber OFF + if (bigEndian) { + int offset = Byte.SIZE * (buffer.limit() - buffer.position() - 1); + while (buffer.hasRemaining()) { + // get the high bits and decrement down + buffer.put((byte) ((value >> offset) & 0xFF)); + offset -= Byte.SIZE; + } + } else { + int offset = 0; + while (buffer.hasRemaining()) { + // get the low bits and increment up + buffer.put((byte) ((value >> offset) & 0xFF)); + offset += Byte.SIZE; + } + } + // CheckStyle:MagicNumber ON + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/UnicodePwdAttribute.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/UnicodePwdAttribute.java new file mode 100644 index 0000000..68dec7d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/UnicodePwdAttribute.java @@ -0,0 +1,56 @@ + +package org.xbib.net.ldap.ad; + +import java.util.Collection; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.ad.transcode.UnicodePwdValueTranscoder; + +/** + * Helper class for the active directory unicodePwd attribute. Configures a binary attribute of that name and allows + * setting of the attribute value using a string. See {@link UnicodePwdValueTranscoder}. + * + */ +public class UnicodePwdAttribute extends LdapAttribute { + + /** + * name of this attribute. + */ + private static final String ATTR_NAME = "unicodePwd"; + + /** + * transcoder used when adding string values. + */ + private static final UnicodePwdValueTranscoder TRANSCODER = new UnicodePwdValueTranscoder(); + + + /** + * Default constructor. + */ + public UnicodePwdAttribute() { + setName(ATTR_NAME); + setBinary(true); + } + + + /** + * Creates a new unicode pwd attribute. + * + * @param values of this attribute + */ + public UnicodePwdAttribute(final String... values) { + this(); + addStringValues(values); + } + + + @Override + public Collection getStringValues() { + return getValues(TRANSCODER.decoder()); + } + + + @Override + public void addStringValues(final String... value) { + addValues(TRANSCODER.encoder(), value); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/DirSyncControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/DirSyncControl.java new file mode 100644 index 0000000..f7070a3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/DirSyncControl.java @@ -0,0 +1,415 @@ + +package org.xbib.net.ldap.ad.control; + +import java.math.BigInteger; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.control.ResponseControl; + +/** + * Request/response control for active directory synchronization. Control is defined as: + * + *
+ * dirSyncValue ::= SEQUENCE {
+ * flags              INTEGER,
+ * maxAttributeCount  INTEGER,
+ * cookie             OCTET STRING
+ * }
+ * 
+ * + *

See http://msdn.microsoft.com/en-us/library/cc223347.aspx

+ * + */ +public class DirSyncControl extends AbstractControl implements RequestControl, ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.841"; + + /** + * hash value seed. + */ + private static final int HASH_CODE_SEED = 907; + + /** + * Empty byte array used for null cookies. + */ + private static final byte[] EMPTY_COOKIE = new byte[0]; + /** + * flags. + */ + private long flags; + /** + * maximum attribute count. + */ + private int maxAttributeCount; + /** + * server generated cookie. + */ + private byte[] cookie; + + /** + * Default constructor. + */ + public DirSyncControl() { + super(OID); + } + + + /** + * Creates a new dir sync control. + * + * @param critical whether this control is critical + */ + public DirSyncControl(final boolean critical) { + this(null, null, critical); + } + + + /** + * Creates a new dir sync control. + * + * @param f request flags + */ + public DirSyncControl(final Flag[] f) { + this(f, false); + } + + + /** + * Creates a new dir sync control. + * + * @param f request flags + * @param critical whether this control is critical + */ + public DirSyncControl(final Flag[] f, final boolean critical) { + this(f, null, critical); + } + + + /** + * Creates a new dir sync control. + * + * @param f request flags + * @param count maximum attribute count + */ + public DirSyncControl(final Flag[] f, final int count) { + this(f, null, count, false); + } + + + /** + * Creates a new dir sync control. + * + * @param f request flags + * @param count maximum attribute count + * @param critical whether this control is critical + */ + public DirSyncControl(final Flag[] f, final int count, final boolean critical) { + this(f, null, count, critical); + } + + + /** + * Creates a new dir sync control. + * + * @param f request flags + * @param value dir sync cookie + * @param critical whether this control is critical + */ + public DirSyncControl(final Flag[] f, final byte[] value, final boolean critical) { + this(f, value, 0, critical); + } + + + /** + * Creates a new dir sync control. + * + * @param f request flags + * @param value dir sync cookie + * @param count maximum attribute count + * @param critical whether this control is critical + */ + public DirSyncControl(final Flag[] f, final byte[] value, final int count, final boolean critical) { + super(OID, critical); + if (f != null) { + long l = 0; + for (Flag flag : f) { + if (flag != null) { + l += flag.value(); + } + } + setFlags(l); + } + setCookie(value); + setMaxAttributeCount(count); + } + + @Override + public boolean hasValue() { + return true; + } + + /** + * Returns the flags value. + * + * @return flags value + */ + public long getFlags() { + return flags; + } + + /** + * Sets the flags. + * + * @param l flags value + */ + public void setFlags(final long l) { + flags = l; + } + + /** + * Returns the maximum attribute count. + * + * @return maximum attribute count + */ + public int getMaxAttributeCount() { + return maxAttributeCount; + } + + /** + * Sets the maximum attribute count. + * + * @param count maximum attribute count + */ + public void setMaxAttributeCount(final int count) { + maxAttributeCount = count; + } + + /** + * Returns the sync request cookie. + * + * @return sync request cookie + */ + public byte[] getCookie() { + return cookie; + } + + /** + * Sets the sync request cookie. + * + * @param value sync request cookie + */ + public void setCookie(final byte[] value) { + cookie = value; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof DirSyncControl v && super.equals(o)) { + return LdapUtils.areEqual(flags, v.flags) && + LdapUtils.areEqual(maxAttributeCount, v.maxAttributeCount) && + LdapUtils.areEqual(cookie, v.cookie); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), flags, maxAttributeCount, cookie); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "flags=" + flags + ", " + + "maxAttributeCount=" + maxAttributeCount + ", " + + "cookie=" + LdapUtils.base64Encode(cookie) + "]"; + } + + @Override + public byte[] encode() { + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(BigInteger.valueOf(getFlags())), + new IntegerType(getMaxAttributeCount()), + new OctetStringType(getCookie() != null ? getCookie() : EMPTY_COOKIE)); + return se.encode(); + } + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(FlagHandler.PATH, new FlagHandler(this)); + parser.registerHandler(MaxAttrCountHandler.PATH, new MaxAttrCountHandler(this)); + parser.registerHandler(CookieHandler.PATH, new CookieHandler(this)); + parser.parse(encoded); + } + + + /** + * Types of flags. + */ + public enum Flag { + + /** + * object security. + */ + OBJECT_SECURITY(1L), + + /** + * ancestors first order. + */ + ANCESTORS_FIRST_ORDER(2048L), + + /** + * public data only. + */ + PUBLIC_DATA_ONLY(8192L), + + /** + * incremental values. + */ + INCREMENTAL_VALUES(2147483648L); + + /** + * underlying value. + */ + private final long value; + + + /** + * Creates a new flag. + * + * @param l value + */ + Flag(final long l) { + value = l; + } + + /** + * Returns the flag for the supplied integer constant. + * + * @param l to find flag for + * @return flag + */ + public static Flag valueOf(final long l) { + for (Flag f : Flag.values()) { + if (f.value() == l) { + return f; + } + } + return null; + } + + /** + * Returns the value. + * + * @return enum value + */ + public long value() { + return value; + } + } + + /** + * Parse handler implementation for the flag. + */ + private static class FlagHandler extends AbstractParseHandler { + + /** + * DER path to flag. + */ + public static final DERPath PATH = new DERPath("/SEQ/INT[0]"); + + + /** + * Creates a new flag handler. + * + * @param control to configure + */ + FlagHandler(final DirSyncControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setFlags(IntegerType.decode(encoded).longValue()); + } + } + + + /** + * Parse handler implementation for the maxAttributeCount. + */ + private static class MaxAttrCountHandler extends AbstractParseHandler { + + /** + * DER path to cookie value. + */ + public static final DERPath PATH = new DERPath("/SEQ/INT[1]"); + + + /** + * Creates a new max attr count handler. + * + * @param control to configure + */ + MaxAttrCountHandler(final DirSyncControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setMaxAttributeCount(IntegerType.decode(encoded).intValue()); + } + } + + + /** + * Parse handler implementation for the cookie. + */ + private static class CookieHandler extends AbstractParseHandler { + + /** + * DER path to cookie value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[2]"); + + + /** + * Creates a new cookie handler. + * + * @param control to configure + */ + CookieHandler(final DirSyncControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setCookie(cookie); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ExtendedDnControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ExtendedDnControl.java new file mode 100644 index 0000000..4fb12a9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ExtendedDnControl.java @@ -0,0 +1,141 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.UniversalDERTag; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to use an extended form of an object distinguished name. Control is + * defined as: + * + *
+ * extendedDnValue ::= SEQUENCE {
+ * flag  INTEGER
+ * }
+ * 
+ * + *

See http://msdn.microsoft.com/en-us/library/cc223349.aspx

+ * + */ +public class ExtendedDnControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.529"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 919; + /** + * flag. + */ + private Flag flag = Flag.STANDARD; + + /** + * Default constructor. + */ + public ExtendedDnControl() { + super(OID); + } + + + /** + * Creates a new extended dn control. + * + * @param f flag + */ + public ExtendedDnControl(final Flag f) { + super(OID); + setFlag(f); + } + + + /** + * Creates a new extended dn control. + * + * @param f flag + * @param critical whether this control is critical + */ + public ExtendedDnControl(final Flag f, final boolean critical) { + super(OID, critical); + setFlag(f); + } + + @Override + public boolean hasValue() { + return true; + } + + /** + * Returns the flag. + * + * @return flag + */ + public Flag getFlag() { + return flag; + } + + /** + * Sets the flag. + * + * @param f flag + */ + public void setFlag(final Flag f) { + flag = f; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof ExtendedDnControl v && super.equals(o)) { + return LdapUtils.areEqual(flag, v.flag); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), flag); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "flag=" + flag + "]"; + } + + @Override + public byte[] encode() { + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(getFlag().ordinal())); + return se.encode(); + } + + + /** + * Types of flags. + */ + public enum Flag { + + /** + * hexadecimal format. + */ + HEXADECIMAL, + + /** + * standard format. + */ + STANDARD + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ForceUpdateControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ForceUpdateControl.java new file mode 100644 index 0000000..e92b8e6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ForceUpdateControl.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to perform an update even if the data is already the same. See + * http://msdn.microsoft.com/en-us/library/cc223344.aspx + * + */ +public class ForceUpdateControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.1974"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 971; + + + /** + * Default constructor. + */ + public ForceUpdateControl() { + super(OID); + } + + + /** + * Creates a new force update control. + * + * @param critical whether this control is critical + */ + public ForceUpdateControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ForceUpdateControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/GetStatsControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/GetStatsControl.java new file mode 100644 index 0000000..8bb9c0c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/GetStatsControl.java @@ -0,0 +1,285 @@ + +package org.xbib.net.ldap.ad.control; + +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.control.ResponseControl; + +/** + * Request/response control for active directory servers to return statistics along with search results. This + * implementation supports the format for Windows Server 2008, Windows Server 2008 R2, and Windows Server 2012 DCs. The + * response control is defined as: + * + *
+ * SEQUENCE {
+ * threadCountTag        INTEGER
+ * threadCount           INTEGER
+ * callTimeTag           INTEGER
+ * callTime              INTEGER
+ * entriesReturnedTag    INTEGER
+ * entriesReturned       INTEGER
+ * entriesVisitedTag     INTEGER
+ * entriesVisited        INTEGER
+ * filterTag             INTEGER
+ * filter                OCTET STRING
+ * indexTag              INTEGER
+ * index                 OCTET STRING
+ * pagesReferencedTag    INTEGER
+ * pagesReferenced       INTEGER
+ * pagesReadTag          INTEGER
+ * pagesRead             INTEGER
+ * pagesPrereadTag       INTEGER
+ * pagesPreread          INTEGER
+ * pagesDirtiedTag       INTEGER
+ * pagesDirtied          INTEGER
+ * pagesRedirtiedTag     INTEGER
+ * pagesRedirtied        INTEGER
+ * logRecordCountTag     INTEGER
+ * logRecordCount        INTEGER
+ * logRecordBytesTag     INTEGER
+ * logRecordBytes        INTEGER
+ * }
+ * 
+ * + *

See http://msdn.microsoft.com/en-us/library/cc223350.aspx

+ * + */ +public class GetStatsControl extends AbstractControl implements RequestControl, ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.970"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 929; + + /** + * DER path to thread count. + */ + private static final DERPath THREAD_COUNT_PATH = new DERPath("/SEQ/INT[1]"); + + /** + * DER path to call time. + */ + private static final DERPath CALL_TIME_PATH = new DERPath("/SEQ/INT[3]"); + + /** + * DER path to entries returned. + */ + private static final DERPath ENTRIES_RETURNED_PATH = new DERPath("/SEQ/INT[5]"); + + /** + * DER path to entries visited. + */ + private static final DERPath ENTRIES_VISITED_PATH = new DERPath("/SEQ/INT[7]"); + + /** + * DER path to filter. + */ + private static final DERPath FILTER_PATH = new DERPath("/SEQ/OCTSTR[9]"); + + /** + * DER path to index. + */ + private static final DERPath INDEX_PATH = new DERPath("/SEQ/OCTSTR[11]"); + + /** + * DER path to pages referenced. + */ + private static final DERPath PAGES_REFERENCED_PATH = new DERPath("/SEQ/INT[13]"); + + /** + * DER path to pages read. + */ + private static final DERPath PAGES_READ_PATH = new DERPath("/SEQ/INT[15]"); + + /** + * DER path to pages preread. + */ + private static final DERPath PAGES_PREREAD_PATH = new DERPath("/SEQ/INT[17]"); + + /** + * DER path to pages dirtied. + */ + private static final DERPath PAGES_DIRTIED_PATH = new DERPath("/SEQ/INT[19]"); + + /** + * DER path to pages redirtied. + */ + private static final DERPath PAGES_REDIRTIED_PATH = new DERPath("/SEQ/INT[21]"); + + /** + * DER path to log record count. + */ + private static final DERPath LOG_RECORD_COUNT_PATH = new DERPath("/SEQ/INT[23]"); + + /** + * DER path to log record bytes. + */ + private static final DERPath LOG_RECORD_BYTES_PATH = new DERPath("/SEQ/INT[25]"); + + /** + * statistics. + */ + private final Map statistics = new HashMap<>(); + + + /** + * Default constructor. + */ + public GetStatsControl() { + super(OID); + } + + + /** + * Creates a new get stats control. + * + * @param critical whether this control is critical + */ + public GetStatsControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the statistics. + * + * @return statistics + */ + public Map getStatistics() { + return statistics; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof GetStatsControl v && super.equals(o)) { + return LdapUtils.areEqual(statistics, v.statistics); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), statistics); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "statistics=" + statistics + "]"; + } + + + @Override + public byte[] encode() { + return null; + } + + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(THREAD_COUNT_PATH, new IntegerHandler(this, "threadCount")); + parser.registerHandler(CALL_TIME_PATH, new IntegerHandler(this, "callTime")); + parser.registerHandler(ENTRIES_RETURNED_PATH, new IntegerHandler(this, "entriesReturned")); + parser.registerHandler(ENTRIES_VISITED_PATH, new IntegerHandler(this, "entriesVisited")); + parser.registerHandler(FILTER_PATH, new StringHandler(this, "filter")); + parser.registerHandler(INDEX_PATH, new StringHandler(this, "index")); + parser.registerHandler(PAGES_REFERENCED_PATH, new IntegerHandler(this, "pagesReferenced")); + parser.registerHandler(PAGES_READ_PATH, new IntegerHandler(this, "pagesRead")); + parser.registerHandler(PAGES_PREREAD_PATH, new IntegerHandler(this, "pagesPreread")); + parser.registerHandler(PAGES_DIRTIED_PATH, new IntegerHandler(this, "pagesDirtied")); + parser.registerHandler(PAGES_REDIRTIED_PATH, new IntegerHandler(this, "pagesRedirtied")); + parser.registerHandler(LOG_RECORD_COUNT_PATH, new IntegerHandler(this, "logRecordCount")); + parser.registerHandler(LOG_RECORD_BYTES_PATH, new IntegerHandler(this, "logRecordBytes")); + parser.parse(encoded); + } + + + /** + * Parse handler implementation for integer stats. + */ + private static class IntegerHandler extends AbstractParseHandler { + + /** + * name of this statistic. + */ + private final String statName; + + + /** + * Creates a new integer handler. + * + * @param control to configure + * @param name of the statistic + */ + IntegerHandler(final GetStatsControl control, final String name) { + super(control); + statName = name; + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().getStatistics().put(statName, IntegerType.decode(encoded).intValue()); + } + } + + + /** + * Parse handler implementation for string stats. + */ + private static class StringHandler extends AbstractParseHandler { + + /** + * name of this statistic. + */ + private final String statName; + + + /** + * Creates a new string handler. + * + * @param control to configure + * @param name of the statistic + */ + StringHandler(final GetStatsControl control, final String name) { + super(control); + statName = name; + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + // strings are terminated with 0x00(null), use trim to remove + getObject().getStatistics().put(statName, OctetStringType.decode(encoded).trim()); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/LazyCommitControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/LazyCommitControl.java new file mode 100644 index 0000000..19d91b8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/LazyCommitControl.java @@ -0,0 +1,68 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory domain controllers to sacrifice durability guarantees on updates to improve + * performance. See http://msdn.microsoft.com/en-us/library/cc223351.aspx + * + */ +public class LazyCommitControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.619"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 937; + + + /** + * Default constructor. + */ + public LazyCommitControl() { + super(OID); + } + + + /** + * Creates a new lazy commit control. + * + * @param critical whether this control is critical + */ + public LazyCommitControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof LazyCommitControl && super.equals(o); + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/NotificationControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/NotificationControl.java new file mode 100644 index 0000000..1fbaa10 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/NotificationControl.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to send asynchronous notifications to the client when a change is made. + * See http://msdn.microsoft.com/en-us/library/cc223353.aspx + * + */ +public class NotificationControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.528"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 947; + + + /** + * Default constructor. + */ + public NotificationControl() { + super(OID); + } + + + /** + * Creates a new notification control. + * + * @param critical whether this control is critical + */ + public NotificationControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof NotificationControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/PermissiveModifyControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/PermissiveModifyControl.java new file mode 100644 index 0000000..a8e69fc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/PermissiveModifyControl.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to return success on add/modify/delete operations that would normally + * return an error. See http://msdn.microsoft.com/en-us/library/cc223352.aspx + * + */ +public class PermissiveModifyControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.1413"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 941; + + + /** + * Default constructor. + */ + public PermissiveModifyControl() { + super(OID); + } + + + /** + * Creates a new permissive modify control. + * + * @param critical whether this control is critical + */ + public PermissiveModifyControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof PermissiveModifyControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/RangeRetrievalNoerrControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/RangeRetrievalNoerrControl.java new file mode 100644 index 0000000..b825fb6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/RangeRetrievalNoerrControl.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to avoid error response with range retrieval. See + * http://msdn.microsoft.com/en-us/library/cc223345.aspx + * + */ +public class RangeRetrievalNoerrControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.1948"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 983; + + + /** + * Default constructor. + */ + public RangeRetrievalNoerrControl() { + super(OID); + } + + + /** + * Creates a new notification control. + * + * @param critical whether this control is critical + */ + public RangeRetrievalNoerrControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof RangeRetrievalNoerrControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/SearchOptionsControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/SearchOptionsControl.java new file mode 100644 index 0000000..a2b0d40 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/SearchOptionsControl.java @@ -0,0 +1,140 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.UniversalDERTag; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to control various search behaviors. Control is defined as: + * + *
+ * searchOptionsValue ::= SEQUENCE {
+ * flag  INTEGER
+ * }
+ * 
+ * + *

See http://msdn.microsoft.com/en-us/library/cc223324.aspx

+ * + */ +public class SearchOptionsControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.1340"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 953; + /** + * flag. + */ + private Flag flag = Flag.DOMAIN_SCOPE; + + /** + * Default constructor. + */ + public SearchOptionsControl() { + super(OID); + } + + + /** + * Creates a new search options control. + * + * @param f flag + */ + public SearchOptionsControl(final Flag f) { + super(OID); + setFlag(f); + } + + + /** + * Creates a new search options control. + * + * @param f flag + * @param critical whether this control is critical + */ + public SearchOptionsControl(final Flag f, final boolean critical) { + super(OID, critical); + setFlag(f); + } + + @Override + public boolean hasValue() { + return true; + } + + /** + * Returns the flag. + * + * @return flag + */ + public Flag getFlag() { + return flag; + } + + /** + * Sets the flag. + * + * @param f flag + */ + public void setFlag(final Flag f) { + flag = f; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SearchOptionsControl v && super.equals(o)) { + return LdapUtils.areEqual(flag, v.flag); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), flag); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "flag=" + flag + "]"; + } + + @Override + public byte[] encode() { + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(getFlag().ordinal())); + return se.encode(); + } + + + /** + * Types of flags. + */ + public enum Flag { + + /** + * SERVER_SEARCH_FLAG_DOMAIN_SCOPE . + */ + DOMAIN_SCOPE, + + /** + * SERVER_SEARCH_FLAG_PHANTOM_ROOT . + */ + PHANTOM_ROOT + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowDeactivatedLinkControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowDeactivatedLinkControl.java new file mode 100644 index 0000000..1d520a0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowDeactivatedLinkControl.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers in include link attributes that refer to deleted-objects in a search + * operation. See http://msdn.microsoft.com/en-us/library/dd302781.aspx + * + */ +public class ShowDeactivatedLinkControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.2065"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 967; + + + /** + * Default constructor. + */ + public ShowDeactivatedLinkControl() { + super(OID); + } + + + /** + * Creates a new show deactivated link control. + * + * @param critical whether this control is critical + */ + public ShowDeactivatedLinkControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ShowDeactivatedLinkControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowDeletedControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowDeletedControl.java new file mode 100644 index 0000000..f75eec3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowDeletedControl.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to include deleted objects that match a search filter. See + * http://msdn.microsoft.com/en-us/library/cc223326.aspx + * + */ +public class ShowDeletedControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.417"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 911; + + + /** + * Default constructor. + */ + public ShowDeletedControl() { + super(OID); + } + + + /** + * Creates a new show deleted control. + * + * @param critical whether this control is critical + */ + public ShowDeletedControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ShowDeletedControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowRecycledControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowRecycledControl.java new file mode 100644 index 0000000..82d98ff --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/ShowRecycledControl.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to include tombstones, deleted-objects, and recycled-objects that match + * a search filter. See http://msdn.microsoft.com/en-us/library/dd304621.aspx + * + */ +public class ShowRecycledControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.2064"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 991; + + + /** + * Default constructor. + */ + public ShowRecycledControl() { + super(OID); + } + + + /** + * Creates a new show recycled control. + * + * @param critical whether this control is critical + */ + public ShowRecycledControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ShowRecycledControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/VerifyNameControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/VerifyNameControl.java new file mode 100644 index 0000000..774af04 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/VerifyNameControl.java @@ -0,0 +1,136 @@ + +package org.xbib.net.ldap.ad.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; +import org.xbib.net.ldap.control.AbstractControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Request control for active directory servers to use an extended form of an object distinguished name. Control is + * defined as: + * + *
+ * verifyNameValue ::= SEQUENCE {
+ * Flags       INTEGER
+ * ServerName  OCTET STRING
+ * }
+ * 
+ * + *

See http://msdn.microsoft.com/en-us/library/cc223328.aspx

+ * + */ +public class VerifyNameControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.1338"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 977; + + /** + * Global catalog server to contact. + */ + private String serverName; + + + /** + * Default constructor. + */ + public VerifyNameControl() { + super(OID); + } + + + /** + * Creates a new verify name control. + * + * @param name server name + */ + public VerifyNameControl(final String name) { + super(OID); + setServerName(name); + } + + + /** + * Creates a new verify name control. + * + * @param name server name + * @param critical whether this control is critical + */ + public VerifyNameControl(final String name, final boolean critical) { + super(OID, critical); + setServerName(name); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the server name. + * + * @return server name + */ + public String getServerName() { + return serverName; + } + + + /** + * Sets the server name. + * + * @param name server name + */ + public void setServerName(final String name) { + serverName = name; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof VerifyNameControl v && super.equals(o)) { + return LdapUtils.areEqual(serverName, v.serverName); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), serverName); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "serverName=" + serverName + "]"; + } + + + @Override + public byte[] encode() { + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(0), + new OctetStringType(serverName)); + return se.encode(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/util/DirSyncClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/util/DirSyncClient.java new file mode 100644 index 0000000..4723e85 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/util/DirSyncClient.java @@ -0,0 +1,403 @@ + +package org.xbib.net.ldap.ad.control.util; + +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.ad.control.DirSyncControl; +import org.xbib.net.ldap.ad.control.ExtendedDnControl; +import org.xbib.net.ldap.ad.control.ShowDeletedControl; +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.control.util.CookieManager; +import org.xbib.net.ldap.control.util.DefaultCookieManager; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.LdapEntryHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.SearchReferenceHandler; +import org.xbib.net.ldap.handler.SearchResultHandler; + +/** + * Client that simplifies using the active directory dir sync control. + * + */ +public class DirSyncClient { + + /** + * Connection factory to get a connection from. + */ + private final ConnectionFactory factory; + + /** + * DirSync flags. + */ + private final DirSyncControl.Flag[] dirSyncFlags; + + /** + * Maximum attribute count. + */ + private final int maxAttributeCount; + + /** + * ExtendedDn flags. + */ + private ExtendedDnControl.Flag extendedDnFlag = ExtendedDnControl.Flag.STANDARD; + + /** + * Functions to handle response results. + */ + private ResultHandler[] resultHandlers; + + /** + * Function to handle exceptions. + */ + private ExceptionHandler exceptionHandler; + + /** + * Function to test results. + */ + private ResultPredicate throwCondition; + + /** + * Functions to handle response entries. + */ + private LdapEntryHandler[] entryHandlers; + + /** + * Functions to handle response references. + */ + private SearchReferenceHandler[] referenceHandlers; + + /** + * Functions to handle response results. + */ + private SearchResultHandler[] searchResultHandlers; + + + /** + * Creates a new dir sync client. + * + * @param cf to get a connection from + */ + public DirSyncClient(final ConnectionFactory cf) { + this(cf, null, 0); + } + + + /** + * Creates a new dir sync client. + * + * @param cf to get a connection from + * @param dsFlags to set on the dir sync control + */ + public DirSyncClient(final ConnectionFactory cf, final DirSyncControl.Flag[] dsFlags) { + this(cf, dsFlags, 0); + } + + + /** + * Creates a new dir sync client. + * + * @param cf to get a connection from + * @param dsFlags to set on the dir sync control + * @param count max attribute count + */ + public DirSyncClient(final ConnectionFactory cf, final DirSyncControl.Flag[] dsFlags, final int count) { + factory = cf; + dirSyncFlags = dsFlags; + maxAttributeCount = count; + } + + + public ResultHandler[] getResultHandlers() { + return resultHandlers; + } + + + public void setResultHandlers(final ResultHandler... handlers) { + resultHandlers = handlers; + } + + + public ExceptionHandler getExceptionHandler() { + return exceptionHandler; + } + + + public void setExceptionHandler(final ExceptionHandler handler) { + exceptionHandler = handler; + } + + + public ResultPredicate getThrowCondition() { + return throwCondition; + } + + + public void setThrowCondition(final ResultPredicate function) { + throwCondition = function; + } + + + public LdapEntryHandler[] getEntryHandlers() { + return entryHandlers; + } + + + public void setEntryHandlers(final LdapEntryHandler... handlers) { + entryHandlers = handlers; + } + + + public SearchReferenceHandler[] getReferenceHandlers() { + return referenceHandlers; + } + + + public void setReferenceHandlers(final SearchReferenceHandler... handlers) { + referenceHandlers = handlers; + } + + + public SearchResultHandler[] getSearchResultHandlers() { + return searchResultHandlers; + } + + + public void setSearchResultHandlers(final SearchResultHandler... handlers) { + searchResultHandlers = handlers; + } + + + /** + * Returns the flag that is used on the extended dn control. + * + * @return extended dn control flag + */ + public ExtendedDnControl.Flag getExtendedDnFlag() { + return extendedDnFlag; + } + + + /** + * Sets the flag to use on the extended dn control. + * + * @param flag to set on the extended dn control + */ + public void setExtendedDnFlag(final ExtendedDnControl.Flag flag) { + extendedDnFlag = flag; + } + + + /** + * Performs a search operation with the {@link DirSyncControl}. The supplied request is modified in the following way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * DirSyncControl}, {@link ShowDeletedControl}, and {@link ExtendedDnControl}
  • + *
+ * + * @param request search request to execute + * @return search operation response + * @throws LdapException if the search fails + */ + public SearchResponse execute(final SearchRequest request) + throws LdapException { + return execute(request, new DefaultCookieManager()); + } + + + /** + * Performs a search operation with the {@link DirSyncControl}. The supplied request is modified in the following way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * DirSyncControl}, {@link ShowDeletedControl}, and {@link ExtendedDnControl}
  • + *
+ * + *

The cookie is extracted from the supplied response and replayed in the request.

+ * + * @param request search request to execute + * @param result of a previous dir sync operation + * @return search operation response + * @throws IllegalArgumentException if the response does not contain a dir sync cookie + * @throws LdapException if the search fails + */ + public SearchResponse execute(final SearchRequest request, final SearchResponse result) + throws LdapException { + final byte[] cookie = getDirSyncCookie(result); + if (cookie == null) { + throw new IllegalArgumentException("Response does not contain a dir sync cookie"); + } + + return execute(request, new DefaultCookieManager(cookie)); + } + + + /** + * Performs a search operation with the {@link DirSyncControl}. The supplied request is modified in the following way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * DirSyncControl}, {@link ShowDeletedControl}, and {@link ExtendedDnControl}
  • + *
+ * + *

The cookie used in the request is read from the cookie manager and written to the cookie manager after a + * successful search, if the response contains a cookie.

+ * + * @param request search request to execute + * @param manager for reading and writing cookies + * @return search operation response + * @throws LdapException if the search fails + */ + public SearchResponse execute(final SearchRequest request, final CookieManager manager) + throws LdapException { + request.setControls(createRequestControls(manager.readCookie())); + final SearchOperation search = createSearchOperation(); + final SearchResponse result = search.execute(request); + final byte[] cookie = getDirSyncCookie(result); + if (cookie != null) { + manager.writeCookie(cookie); + } + return result; + } + + + /** + * Returns whether {@link #execute(SearchRequest, SearchResponse)} can be invoked again. + * + * @param result of a previous dir sync operation + * @return whether more dir sync results can be retrieved from the server + */ + public boolean hasMore(final SearchResponse result) { + return getDirSyncFlags(result) != 0; + } + + + /** + * Invokes {@link #execute(SearchRequest, CookieManager)} with a {@link DefaultCookieManager}. + * + * @param request search request to execute + * @return search operation response of the last dir sync operation + * @throws LdapException if the search fails + */ + public SearchResponse executeToCompletion(final SearchRequest request) + throws LdapException { + return executeToCompletion(request, new DefaultCookieManager()); + } + + + /** + * Performs a search operation with the {@link DirSyncControl}. The supplied request is modified in the following way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * DirSyncControl}, {@link ShowDeletedControl}, and {@link ExtendedDnControl}
  • + *
+ * + *

This method will continue to execute search operations until all dir sync search results have been retrieved + * from the server. The returned response contains the response data of the last dir sync operation plus the entries + * and references returned by all previous search operations.

+ * + *

The cookie used for each request is read from the cookie manager and written to the cookie manager after a + * successful search, if the response contains a cookie.

+ * + * @param request search request to execute + * @param manager for reading and writing cookies + * @return search operation response of the last dir sync operation + * @throws LdapException if the search fails + */ + public SearchResponse executeToCompletion(final SearchRequest request, final CookieManager manager) + throws LdapException { + SearchResponse response = null; + final SearchResponse combinedResponse = new SearchResponse(); + final SearchOperation search = createSearchOperation(); + byte[] cookie = manager.readCookie(); + long flags; + do { + if (response != null) { + combinedResponse.addEntries(response.getEntries()); + combinedResponse.addReferences(response.getReferences()); + } + request.setControls(createRequestControls(cookie)); + response = search.execute(request); + flags = getDirSyncFlags(response); + cookie = getDirSyncCookie(response); + if (cookie != null) { + manager.writeCookie(cookie); + } + } while (flags != 0); + response.addEntries(combinedResponse.getEntries()); + response.addReferences(combinedResponse.getReferences()); + return response; + } + + + /** + * Creates a new search operation configured with the properties on this client. + * + * @return new search operation + */ + protected SearchOperation createSearchOperation() { + final SearchOperation search = new SearchOperation(factory); + search.setResultHandlers(resultHandlers); + search.setExceptionHandler(exceptionHandler); + search.setThrowCondition(throwCondition); + search.setEntryHandlers(entryHandlers); + search.setReferenceHandlers(referenceHandlers); + search.setSearchResultHandlers(searchResultHandlers); + return search; + } + + + /** + * Returns the dir sync flags in the supplied response or -1 if no flags exists. + * + * @param result of a previous dir sync operation + * @return dir sync flags or -1 + */ + protected long getDirSyncFlags(final SearchResponse result) { + long flags = -1; + final DirSyncControl ctl = (DirSyncControl) result.getControl(DirSyncControl.OID); + if (ctl != null) { + flags = ctl.getFlags(); + } + return flags; + } + + + /** + * Returns the dir sync cookie in the supplied response or null if no cookie exists. + * + * @param result of a previous dir sync operation + * @return dir sync cookie or null + */ + protected byte[] getDirSyncCookie(final SearchResponse result) { + byte[] cookie = null; + final DirSyncControl ctl = (DirSyncControl) result.getControl(DirSyncControl.OID); + if (ctl != null) { + if (ctl.getCookie() != null && ctl.getCookie().length > 0) { + cookie = ctl.getCookie(); + } + } + return cookie; + } + + + /** + * Returns the list of request controls configured for this client. + * + * @param cookie to add to the dir sync control or null + * @return search request controls + */ + private RequestControl[] createRequestControls(final byte[] cookie) { + return + new RequestControl[]{ + new DirSyncControl(dirSyncFlags, cookie, maxAttributeCount, true), + new ExtendedDnControl(extendedDnFlag), + new ShowDeletedControl(), + }; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/util/NotificationClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/util/NotificationClient.java new file mode 100644 index 0000000..ddf2ea1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/control/util/NotificationClient.java @@ -0,0 +1,245 @@ + +package org.xbib.net.ldap.ad.control.util; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchOperationHandle; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.ad.control.NotificationControl; + +/** + * Client that simplifies using the notification control. + * + */ +public class NotificationClient { + + /** + * Connection factory to get a connection from. + */ + private final ConnectionFactory factory; + + /** + * Search operation handle. + */ + private SearchOperationHandle handle; + + + /** + * Creates a new notification client. + * + * @param cf to get a connection from + */ + public NotificationClient(final ConnectionFactory cf) { + factory = cf; + } + + + /** + * Invokes {@link #execute(SearchRequest, int)} with a capacity of {@link Integer#MAX_VALUE}. + * + * @param request search request to execute + * @return blocking queue to wait for search entries + * @throws LdapException if the search fails + */ + public BlockingQueue execute(final SearchRequest request) + throws LdapException { + return execute(request, Integer.MAX_VALUE); + } + + + /** + * Performs a search operation with the {@link NotificationControl}. The supplied request is modified in the following + * way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * NotificationControl}
  • + *
+ * + *

The search request object should not be reused for any other search operations.

+ * + * @param request search request to execute + * @param capacity of the returned blocking queue + * @return blocking queue to wait for search entries + * @throws LdapException if the search fails + */ + public BlockingQueue execute(final SearchRequest request, final int capacity) + throws LdapException { + final BlockingQueue queue = new LinkedBlockingQueue<>(capacity); + + request.setControls(new NotificationControl()); + final SearchOperation search = new SearchOperation(factory, request); + search.setResultHandlers(result -> { + try { + queue.put(new NotificationItem(result)); + } catch (InterruptedException e) { + // + } + }); + search.setExceptionHandler(e -> { + try { + queue.put(new NotificationItem(e)); + } catch (InterruptedException ex) { + // + } + }); + search.setEntryHandlers(entry -> { + try { + queue.put(new NotificationItem(entry)); + } catch (InterruptedException e) { + // + } + return entry; + }); + + handle = search.send(); + return queue; + } + + + /** + * Invokes an abandon operation on the last invocation of {@link #execute(SearchRequest, int)}. + */ + public void abandon() { + handle.abandon(); + } + + + /** + * Contains data returned when using the notification control. + */ + public static class NotificationItem { + + /** + * Entry contained in this notification item. + */ + private final LdapEntry searchEntry; + + /** + * Result contained in this notification item. + */ + private final Result searchResult; + + /** + * Exception thrown by the search operation. + */ + private final Exception searchException; + + + /** + * Creates a new notification item. + * + * @param entry that represents this item + */ + public NotificationItem(final LdapEntry entry) { + searchEntry = entry; + searchResult = null; + searchException = null; + } + + + /** + * Creates a new notification item. + * + * @param result that represents this item + */ + public NotificationItem(final Result result) { + searchEntry = null; + searchResult = result; + searchException = null; + } + + + /** + * Creates a new notification item. + * + * @param exception that represents this item + */ + public NotificationItem(final Exception exception) { + searchEntry = null; + searchResult = null; + searchException = exception; + } + + + /** + * Returns whether this item represents a search entry. + * + * @return whether this item represents a search entry + */ + public boolean isEntry() { + return searchEntry != null; + } + + + /** + * Returns the search entry contained in this item or null if this item does not contain a search entry. + * + * @return search entry + */ + public LdapEntry getEntry() { + return searchEntry; + } + + + /** + * Returns whether this item represents a response. + * + * @return whether this item represents a response + */ + public boolean isResult() { + return searchResult != null; + } + + + /** + * Returns the response contained in this item or null if this item does not contain a response. + * + * @return response + */ + public Result getResult() { + return searchResult; + } + + + /** + * Returns whether this item represents an exception. + * + * @return whether this item represents an exception + */ + public boolean isException() { + return searchException != null; + } + + + /** + * Returns the exception contained in this item or null if this item does not contain an exception. + * + * @return exception + */ + public Exception getException() { + return searchException; + } + + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("[").append(getClass().getName()).append("@").append(hashCode()); + if (isEntry()) { + sb.append("::searchEntry=").append(searchEntry).append("]"); + } else if (isResult()) { + sb.append("::searchResult=").append(searchResult).append("]"); + } else if (isException()) { + sb.append("::syncReplException=").append(searchException).append("]"); + } else { + sb.append("]"); + } + return sb.toString(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/extended/FastBindConnectionInitializer.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/extended/FastBindConnectionInitializer.java new file mode 100644 index 0000000..93c94e1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/extended/FastBindConnectionInitializer.java @@ -0,0 +1,21 @@ + +package org.xbib.net.ldap.ad.extended; + +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionInitializer; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Result; + +/** + * Initializes a connection by performing a fast bind operation. + * + */ +public class FastBindConnectionInitializer implements ConnectionInitializer { + + + @Override + public Result initialize(final Connection c) + throws LdapException { + return c.operation(new FastBindRequest()).execute(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/extended/FastBindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/extended/FastBindRequest.java new file mode 100644 index 0000000..336cc0f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/extended/FastBindRequest.java @@ -0,0 +1,32 @@ + +package org.xbib.net.ldap.ad.extended; + +import org.xbib.net.ldap.extended.ExtendedRequest; + +/** + * LDAP fast bind request defined as: + * + *
+ * ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
+ * requestName      [0] LDAPOID,
+ * requestValue     [1] OCTET STRING OPTIONAL }
+ * 
+ *

+ * where the request value is absent. + * + */ +public class FastBindRequest extends ExtendedRequest { + + /** + * OID of this extended request. + */ + public static final String OID = "1.2.840.113556.1.4.1781"; + + + /** + * Default constructor. + */ + public FastBindRequest() { + super(OID); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/AbstractBinaryAttributeHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/AbstractBinaryAttributeHandler.java new file mode 100644 index 0000000..e389745 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/AbstractBinaryAttributeHandler.java @@ -0,0 +1,110 @@ + +package org.xbib.net.ldap.ad.handler; + +import java.util.stream.Stream; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.handler.AbstractEntryHandler; + +/** + * Base class for entry handlers that convert a binary attribute to its string form. + * + * @param type of object to handle + */ +public abstract class AbstractBinaryAttributeHandler extends AbstractEntryHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1847; + + /** + * attribute name. + */ + private String attributeName; + + + /** + * Returns the attribute name to convert from binary to string. + * + * @return attribute name + */ + public String getAttributeName() { + return attributeName; + } + + + /** + * Sets the attribute name to convert from binary to string. + * + * @param name of the attribute + */ + public void setAttributeName(final String name) { + attributeName = name; + } + + + @Override + protected void handleAttributes(final LdapEntry entry) { + for (LdapAttribute la : entry.getAttributes()) { + if (attributeName.equalsIgnoreCase(la.getName())) { + if (la.isBinary()) { + final LdapAttribute newAttr = new LdapAttribute(); + newAttr.setName(la.getName()); + for (byte[] b : la.getBinaryValues()) { + newAttr.addStringValues(convertValue(b)); + } + entry.addAttributes(newAttr); + handleAttribute(newAttr); + } else { + handleAttribute(la); + } + } else { + handleAttribute(la); + } + } + } + + + /** + * Converts the supplied binary value to its string form. + * + * @param value to convert + * @return string form of the value + */ + protected abstract String convertValue(byte[] value); + + + @Override + public void setRequest(final SearchRequest request) { + final String[] binaryAttrs = request.getBinaryAttributes(); + if (binaryAttrs != null) { + final boolean isAttrSet = Stream.of(binaryAttrs).anyMatch(a -> attributeName.equalsIgnoreCase(a)); + if (!isAttrSet) { + request.setBinaryAttributes(LdapUtils.concatArrays(binaryAttrs, new String[]{attributeName})); + } + } else { + request.setBinaryAttributes(attributeName); + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AbstractBinaryAttributeHandler v) { + return LdapUtils.areEqual(attributeName, v.attributeName); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, attributeName); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/ObjectGuidHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/ObjectGuidHandler.java new file mode 100644 index 0000000..70c1f6c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/ObjectGuidHandler.java @@ -0,0 +1,70 @@ + +package org.xbib.net.ldap.ad.handler; + +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ad.GlobalIdentifier; +import org.xbib.net.ldap.handler.LdapEntryHandler; + +/** + * Processes an objectGuid attribute by converting it from binary to its string form. + * + */ +public class ObjectGuidHandler extends AbstractBinaryAttributeHandler implements LdapEntryHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1823; + + /** + * objectGuid attribute name. + */ + private static final String ATTRIBUTE_NAME = "objectGUID"; + + + /** + * Creates a new object guid handler. + */ + public ObjectGuidHandler() { + setAttributeName(ATTRIBUTE_NAME); + } + + + /** + * Creates a new object guid handler. + * + * @param attrName name of the attribute which is encoded as an objectGUID + */ + public ObjectGuidHandler(final String attrName) { + setAttributeName(attrName); + } + + + @Override + public LdapEntry apply(final LdapEntry entry) { + handleEntry(entry); + return entry; + } + + + @Override + protected String convertValue(final byte[] value) { + return GlobalIdentifier.toString(value); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ObjectGuidHandler && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getAttributeName()); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/ObjectSidHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/ObjectSidHandler.java new file mode 100644 index 0000000..3e088b4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/ObjectSidHandler.java @@ -0,0 +1,71 @@ + +package org.xbib.net.ldap.ad.handler; + +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ad.SecurityIdentifier; +import org.xbib.net.ldap.handler.LdapEntryHandler; + +/** + * Processes an objectSid attribute by converting it from binary to its string form. See + * http://msdn.microsoft.com/en-us/library/windows/desktop/ms679024(v=vs.85).aspx. + * + */ +public class ObjectSidHandler extends AbstractBinaryAttributeHandler implements LdapEntryHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1801; + + /** + * objectSid attribute name. + */ + private static final String ATTRIBUTE_NAME = "objectSid"; + + + /** + * Creates a new object sid handler. + */ + public ObjectSidHandler() { + setAttributeName(ATTRIBUTE_NAME); + } + + + /** + * Creates a new object sid handler. + * + * @param attrName name of the attribute which is encoded as an objectSid + */ + public ObjectSidHandler(final String attrName) { + setAttributeName(attrName); + } + + + @Override + public LdapEntry apply(final LdapEntry entry) { + handleEntry(entry); + return entry; + } + + + @Override + protected String convertValue(final byte[] value) { + return SecurityIdentifier.toString(value); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ObjectSidHandler && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getAttributeName()); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/PrimaryGroupIdHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/PrimaryGroupIdHandler.java new file mode 100644 index 0000000..26a07ba --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/PrimaryGroupIdHandler.java @@ -0,0 +1,149 @@ + +package org.xbib.net.ldap.ad.handler; + +import org.xbib.net.ldap.FilterTemplate; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ReturnAttributes; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.ad.SecurityIdentifier; +import org.xbib.net.ldap.handler.AbstractEntryHandler; +import org.xbib.net.ldap.handler.SearchResultHandler; + +/** + * Constructs the primary group SID and then searches for that group and puts its DN in the 'memberOf' attribute of the + * original search entry. This handler requires that entries contain both the 'objectSid' and 'primaryGroupID' + * attributes. If those attributes are not found this handler is a no-op. This handler should be used in conjunction + * with the {@link ObjectSidHandler} to ensure the 'objectSid' attribute is in the proper form. See + * http://support2.microsoft.com/kb/297951 + *

+ * This handler should only be used with the {@link org.xbib.net.ldap.SearchOperation#execute()} method since it leverages + * the connection to make further searches. + * + */ +public class PrimaryGroupIdHandler extends AbstractEntryHandler implements SearchResultHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1831; + + /** + * search filter used to find the primary group. + */ + private String groupFilter = "(&(objectClass=group)(objectSid={0}))"; + + /** + * base DN used for searching for the primary group. + */ + private String baseDn; + + + /** + * Returns the search filter used to find the primary group. + * + * @return group search filter + */ + public String getGroupFilter() { + return groupFilter; + } + + + /** + * Sets the search filter used to find the primary group. + * + * @param filter search filter + */ + public void setGroupFilter(final String filter) { + groupFilter = filter; + } + + + /** + * Returns the base DN to search for the primary group. If this is not set the base DN from the original search is + * used. + * + * @return base DN to search for the primary group + */ + public String getBaseDn() { + return baseDn; + } + + + /** + * Sets the base DN to search for the primary group. If this is not set the base DN from the original search is used. + * + * @param dn base DN + */ + public void setBaseDn(final String dn) { + baseDn = dn; + } + + + @Override + public SearchResponse apply(final SearchResponse response) { + response.getEntries().forEach(this::handleEntry); + return response; + } + + + @Override + protected void handleAttributes(final LdapEntry entry) { + final LdapAttribute objectSid = entry.getAttribute("objectSid"); + final LdapAttribute primaryGroupId = entry.getAttribute("primaryGroupID"); + + if (objectSid != null && primaryGroupId != null) { + final String sid; + if (objectSid.isBinary()) { + sid = SecurityIdentifier.toString(objectSid.getBinaryValue()); + } else { + sid = objectSid.getStringValue(); + } + + final String groupSid = sid.substring(0, sid.lastIndexOf('-') + 1) + primaryGroupId.getStringValue(); + + try { + final SearchRequest sr = SearchRequest.builder() + .dn(baseDn != null ? baseDn : getRequest().getBaseDn()) + .returnAttributes(ReturnAttributes.NONE.value()) + .filter(new FilterTemplate(groupFilter, new Object[]{groupSid}).format()) + .build(); + + final SearchResponse result = getConnection().operation(sr).execute(); + if (!result.isSuccess() || result.entrySize() == 0) { + } else { + LdapAttribute memberOf = entry.getAttribute("memberOf"); + if (memberOf == null) { + memberOf = new LdapAttribute("memberOf"); + entry.addAttributes(memberOf); + } + memberOf.addStringValues(result.getEntry().getDn()); + } + } catch (LdapException e) { + // + } + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof PrimaryGroupIdHandler v) { + return LdapUtils.areEqual(groupFilter, v.groupFilter) && + LdapUtils.areEqual(baseDn, v.baseDn); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, groupFilter, baseDn); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/RangeEntryHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/RangeEntryHandler.java new file mode 100644 index 0000000..cebe916 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/handler/RangeEntryHandler.java @@ -0,0 +1,155 @@ + +package org.xbib.net.ldap.ad.handler; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.handler.AbstractEntryHandler; +import org.xbib.net.ldap.handler.SearchResultHandler; + +/** + * Rewrites attributes returned from Active Directory to include all values by performing additional searches. This + * behavior is based on the expired RFC "Incremental Retrieval of Multi-valued Properties" + * http://www.ietf.org/proceedings/53/I-D/draft-kashi-incremental-00.txt. + * + *

For example, when the membership of a group exceeds 1500, requests for the member attribute will likely return an + * attribute with name "member;Range=0-1499" and 1500 values. For a group with just over 3000 members, subsequent + * searches will request "member;Range=1500-2999" and then "member;Range=3000-4499". When the returned attribute is of + * the form "member;Range=3000-*", all values have been retrieved.

+ *

+ * This handler should only be used with the {@link org.xbib.net.ldap.SearchOperation#execute()} method since it leverages + * the connection to make further searches. + * + * @author Tom Zeller + */ +public class RangeEntryHandler extends AbstractEntryHandler implements SearchResultHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 839; + + /** + * The character indicating that the end of the range has been reached. + */ + private static final String END_OF_RANGE = "*"; + + /** + * The format used to calculate attribute IDs for subsequent searches. + */ + private static final String RANGE_FORMAT = "%1$s;Range=%2$s-%3$s"; + + /** + * The expression matching the range attribute ID "<id>range=<X>-<Y>". + */ + private static final String RANGE_PATTERN_STRING = "^(.*?);Range=([\\d\\*]+)-([\\d\\*]+)"; + + /** + * The pattern matching the range attribute ID. + */ + private static final Pattern RANGE_PATTERN = Pattern.compile(RANGE_PATTERN_STRING, Pattern.CASE_INSENSITIVE); + + + @Override + public SearchResponse apply(final SearchResponse response) { + response.getEntries().forEach(this::handleEntry); + return response; + } + + + @Override + protected void handleAttributes(final LdapEntry entry) { + final Map matchingAttrs = new HashMap<>(); + for (LdapAttribute la : entry.getAttributes()) { + // Match attribute ID against the pattern + final Matcher matcher = RANGE_PATTERN.matcher(la.getName()); + + // If the attribute ID matches the pattern + if (matcher.find()) { + matchingAttrs.put(la, matcher); + } + } + + for (Map.Entry mEntry : matchingAttrs.entrySet()) { + final LdapAttribute la = mEntry.getKey(); + final Matcher matcher = mEntry.getValue(); + final String msg = String.format("attribute '%s' entry '%s'", la.getName(), entry.getDn()); + + // Determine the attribute name without the range syntax + final String attrTypeName = matcher.group(1); + if (attrTypeName == null || attrTypeName.isEmpty()) { + throw new IllegalArgumentException("Unable to determine the attribute type name for " + msg); + } + + // Create or update the attribute whose ID has the range syntax removed + LdapAttribute newAttr = entry.getAttribute(attrTypeName); + if (newAttr == null) { + newAttr = new LdapAttribute(); + newAttr.setBinary(la.isBinary()); + newAttr.setName(attrTypeName); + entry.addAttributes(newAttr); + } + + // Copy values + if (la.isBinary()) { + newAttr.addBinaryValues(la.getBinaryValues()); + } else { + newAttr.addStringValues(la.getStringValues()); + } + + // Remove original attribute with range syntax from returned attributes + entry.removeAttributes(la); + + // If the attribute ID ends with * we're done, otherwise increment + if (!la.getName().endsWith(END_OF_RANGE)) { + + // Determine next attribute ID + // CheckStyle:MagicNumber OFF + final int start = Integer.parseInt(matcher.group(2)); + final int end = Integer.parseInt(matcher.group(3)); + // CheckStyle:MagicNumber ON + final int diff = end - start; + final String nextAttrID = String.format(RANGE_FORMAT, attrTypeName, end + 1, end + diff + 1); + + // Search for next increment of values + try { + final SearchRequest sr = SearchRequest.objectScopeSearchRequest(entry.getDn(), new String[]{nextAttrID}); + final SearchResponse result = getConnection().operation(sr).execute(); + + // Add all attributes to the search result + if (!result.isSuccess() || result.entrySize() == 0) { + } else { + entry.addAttributes(result.getEntry().getAttributes()); + } + } catch (LdapException e) { + // + } + + // Iterate + handleAttributes(entry); + } + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof RangeEntryHandler; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, (Object[]) null); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/schema/SchemaFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/schema/SchemaFactory.java new file mode 100644 index 0000000..25aebc8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/schema/SchemaFactory.java @@ -0,0 +1,226 @@ + +package org.xbib.net.ldap.ad.schema; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ReturnAttributes; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.control.util.PagedResultsClient; +import org.xbib.net.ldap.io.LdifReader; +import org.xbib.net.ldap.schema.AttributeType; +import org.xbib.net.ldap.schema.ObjectClass; +import org.xbib.net.ldap.schema.ObjectClassType; +import org.xbib.net.ldap.schema.Schema; + +/** + * Factory to create {@link Schema} from an active directory schema search result. Active Directory does not adhere to + * RFC 4512 to represent its schema. Each schema element is represented with a separate LDAP entry. The factory parses + * and sets the object classes and attribute types for the schema. The other properties on the schema object are not + * available. + */ +public final class SchemaFactory { + + + /** + * Default constructor. + */ + private SchemaFactory() { + } + + + /** + * Creates a new schema. The input stream should contain the LDIF for the schema search results. + * + * @param is containing the schema ldif + * @return schema + * @throws IOException if an error occurs reading the input stream + */ + public static Schema createSchema(final InputStream is) + throws IOException { + final LdifReader reader = new LdifReader(new InputStreamReader(is)); + return createSchema(reader.read()); + } + + + /** + * Creates a new schema. The entryDn is searched to obtain the schema. + * + * @param factory to obtain an LDAP connection from + * @param entryDn the schema entries + * @return schema + * @throws LdapException if the search fails + */ + public static Schema createSchema(final ConnectionFactory factory, final String entryDn) + throws LdapException { + return createSchema(getSearchResult(factory, entryDn, "(objectClass=*)", ReturnAttributes.ALL.value())); + } + + + /** + * Creates a new schema. The schema result should contain entries with the 'attributeSchema' and 'classSchema' + * objectClasses. + * + * @param schemaResult containing the schema entries + * @return schema + */ + public static Schema createSchema(final SearchResponse schemaResult) { + final Set attributeTypes = new HashSet<>(); + final Set objectClasses = new HashSet<>(); + for (LdapEntry entry : schemaResult.getEntries()) { + final LdapAttribute la = entry.getAttribute("objectClass"); + if (la != null && la.getStringValues().contains("attributeSchema")) { + attributeTypes.add(createAttributeType(entry)); + } + if (la != null && la.getStringValues().contains("classSchema")) { + objectClasses.add(createObjectClass(entry)); + } + } + + final Schema schema = new Schema(); + schema.setAttributeTypes(attributeTypes); + schema.setObjectClasses(objectClasses); + return schema; + } + + + /** + * Searches for the supplied dn and returns its ldap entry. This methods uses the paged results search control as + * schema entries typically number beyond the server search size limit. + * + * @param factory to obtain an LDAP connection from + * @param dn to search for + * @param filter to search with + * @param retAttrs attributes to return + * @return ldap entry + * @throws LdapException if the search fails + */ + private static SearchResponse getSearchResult( + final ConnectionFactory factory, + final String dn, + final String filter, + final String[] retAttrs) + throws LdapException { + final PagedResultsClient client = new PagedResultsClient(factory, 100); + return client.executeToCompletion( + SearchRequest.builder() + .dn(dn).filter(filter).returnAttributes(retAttrs).build()); + } + + + /** + * Creates an attribute type from the supplied ldap entry. The entry must contain an objectClass of 'attributeSchema'. + * This method only populates the OID, names, description, syntax, and single valued properties of the attribute type. + * + * @param entry containing an attribute schema + * @return attribute type + */ + private static AttributeType createAttributeType(final LdapEntry entry) { + final LdapAttribute la = entry.getAttribute("objectClass"); + if (la == null || !la.getStringValues().contains("attributeSchema")) { + throw new IllegalArgumentException("Entry is not an attribute schema"); + } + return + new AttributeType( + getAttributeValue(entry, "attributeID"), + getAttributeValues(entry, "lDAPDisplayName", "adminDisplayName", "name"), + getAttributeValue(entry, "adminDescription"), + false, + null, + null, + null, + null, + getAttributeValue(entry, "attributeSyntax"), + Boolean.parseBoolean(getAttributeValue(entry, "isSingleValued")), + false, + false, + null, + null); + } + + + /** + * Creates an object class from the supplied ldap entry. The entry must contain an objectClass of 'classSchema'. This + * method only populates the OID, names, description, superior classes, object class type, required attributes, and + * optional attributes of the object class. + * + * @param entry containing a class schema + * @return object class + */ + private static ObjectClass createObjectClass(final LdapEntry entry) { + final LdapAttribute la = entry.getAttribute("objectClass"); + if (la == null || !la.getStringValues().contains("classSchema")) { + throw new IllegalArgumentException("Entry is not an object class"); + } + + ObjectClassType ocType = null; + final String ocCategory = getAttributeValue(entry, "objectClassCategory"); + if (ocCategory != null) { + for (ObjectClassType type : ObjectClassType.values()) { + if (type.ordinal() == Integer.parseInt(ocCategory)) { + ocType = type; + break; + } + } + } + return + new ObjectClass( + getAttributeValue(entry, "governsID"), + getAttributeValues(entry, "lDAPDisplayName", "adminDisplayName", "name"), + getAttributeValue(entry, "adminDescription"), + false, + getAttributeValues(entry, "possSuperiors", "systemPossSuperiors"), + ocType, + getAttributeValues(entry, "mustContain", "systemMustContain"), + getAttributeValues(entry, "mayContain", "systemMayContain"), + null); + } + + + /** + * Returns a single value for the first attribute name found in the supplied entry. + * + * @param entry containing the attributes + * @param names to search for in the entry + * @return single attribute value + */ + private static String getAttributeValue(final LdapEntry entry, final String... names) { + String value = null; + for (String name : names) { + final LdapAttribute la = entry.getAttribute(name); + if (la != null) { + value = la.getStringValue(); + break; + } + } + return value; + } + + + /** + * Returns the values for the first attribute name found in the supplied entry. + * + * @param entry containing the attributes + * @param names to search for in the entry + * @return attribute values + */ + private static String[] getAttributeValues(final LdapEntry entry, final String... names) { + Collection values = null; + for (String name : names) { + final LdapAttribute la = entry.getAttribute(name); + if (la != null) { + values = la.getStringValues(); + break; + } + } + return values != null ? values.toArray(new String[0]) : null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/DeltaTimeValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/DeltaTimeValueTranscoder.java new file mode 100644 index 0000000..a015be8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/DeltaTimeValueTranscoder.java @@ -0,0 +1,34 @@ + +package org.xbib.net.ldap.ad.transcode; + +import org.xbib.net.ldap.transcode.AbstractStringValueTranscoder; + +/** + * Decodes and encodes an active directory delta time value for use in an ldap attribute value. + * + */ +public class DeltaTimeValueTranscoder extends AbstractStringValueTranscoder { + + /** + * Delta time uses 100-nanosecond intervals. For conversion purposes this is 1x10^6 / 100. + */ + private static final long ONE_HUNDRED_NANOSECOND_INTERVAL = 10000L; + + + @Override + public Long decodeStringValue(final String value) { + return -Long.parseLong(value) / ONE_HUNDRED_NANOSECOND_INTERVAL; + } + + + @Override + public String encodeStringValue(final Long value) { + return String.valueOf(-value * ONE_HUNDRED_NANOSECOND_INTERVAL); + } + + + @Override + public Class getType() { + return Long.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/FileTimeValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/FileTimeValueTranscoder.java new file mode 100644 index 0000000..1bb9cb3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/FileTimeValueTranscoder.java @@ -0,0 +1,43 @@ + +package org.xbib.net.ldap.ad.transcode; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import org.xbib.net.ldap.transcode.AbstractStringValueTranscoder; + +/** + * Decodes and encodes an active directory file time value for use in an ldap attribute value. + * + */ +public class FileTimeValueTranscoder extends AbstractStringValueTranscoder { + + /** + * Number of milliseconds between standard Unix era (1/1/1970) and filetime start (1/1/1601). + */ + private static final long ERA_OFFSET = 11644473600000L; + + /** + * File time uses 100-nanosecond intervals. For conversion purposes this is 1x10^6 / 100. + */ + private static final long ONE_HUNDRED_NANOSECOND_INTERVAL = 10000L; + + + @Override + public ZonedDateTime decodeStringValue(final String value) { + final Instant i = Instant.ofEpochMilli(Long.parseLong(value) / ONE_HUNDRED_NANOSECOND_INTERVAL - ERA_OFFSET); + return ZonedDateTime.ofInstant(i, ZoneId.of("Z")); + } + + + @Override + public String encodeStringValue(final ZonedDateTime value) { + return String.valueOf((value.toInstant().toEpochMilli() + ERA_OFFSET) * ONE_HUNDRED_NANOSECOND_INTERVAL); + } + + + @Override + public Class getType() { + return ZonedDateTime.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/UnicodePwdValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/UnicodePwdValueTranscoder.java new file mode 100644 index 0000000..3aedf5d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ad/transcode/UnicodePwdValueTranscoder.java @@ -0,0 +1,39 @@ + +package org.xbib.net.ldap.ad.transcode; + +import java.nio.charset.StandardCharsets; +import org.xbib.net.ldap.transcode.AbstractBinaryValueTranscoder; + +/** + * Decodes and encodes an active directory unicodePwd value for use in an ldap attribute value. + * + */ +public class UnicodePwdValueTranscoder extends AbstractBinaryValueTranscoder { + + + @Override + public String decodeBinaryValue(final byte[] value) { + final String pwd = new String(value, StandardCharsets.UTF_16LE); + if (pwd.length() < 2) { + throw new IllegalArgumentException("unicodePwd must be at least 2 characters long"); + } + return pwd.substring(1, pwd.length() - 1); + } + + + @Override + public byte[] encodeBinaryValue(final String value) { + if (value == null) { + throw new IllegalArgumentException("Cannot encode null value"); + } + + final String pwd = String.format("\"%s\"", value); + return pwd.getBytes(StandardCharsets.UTF_16LE); + } + + + @Override + public Class getType() { + return String.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AbstractAuthenticationHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AbstractAuthenticationHandler.java new file mode 100644 index 0000000..b67dd50 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AbstractAuthenticationHandler.java @@ -0,0 +1,112 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.ConnectionFactoryManager; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Base class for an LDAP authentication implementations. + * + */ +public abstract class AbstractAuthenticationHandler implements AuthenticationHandler, ConnectionFactoryManager { + + /** + * Connection factory. + */ + private ConnectionFactory factory; + + /** + * controls used by this handler. + */ + private RequestControl[] authenticationControls; + + + @Override + public ConnectionFactory getConnectionFactory() { + return factory; + } + + + @Override + public void setConnectionFactory(final ConnectionFactory cf) { + factory = cf; + } + + + /** + * Returns the controls for this authentication handler. + * + * @return controls + */ + public RequestControl[] getAuthenticationControls() { + return authenticationControls; + } + + + /** + * Sets the controls for this authentication handler. + * + * @param cntrls controls to set + */ + public void setAuthenticationControls(final RequestControl... cntrls) { + authenticationControls = cntrls; + } + + + @Override + public AuthenticationHandlerResponse authenticate(final AuthenticationCriteria ac) + throws LdapException { + final AuthenticationHandlerResponse response; + final Connection conn = factory.getConnection(); + boolean closeConn = false; + try { + conn.open(); + response = authenticateInternal(conn, ac); + } catch (Exception e) { + closeConn = true; + throw e; + } finally { + if (closeConn) { + conn.close(); + } + } + return response; + } + + + /** + * Authenticate on the supplied connection using the supplied criteria. + * + * @param c to authenticate on + * @param criteria criteria to authenticate with + * @return authentication handler response + * @throws LdapException if the authentication fails + */ + protected abstract AuthenticationHandlerResponse authenticateInternal(Connection c, AuthenticationCriteria criteria) + throws LdapException; + + + /** + * Combines request controls in the {@link AuthenticationRequest} with {@link #authenticationControls}. + * + * @param criteria containing request controls + * @return combined request controls or null + */ + protected RequestControl[] processRequestControls(final AuthenticationCriteria criteria) { + final RequestControl[] ctls; + if (criteria.getAuthenticationRequest().getControls() != null) { + if (getAuthenticationControls() != null) { + ctls = LdapUtils.concatArrays(criteria.getAuthenticationRequest().getControls(), getAuthenticationControls()); + } else { + ctls = criteria.getAuthenticationRequest().getControls(); + } + } else { + ctls = getAuthenticationControls(); + } + return ctls; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AbstractSearchEntryResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AbstractSearchEntryResolver.java new file mode 100644 index 0000000..f884e0e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AbstractSearchEntryResolver.java @@ -0,0 +1,289 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Iterator; +import org.xbib.net.ldap.AbstractSearchOperationFactory; +import org.xbib.net.ldap.DerefAliases; +import org.xbib.net.ldap.FilterTemplate; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.SearchScope; + +/** + * Base implementation for search entry resolvers. Uses an object level search on the {@link + * AuthenticationCriteria#getDn()} if no {@link #userFilter} is configured. If a {@link #userFilter} is configured, then + * a search is executed using that filter. + * + */ +public abstract class AbstractSearchEntryResolver extends AbstractSearchOperationFactory implements EntryResolver { + + /** + * DN to search. + */ + private String baseDn = ""; + + /** + * Filter for searching for the user. + */ + private String userFilter; + + /** + * Filter parameters for searching for the user. + */ + private Object[] userFilterParameters; + + /** + * Whether to throw an exception if multiple entries are found. + */ + private boolean allowMultipleEntries; + + /** + * Whether to use a subtree search when resolving DNs. + */ + private boolean subtreeSearch; + + /** + * How to handle aliases. + */ + private DerefAliases derefAliases = DerefAliases.NEVER; + + /** + * Binary attribute names. + */ + private String[] binaryAttributes; + + + /** + * Returns the base DN. + * + * @return base DN + */ + public String getBaseDn() { + return baseDn; + } + + + /** + * Sets the base DN. + * + * @param dn base DN + */ + public void setBaseDn(final String dn) { + baseDn = dn; + } + + + /** + * Returns the filter used to search for the user. + * + * @return filter for searching + */ + public String getUserFilter() { + return userFilter; + } + + + /** + * Sets the filter used to search for the user. + * + * @param filter for searching + */ + public void setUserFilter(final String filter) { + userFilter = filter; + } + + + /** + * Returns the filter parameters used to search for the user. + * + * @return filter parameters + */ + public Object[] getUserFilterParameters() { + return userFilterParameters; + } + + + /** + * Sets the filter parameters used to search for the user. + * + * @param filterParams filter parameters + */ + public void setUserFilterParameters(final Object[] filterParams) { + userFilterParameters = filterParams; + } + + + /** + * Returns whether entry resolution should fail if multiple entries are found. + * + * @return whether an exception will be thrown if multiple entries are found + */ + public boolean getAllowMultipleEntries() { + return allowMultipleEntries; + } + + + /** + * Sets whether entry resolution should fail if multiple entries are found. If false an exception will be thrown if + * {@link #resolve(AuthenticationCriteria, AuthenticationHandlerResponse)} finds more than one entry matching its + * filter. Otherwise, the first entry found is returned. + * + * @param b whether multiple entries are allowed + */ + public void setAllowMultipleEntries(final boolean b) { + allowMultipleEntries = b; + } + + + /** + * Returns whether subtree searching will be used. + * + * @return whether the entry will be searched for over the entire base + */ + public boolean getSubtreeSearch() { + return subtreeSearch; + } + + + /** + * Sets whether subtree searching will be used. If true, the entry will be searched for over the entire {@link + * #getBaseDn()}. Otherwise the entry will be searched for in the {@link #getBaseDn()} context. + * + * @param b whether the entry will be searched for over the entire base + */ + public void setSubtreeSearch(final boolean b) { + subtreeSearch = b; + } + + + /** + * Returns how to dereference aliases. + * + * @return how to dereference aliases + */ + public DerefAliases getDerefAliases() { + return derefAliases; + } + + + /** + * Sets how to dereference aliases. + * + * @param da how to dereference aliases + */ + public void setDerefAliases(final DerefAliases da) { + derefAliases = da; + } + + + /** + * Returns names of binary attributes. + * + * @return binary attribute names + */ + public String[] getBinaryAttributes() { + return binaryAttributes; + } + + + /** + * Sets names of binary attributes. + * + * @param attrs binary attribute names + */ + public void setBinaryAttributes(final String... attrs) { + binaryAttributes = attrs; + } + + + /** + * Executes an ldap search with the supplied authentication criteria. + * + * @param criteria authentication criteria associated with the user + * @param response response from the authentication event + * @return search result + * @throws LdapException if an error occurs attempting the search + */ + protected abstract SearchResponse performLdapSearch( + AuthenticationCriteria criteria, + AuthenticationHandlerResponse response) + throws LdapException; + + + /** + * Returns a filter template using {@link #userFilter} and {@link #userFilterParameters}. {@link User#getIdentifier()} + * is injected with a named parameter of 'user', {@link User#getContext()} is injected with a named parameter of + * 'context', and {@link AuthenticationCriteria#getDn()} is injected with a named parameter of 'dn'. + * + * @param ac authentication criteria + * @return filter template + */ + protected FilterTemplate createFilterTemplate(final AuthenticationCriteria ac) { + final FilterTemplate filter = new FilterTemplate(); + if (userFilter != null) { + filter.setFilter(userFilter); + if (userFilterParameters != null) { + filter.setParameters(userFilterParameters); + } + // assign named parameters + filter.setParameter("user", ac.getAuthenticationRequest().getUser().getIdentifier()); + filter.setParameter("context", ac.getAuthenticationRequest().getUser().getContext()); + filter.setParameter("dn", ac.getDn()); + } + return filter; + } + + + /** + * Returns a search request for the supplied authentication criteria. If no {@link #userFilter} is defined then an + * object level search on the authentication criteria DN is returned. Otherwise the {@link #userFilter}, {@link + * #baseDn} and {@link #subtreeSearch} are used to create the search request. + * + * @param ac authentication criteria containing a DN + * @return search request + */ + protected SearchRequest createSearchRequest(final AuthenticationCriteria ac) { + final SearchRequest request; + if (userFilter != null) { + request = SearchRequest.builder() + .dn(baseDn) + .filter(createFilterTemplate(ac)) + .returnAttributes(ac.getAuthenticationRequest().getReturnAttributes()) + .scope(subtreeSearch ? SearchScope.SUBTREE : SearchScope.ONELEVEL) + .build(); + } else { + request = SearchRequest.objectScopeSearchRequest( + ac.getDn(), + ac.getAuthenticationRequest().getReturnAttributes()); + } + request.setDerefAliases(derefAliases); + request.setBinaryAttributes(binaryAttributes); + return request; + } + + + @Override + public LdapEntry resolve(final AuthenticationCriteria criteria, final AuthenticationHandlerResponse response) + throws LdapException { + final SearchResponse result = performLdapSearch(criteria, response); + + if (!result.isSuccess()) { + throw new LdapException( + "Error resolving entry for " + criteria.getDn() + ". Unsuccessful search response: " + result); + } + + LdapEntry entry = null; + final Iterator answer = result.getEntries().iterator(); + if (answer != null && answer.hasNext()) { + entry = answer.next(); + if (answer.hasNext()) { + if (!allowMultipleEntries) { + throw new LdapException("Found more than (1) entry for: " + criteria.getDn()); + } + } + } + return entry; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AccountState.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AccountState.java new file mode 100644 index 0000000..09d9db9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AccountState.java @@ -0,0 +1,200 @@ + +package org.xbib.net.ldap.auth; + +import java.time.ZonedDateTime; +import java.util.Arrays; +import javax.security.auth.login.LoginException; + +/** + * Represents the state of an LDAP account based on account policies for that LDAP. Note that only warning(s) or + * error(s) may be set, not both. + * + */ +public class AccountState { + + /** + * account warning. + */ + private final AccountState.Warning[] accountWarnings; + + /** + * account error. + */ + private final AccountState.Error[] accountErrors; + + + /** + * Creates a new account state. + * + * @param warnings associated with the account + */ + public AccountState(final AccountState.Warning... warnings) { + accountWarnings = warnings; + accountErrors = null; + } + + + /** + * Creates a new account state. + * + * @param errors associated with the account + */ + public AccountState(final AccountState.Error... errors) { + accountWarnings = null; + accountErrors = errors; + } + + + /** + * Returns the account state warnings. + * + * @return account state warnings + */ + public AccountState.Warning[] getWarnings() { + return accountWarnings; + } + + + /** + * Returns the first account state warning or null if no warnings exist. + * + * @return first account state warning + */ + public AccountState.Warning getWarning() { + return accountWarnings != null && accountWarnings.length > 0 ? accountWarnings[0] : null; + } + + + /** + * Returns the account state errors. + * + * @return account state errors + */ + public AccountState.Error[] getErrors() { + return accountErrors; + } + + + /** + * Returns the first account state error or null if no errors exist. + * + * @return first account state error + */ + public AccountState.Error getError() { + return accountErrors != null && accountErrors.length > 0 ? accountErrors[0] : null; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "accountWarnings=" + Arrays.toString(accountWarnings) + ", " + + "accountErrors=" + Arrays.toString(accountErrors) + "]"; + } + + + /** + * Contains error information for an account state. + */ + public interface Error { + + + /** + * Returns the error code. + * + * @return error code + */ + int getCode(); + + + /** + * Returns the error message. + * + * @return error message + */ + String getMessage(); + + + /** + * Throws the LoginException that best maps to this error. + * + * @throws LoginException for this account state error + */ + void throwSecurityException() + throws LoginException; + } + + + /** + * Contains warning information for an account state. + */ + public interface Warning { + + + /** + * Returns the expiration. + * + * @return expiration + */ + ZonedDateTime getExpiration(); + + + /** + * Returns the number of logins remaining until the account locks. + * + * @return number of logins remaining + */ + int getLoginsRemaining(); + } + + + /** + * Default warning implementation. + */ + public static class DefaultWarning implements Warning { + + /** + * expiration. + */ + private final ZonedDateTime expiration; + + /** + * number of logins remaining before the account locks. + */ + private final int loginsRemaining; + + + /** + * Creates a new warning. + * + * @param exp date of expiration + * @param remaining number of logins + */ + public DefaultWarning(final ZonedDateTime exp, final int remaining) { + expiration = exp; + loginsRemaining = remaining; + } + + + @Override + public ZonedDateTime getExpiration() { + return expiration; + } + + + @Override + public int getLoginsRemaining() { + return loginsRemaining; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "expiration=" + expiration + ", " + + "loginsRemaining=" + loginsRemaining + "]"; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AddControlAuthenticationRequestHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AddControlAuthenticationRequestHandler.java new file mode 100644 index 0000000..946dbcb --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AddControlAuthenticationRequestHandler.java @@ -0,0 +1,60 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Authentication request handler that adds {@link RequestControl}s to the {@link AuthenticationRequest}. + * + */ +public class AddControlAuthenticationRequestHandler implements AuthenticationRequestHandler { + + /** + * Factory that produces request controls. + */ + private final ControlFactory controlFactory; + + + /** + * Creates a new add control authentication request handler. + * + * @param factory to produce request controls + */ + public AddControlAuthenticationRequestHandler(final ControlFactory factory) { + controlFactory = factory; + } + + + @Override + public void handle(final String dn, final AuthenticationRequest request) + throws LdapException { + final RequestControl[] ctls = controlFactory.getControls(dn, request.getUser()); + if (ctls != null && ctls.length > 0) { + if (request.getControls() != null && request.getControls().length > 0) { + request.setControls(LdapUtils.concatArrays(request.getControls(), ctls)); + } else { + request.setControls(ctls); + } + } + } + + + /** + * Factory that produces {@link RequestControl}s. + */ + public interface ControlFactory { + + + /** + * Creates a new array of request controls. Implementations must treat the supplied parameters as unauthenticated + * data. Authentication has not been performed when this factory is invoked. + * + * @param dn distinguished name of the unauthenticated user + * @param user id of the unauthenticated user + * @return request controls + */ + RequestControl[] getControls(String dn, User user); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateAuthenticationHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateAuthenticationHandler.java new file mode 100644 index 0000000..d8cc58e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateAuthenticationHandler.java @@ -0,0 +1,112 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ResultCode; + +/** + * Used in conjunction with an {@link AggregateDnResolver} to authenticate the resolved DN. In particular, the + * resolved DN is expected to be of the form: label:DN where the label indicates the authentication handler to use. + * This class only invokes one authentication handler that matches the label found on the DN. + * + */ +public class AggregateAuthenticationHandler implements AuthenticationHandler { + + /** + * Labeled authentication handlers. + */ + private Map authenticationHandlers = new HashMap<>(); + + + /** + * Default constructor. + */ + public AggregateAuthenticationHandler() { + } + + + /** + * Creates a new aggregate authentication handler. + * + * @param handlers authentication handlers + */ + public AggregateAuthenticationHandler(final Map handlers) { + setAuthenticationHandlers(handlers); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the authentication handlers to aggregate over. + * + * @return map of label to authentication handler + */ + public Map getAuthenticationHandlers() { + return Collections.unmodifiableMap(authenticationHandlers); + } + + /** + * Sets the authentication handlers to aggregate over. + * + * @param handlers to set + */ + public void setAuthenticationHandlers(final Map handlers) { + authenticationHandlers = handlers; + } + + /** + * Adds an authentication handler with the supplied label. + * + * @param label of the resolver + * @param handler authentication handler + */ + public void addAuthenticationHandler(final String label, final AuthenticationHandler handler) { + authenticationHandlers.put(label, handler); + } + + @Override + public AuthenticationHandlerResponse authenticate(final AuthenticationCriteria criteria) + throws LdapException { + final String[] labeledDn = criteria.getDn().split(":", 2); + final AuthenticationHandler ah = authenticationHandlers.get(labeledDn[0]); + if (ah == null) { + throw new LdapException( + ResultCode.PARAM_ERROR, + "Could not find authentication handler for label: " + labeledDn[0]); + } + return ah.authenticate(new AuthenticationCriteria(labeledDn[1], criteria.getAuthenticationRequest())); + } + + // CheckStyle:OFF + public static class Builder { + + + private final AggregateAuthenticationHandler object = new AggregateAuthenticationHandler(); + + + protected Builder() { + } + + + public Builder handler(final String label, final AuthenticationHandler handler) { + object.addAuthenticationHandler(label, handler); + return this; + } + + + public AggregateAuthenticationHandler build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateAuthenticationResponseHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateAuthenticationResponseHandler.java new file mode 100644 index 0000000..ad7c0b7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateAuthenticationResponseHandler.java @@ -0,0 +1,111 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ResultCode; + +/** + * Used in conjunction with an {@link AggregateDnResolver} to execute a list of response handlers. In particular, the + * resolved DN is expected to be of the form: label:DN where the label indicates the response handler to use. This + * class only invokes the response handlers that matches the label found on the DN. + * + */ +public class AggregateAuthenticationResponseHandler implements AuthenticationResponseHandler { + + /** + * Labeled entry resolvers. + */ + private Map responseHandlers = new HashMap<>(); + + + /** + * Default constructor. + */ + public AggregateAuthenticationResponseHandler() { + } + + + /** + * Creates a new aggregate authentication response handler. + * + * @param handlers authentication response handlers + */ + public AggregateAuthenticationResponseHandler(final Map handlers) { + setAuthenticationResponseHandlers(handlers); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the response handlers to aggregate over. + * + * @return map of label to response handlers + */ + public Map getAuthenticationResponseHandlers() { + return Collections.unmodifiableMap(responseHandlers); + } + + /** + * Sets the response handlers to aggregate over. + * + * @param handlers to set + */ + public void setAuthenticationResponseHandlers(final Map handlers) { + responseHandlers = handlers; + } + + /** + * Adds an authentication response handler with the supplied label. + * + * @param label of the resolver + * @param handlers authentication response handler + */ + public void addAuthenticationResponseHandlers(final String label, final AuthenticationResponseHandler... handlers) { + responseHandlers.put(label, handlers); + } + + @Override + public void handle(final AuthenticationResponse response) throws LdapException { + final String[] labeledDn = response.getResolvedDn().split(":", 2); + final AuthenticationResponseHandler[] handlers = responseHandlers.get(labeledDn[0]); + if (handlers == null) { + throw new LdapException(ResultCode.PARAM_ERROR, "Could not find response handlers for label: " + labeledDn[0]); + } + for (AuthenticationResponseHandler ah : handlers) { + ah.handle(response); + } + } + + // CheckStyle:OFF + public static class Builder { + + + private final AggregateAuthenticationResponseHandler object = new AggregateAuthenticationResponseHandler(); + + + protected Builder() { + } + + + public Builder handler(final String label, final AuthenticationResponseHandler... handlers) { + object.addAuthenticationResponseHandlers(label, handlers); + return this; + } + + + public AggregateAuthenticationResponseHandler build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateDnResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateDnResolver.java new file mode 100644 index 0000000..f0c78ef --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateDnResolver.java @@ -0,0 +1,205 @@ + +package org.xbib.net.ldap.auth; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.concurrent.CallableWorker; + +/** + * Looks up a user's DN using multiple DN resolvers. Each DN resolver is invoked on a separate thread. If multiple DNs + * are allowed then the first one retrieved is returned. + * + */ +public class AggregateDnResolver implements DnResolver { + + /** + * To submit operations to. + */ + private final CallableWorker callableWorker; + + /** + * Labeled DN resolvers. + */ + private Map dnResolvers = new HashMap<>(); + + /** + * Whether to throw an exception if multiple DNs are found. + */ + private boolean allowMultipleDns; + + + /** + * Default constructor. + */ + public AggregateDnResolver() { + callableWorker = new CallableWorker<>(AggregateDnResolver.class.getSimpleName()); + } + + + /** + * Creates a new aggregate dn resolver. + * + * @param resolvers dn resolvers + */ + public AggregateDnResolver(final Map resolvers) { + setDnResolvers(resolvers); + callableWorker = new CallableWorker<>(AggregateDnResolver.class.getSimpleName()); + } + + + /** + * Creates a new aggregate dn resolver. + * + * @param resolvers dn resolvers + * @param es executor service for invoking DN resolvers + */ + public AggregateDnResolver(final Map resolvers, final ExecutorService es) { + setDnResolvers(resolvers); + callableWorker = new CallableWorker<>(es); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the DN resolvers to aggregate over. + * + * @return map of label to dn resolver + */ + public Map getDnResolvers() { + return Collections.unmodifiableMap(dnResolvers); + } + + /** + * Sets the DN resolvers to aggregate over. + * + * @param resolvers to set + */ + public void setDnResolvers(final Map resolvers) { + dnResolvers = resolvers; + } + + /** + * Adds a DN resolver with the supplied label. + * + * @param label of the resolver + * @param resolver DN resolver + */ + public void addDnResolver(final String label, final DnResolver resolver) { + dnResolvers.put(label, resolver); + } + + /** + * Returns whether DN resolution should fail if multiple DNs are found. + * + * @return whether an exception will be thrown if multiple DNs are found + */ + public boolean getAllowMultipleDns() { + return allowMultipleDns; + } + + /** + * Sets whether DN resolution should fail if multiple DNs are found If false an exception will be thrown if {@link + * #resolve(User)} finds that more than one DN resolver returns a DN. Otherwise, the first DN found is returned. + * + * @param b whether multiple DNs are allowed + */ + public void setAllowMultipleDns(final boolean b) { + allowMultipleDns = b; + } + + /** + * Creates an aggregate entry resolver using the labels from the DN resolver and the supplied entry resolver. + * + * @param resolver used for every label + * @return aggregate entry resolver + */ + public EntryResolver createEntryResolver(final EntryResolver resolver) { + final Map resolvers = new HashMap<>(dnResolvers.size()); + for (String label : dnResolvers.keySet()) { + resolvers.put(label, resolver); + } + return new AggregateEntryResolver(resolvers); + } + + @Override + public String resolve(final User user) + throws LdapException { + final List> callables = new ArrayList<>(); + for (final Map.Entry entry : dnResolvers.entrySet()) { + callables.add( + () -> { + final String dn = entry.getValue().resolve(user); + if (dn != null && !dn.isEmpty()) { + return String.format("%s:%s", entry.getKey(), dn); + } + return null; + }); + } + + final List results = new ArrayList<>(dnResolvers.size()); + final List exceptions = callableWorker.execute( + callables, + s -> { + if (s != null) { + results.add(s); + } + }); + for (ExecutionException e : exceptions) { + if (e.getCause() instanceof LdapException) { + throw (LdapException) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + // + } + } + if (results.size() > 1 && !allowMultipleDns) { + throw new LdapException("Found more than (1) DN for: " + user); + } + return results.isEmpty() ? null : results.get(0); + } + + /** + * Invokes {@link ExecutorService#shutdown()} on the underlying executor service. + */ + public void shutdown() { + callableWorker.shutdown(); + } + + // CheckStyle:OFF + public static class Builder { + + + private final AggregateDnResolver object = new AggregateDnResolver(); + + + protected Builder() { + } + + + public Builder resolver(final String label, final DnResolver resolver) { + object.addDnResolver(label, resolver); + return this; + } + + + public AggregateDnResolver build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateEntryResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateEntryResolver.java new file mode 100644 index 0000000..acb103f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AggregateEntryResolver.java @@ -0,0 +1,111 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ResultCode; + +/** + * Used in conjunction with an {@link AggregateDnResolver} to resolve an entry. In particular, the resolved DN is + * expected to be of the form: label:DN where the label indicates the entry resolver to use. This class only invokes + * one entry resolver that matches the label found on the DN. + * + */ +public class AggregateEntryResolver implements EntryResolver { + + /** + * Labeled entry resolvers. + */ + private Map entryResolvers = new HashMap<>(); + + + /** + * Default constructor. + */ + public AggregateEntryResolver() { + } + + + /** + * Creates a new aggregate entry resolver. + * + * @param resolvers entry resolvers + */ + public AggregateEntryResolver(final Map resolvers) { + setEntryResolvers(resolvers); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the entry resolvers to aggregate over. + * + * @return map of label to entry resolver + */ + public Map getEntryResolvers() { + return Collections.unmodifiableMap(entryResolvers); + } + + /** + * Sets the entry resolvers to aggregate over. + * + * @param resolvers to set + */ + public void setEntryResolvers(final Map resolvers) { + entryResolvers = resolvers; + } + + /** + * Adds an entry resolver with the supplied label. + * + * @param label of the resolver + * @param resolver entry resolver + */ + public void addEntryResolver(final String label, final EntryResolver resolver) { + entryResolvers.put(label, resolver); + } + + @Override + public LdapEntry resolve(final AuthenticationCriteria criteria, final AuthenticationHandlerResponse response) + throws LdapException { + final String[] labeledDn = criteria.getDn().split(":", 2); + final EntryResolver er = entryResolvers.get(labeledDn[0]); + if (er == null) { + throw new LdapException(ResultCode.PARAM_ERROR, "Could not find entry resolver for label: " + labeledDn[0]); + } + return er.resolve(new AuthenticationCriteria(labeledDn[1], criteria.getAuthenticationRequest()), response); + } + + // CheckStyle:OFF + public static class Builder { + + + private final AggregateEntryResolver object = new AggregateEntryResolver(); + + + protected Builder() { + } + + + public Builder resolver(final String label, final EntryResolver resolver) { + object.addEntryResolver(label, resolver); + return this; + } + + + public AggregateEntryResolver build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationCriteria.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationCriteria.java new file mode 100644 index 0000000..d7ec458 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationCriteria.java @@ -0,0 +1,109 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.Credential; + +/** + * Contains the properties used to perform authentication. + * + */ +public class AuthenticationCriteria { + + /** + * dn. + */ + private String authenticationDn; + + /** + * authentication request. + */ + private AuthenticationRequest authenticationRequest; + + + /** + * Default constructor. + */ + public AuthenticationCriteria() { + } + + + /** + * Creates a new authentication criteria. + * + * @param dn to authenticate + */ + public AuthenticationCriteria(final String dn) { + authenticationDn = dn; + } + + + /** + * Creates a new authentication criteria. + * + * @param dn to authenticate + * @param request that initiated the authentication + */ + public AuthenticationCriteria(final String dn, final AuthenticationRequest request) { + authenticationDn = dn; + authenticationRequest = request; + } + + + /** + * Returns the dn. + * + * @return dn to authenticate + */ + public String getDn() { + return authenticationDn; + } + + + /** + * Sets the dn. + * + * @param dn to set dn + */ + public void setDn(final String dn) { + authenticationDn = dn; + } + + + /** + * Returns the credential. + * + * @return credential to authenticate dn + */ + public Credential getCredential() { + return authenticationRequest.getCredential(); + } + + + /** + * Returns the authentication request. + * + * @return authentication request + */ + public AuthenticationRequest getAuthenticationRequest() { + return authenticationRequest; + } + + + /** + * Sets the authentication request. + * + * @param request to set authentication request + */ + public void setAuthenticationRequest(final AuthenticationRequest request) { + authenticationRequest = request; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "dn=" + authenticationDn + ", " + + "authenticationRequest=" + authenticationRequest + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationHandler.java new file mode 100644 index 0000000..9cf0fd8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationHandler.java @@ -0,0 +1,22 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapException; + +/** + * Provides an interface for LDAP authentication implementations. + * + */ +public interface AuthenticationHandler { + + + /** + * Perform an ldap authentication. + * + * @param criteria to perform the authentication with + * @return authentication handler response + * @throws LdapException if ldap operation fails + */ + AuthenticationHandlerResponse authenticate(AuthenticationCriteria criteria) + throws LdapException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationHandlerResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationHandlerResponse.java new file mode 100644 index 0000000..66b2235 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationHandlerResponse.java @@ -0,0 +1,146 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Arrays; +import org.xbib.net.ldap.AbstractResult; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.Result; + +/** + * Response object for authentication handlers. + * + */ +public class AuthenticationHandlerResponse extends AbstractResult { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10429; + + /** + * Authentication result code. + */ + private AuthenticationResultCode authenticationResultCode; + + /** + * Connection that authentication occurred on. + */ + private Connection connection; + + + /** + * Default constructor. + */ + private AuthenticationHandlerResponse() { + } + + + /** + * Creates a new authentication response. + * + * @param type of LDAP result + * @param result of the LDAP operation used to produce this response + * @param code authentication result code + * @param conn connection the authentication occurred on + */ + public AuthenticationHandlerResponse( + final T result, + final AuthenticationResultCode code, + final Connection conn) { + copyValues(result); + authenticationResultCode = code; + connection = conn; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + protected static Builder builder() { + return new Builder(); + } + + public AuthenticationResultCode getAuthenticationResultCode() { + return authenticationResultCode; + } + + public Connection getConnection() { + return connection; + } + + @Override + public boolean isSuccess() { + return AuthenticationResultCode.AUTHENTICATION_HANDLER_SUCCESS == authenticationResultCode; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AuthenticationHandlerResponse v) { + return super.equals(o) && + LdapUtils.areEqual(authenticationResultCode, v.authenticationResultCode) && + LdapUtils.areEqual(connection, v.connection); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs(), + authenticationResultCode, + connection); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "connection=" + connection + ", " + + "authenticationResultCode=" + authenticationResultCode + ", " + + "resultCode=" + getResultCode() + ", " + + "matchedDN=" + getMatchedDN() + ", " + + "diagnosticMessage=" + getEncodedDiagnosticMessage() + ", " + + "referralURLs=" + Arrays.toString(getReferralURLs()) + ", " + + "messageID=" + getMessageID() + ", " + + "controls=" + Arrays.toString(getControls()) + "]"; + } + + // CheckStyle:OFF + protected static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new AuthenticationHandlerResponse()); + } + + + @Override + protected Builder self() { + return this; + } + + + public Builder resultCode(final AuthenticationResultCode code) { + object.authenticationResultCode = code; + return this; + } + + + public Builder connection(final Connection conn) { + object.connection = conn; + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationRequest.java new file mode 100644 index 0000000..b9eb41c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationRequest.java @@ -0,0 +1,322 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Arrays; +import org.xbib.net.ldap.Credential; +import org.xbib.net.ldap.ReturnAttributes; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Contains the data required to perform an ldap authentication. + * + */ +public class AuthenticationRequest { + + /** + * User. + */ + private User user; + + /** + * User credential. + */ + private Credential credential; + + /** + * User attributes to return. + */ + private String[] returnAttributes = ReturnAttributes.NONE.value(); + + /** + * Request controls. + */ + private RequestControl[] controls; + + + /** + * Default constructor. + */ + public AuthenticationRequest() { + } + + + /** + * Creates a new authentication request. + * + * @param id that identifies the user + * @param c credential to authenticate the user + */ + public AuthenticationRequest(final String id, final Credential c) { + setUser(new User(id)); + setCredential(c); + } + + + /** + * Creates a new authentication request. + * + * @param id that identifies the user + * @param c credential to authenticate the user + * @param attrs attributes to return + */ + public AuthenticationRequest(final String id, final Credential c, final String... attrs) { + setUser(new User(id)); + setCredential(c); + setReturnAttributes(attrs); + } + + + /** + * Creates a new authentication request. + * + * @param u that identifies the user + * @param c credential to authenticate the user + */ + public AuthenticationRequest(final User u, final Credential c) { + setUser(u); + setCredential(c); + } + + + /** + * Creates a new authentication request. + * + * @param u that identifies the user + * @param c credential to authenticate the user + * @param attrs attributes to return + */ + public AuthenticationRequest(final User u, final Credential c, final String... attrs) { + setUser(u); + setCredential(c); + setReturnAttributes(attrs); + } + + /** + * Returns an authentication request initialized with the supplied request. + * + * @param request authentication request to read properties from + * @return authentication request + */ + public static AuthenticationRequest copy(final AuthenticationRequest request) { + final AuthenticationRequest r = new AuthenticationRequest(); + r.setUser(request.getUser()); + r.setCredential(request.getCredential()); + r.setReturnAttributes(request.getReturnAttributes()); + r.setControls(request.getControls()); + return r; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the user. + * + * @return user identifier + */ + public User getUser() { + return user; + } + + /** + * Sets the user. + * + * @param u user + */ + public void setUser(final User u) { + user = u; + } + + /** + * Returns the credential. + * + * @return user credential + */ + public Credential getCredential() { + return credential; + } + + /** + * Sets the credential. + * + * @param c user credential + */ + public void setCredential(final Credential c) { + credential = c; + } + + /** + * Returns the return attributes. + * + * @return attributes to return + */ + public String[] getReturnAttributes() { + return returnAttributes; + } + + /** + * Sets the return attributes. + * + * @param attrs return attributes + */ + public void setReturnAttributes(final String... attrs) { + returnAttributes = ReturnAttributes.parse(attrs); + } + + /** + * Returns the controls. + * + * @return controls + */ + public RequestControl[] getControls() { + return controls; + } + + /** + * Sets the controls. + * + * @param cntrls controls to set + */ + public void setControls(final RequestControl... cntrls) { + controls = cntrls; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "user=" + user + ", " + + "returnAttributes=" + Arrays.toString(returnAttributes) + ", " + + "controls=" + Arrays.toString(controls) + "]"; + } + + /** + * Authentication request builder. + */ + public static class Builder { + + /** + * Authentication request to build. + */ + private final AuthenticationRequest object = new AuthenticationRequest(); + + + /** + * Default constructor. + */ + protected Builder() { + } + + + /** + * Sets the user id. + * + * @param id user id + * @return this builder + */ + public Builder id(final String id) { + object.setUser(new User(id)); + return this; + } + + + /** + * Sets the user credential. + * + * @param credential user credential + * @return this builder + */ + public Builder credential(final Credential credential) { + object.setCredential(credential); + return this; + } + + + /** + * Sets the user credential. + * + * @param credential user credential + * @return this builder + */ + public Builder credential(final String credential) { + object.setCredential(new Credential(credential)); + return this; + } + + + /** + * Sets the user credential. + * + * @param credential user credential + * @return this builder + */ + public Builder credential(final char[] credential) { + object.setCredential(new Credential(credential)); + return this; + } + + + /** + * Sets the user credential. + * + * @param credential user credential + * @return this builder + */ + public Builder credential(final byte[] credential) { + object.setCredential(new Credential(credential)); + return this; + } + + + /** + * Sets the user. + * + * @param user to authenticate + * @return this builder + */ + public Builder user(final User user) { + object.setUser(user); + return this; + } + + + /** + * Sets the return attributes. + * + * @param attributes return attributes + * @return this builder + */ + public Builder returnAttributes(final String... attributes) { + object.setReturnAttributes(attributes); + return this; + } + + + /** + * Sets the request controls. + * + * @param controls request controls + * @return this builder + */ + public Builder controls(final RequestControl... controls) { + object.setControls(controls); + return this; + } + + + /** + * Returns the authentication request. + * + * @return authentication request + */ + public AuthenticationRequest build() { + return object; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationRequestHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationRequestHandler.java new file mode 100644 index 0000000..20011ff --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationRequestHandler.java @@ -0,0 +1,22 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapException; + +/** + * Provides pre authentication handling of authentication requests. + * + */ +public interface AuthenticationRequestHandler { + + + /** + * Handle the request for an ldap authentication. + * + * @param dn distinguished name resolved for this request + * @param request for this authentication event + * @throws LdapException if an error occurs handling an authentication request + */ + void handle(String dn, AuthenticationRequest request) + throws LdapException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResponse.java new file mode 100644 index 0000000..5cc0232 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResponse.java @@ -0,0 +1,217 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Arrays; +import org.xbib.net.ldap.AbstractResult; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; + +/** + * Synthetic response object that encapsulates data used for authentication. + * + */ +public class AuthenticationResponse extends AbstractResult { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10427; + + /** + * Result of the authentication operation. + */ + private AuthenticationHandlerResponse authenticationHandlerResponse; + + /** + * Resolved DN. + */ + private String resolvedDn; + + /** + * Ldap entry of authenticated user. + */ + private LdapEntry ldapEntry; + + /** + * Account state. + */ + private AccountState accountState; + + + /** + * Default constructor. + */ + private AuthenticationResponse() { + } + + + /** + * Creates a new authentication response. + * + * @param response authentication handler response + * @param dn produced by the DN resolver + * @param entry of the authenticated user + */ + public AuthenticationResponse( + final AuthenticationHandlerResponse response, + final String dn, + final LdapEntry entry) { + copyValues(response); + authenticationHandlerResponse = response; + resolvedDn = dn; + ldapEntry = entry; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + protected static Builder builder() { + return new Builder(); + } + + /** + * Returns whether the authentication handler produced a {@link + * AuthenticationResultCode#AUTHENTICATION_HANDLER_SUCCESS} result. + * + * @return whether authentication was successful + */ + public boolean isSuccess() { + return + AuthenticationResultCode.AUTHENTICATION_HANDLER_SUCCESS == + authenticationHandlerResponse.getAuthenticationResultCode(); + } + + public AuthenticationResultCode getAuthenticationResultCode() { + return authenticationHandlerResponse.getAuthenticationResultCode(); + } + + public AuthenticationHandlerResponse getAuthenticationHandlerResponse() { + return authenticationHandlerResponse; + } + + /** + * Returns the DN that was resolved in order to perform authentication. + * + * @return resolved dn + */ + public String getResolvedDn() { + return resolvedDn; + } + + /** + * Returns the ldap entry of the authenticated user. + * + * @return ldap entry + */ + public LdapEntry getLdapEntry() { + return ldapEntry; + } + + /** + * Returns the account state associated with the authenticated user. + * + * @return account state + */ + public AccountState getAccountState() { + return accountState; + } + + /** + * Sets the account state for the authenticated user. + * + * @param state for this user + */ + public void setAccountState(final AccountState state) { + accountState = state; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AuthenticationResponse v) { + return super.equals(o) && + LdapUtils.areEqual(authenticationHandlerResponse, v.authenticationHandlerResponse) && + LdapUtils.areEqual(resolvedDn, v.resolvedDn) && + LdapUtils.areEqual(ldapEntry, v.ldapEntry) && + LdapUtils.areEqual(accountState, v.accountState); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs(), + authenticationHandlerResponse, + resolvedDn, + ldapEntry, + accountState); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "authenticationHandlerResponse=" + authenticationHandlerResponse + ", " + + "resolvedDn=" + resolvedDn + ", " + + "ldapEntry=" + ldapEntry + ", " + + "accountState=" + accountState + ", " + + "resultCode=" + getResultCode() + ", " + + "matchedDN=" + getMatchedDN() + ", " + + "diagnosticMessage=" + getEncodedDiagnosticMessage() + ", " + + "referralURLs=" + Arrays.toString(getReferralURLs()) + ", " + + "messageID=" + getMessageID() + ", " + + "controls=" + Arrays.toString(getControls()) + "]"; + } + + // CheckStyle:OFF + protected static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new AuthenticationResponse()); + } + + + @Override + protected Builder self() { + return this; + } + + + public Builder response(final AuthenticationHandlerResponse response) { + object.copyValues(response); + object.authenticationHandlerResponse = response; + return this; + } + + + public Builder dn(final String dn) { + object.resolvedDn = dn; + return this; + } + + + public Builder entry(final LdapEntry entry) { + object.ldapEntry = entry; + return this; + } + + + public Builder state(final AccountState state) { + object.accountState = state; + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResponseHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResponseHandler.java new file mode 100644 index 0000000..d054fdb --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResponseHandler.java @@ -0,0 +1,21 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapException; + +/** + * Provides post authentication handling of authentication responses. + * + */ +public interface AuthenticationResponseHandler { + + + /** + * Handle the response from an ldap authentication. + * + * @param response produced from an authentication + * @throws LdapException if an error occurs handling an authentication response + */ + void handle(AuthenticationResponse response) + throws LdapException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResultCode.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResultCode.java new file mode 100644 index 0000000..5ce768f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthenticationResultCode.java @@ -0,0 +1,29 @@ + +package org.xbib.net.ldap.auth; + +/** + * Enum to define authentication results. + * + */ +public enum AuthenticationResultCode { + + /** + * The configured authentication handler produced a result of true. + */ + AUTHENTICATION_HANDLER_SUCCESS, + + /** + * The configured authentication handler produced a result of false. + */ + AUTHENTICATION_HANDLER_FAILURE, + + /** + * The supplied credential was empty or null. + */ + INVALID_CREDENTIAL, + + /** + * The configured DN resolver produced an empty or null value. + */ + DN_RESOLUTION_FAILURE +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/Authenticator.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/Authenticator.java new file mode 100644 index 0000000..826ae45 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/Authenticator.java @@ -0,0 +1,588 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.xbib.net.ldap.ConnectionFactoryManager; +import org.xbib.net.ldap.Credential; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ReturnAttributes; + +/** + * Provides functionality to authenticate users against an ldap directory. + * + */ +public class Authenticator { + + /** + * NoOp entry resolver. + */ + private static final EntryResolver NO_OP_RESOLVER = new NoOpEntryResolver(); + + /** + * For finding user DNs. + */ + private DnResolver dnResolver; + + /** + * Handler to handle authentication. + */ + private AuthenticationHandler authenticationHandler; + + /** + * For finding user entries. + */ + private EntryResolver entryResolver; + + /** + * User attributes to return. Concatenated to {@link AuthenticationRequest#getReturnAttributes()}. + */ + private String[] returnAttributes; + + /** + * Handlers to handle authentication requests. + */ + private AuthenticationRequestHandler[] requestHandlers; + + /** + * Handlers to handle authentication responses. + */ + private AuthenticationResponseHandler[] responseHandlers; + + /** + * Whether to execute the entry resolver on authentication failure. + */ + private boolean resolveEntryOnFailure; + + + /** + * Default constructor. + */ + public Authenticator() { + } + + + /** + * Creates a new authenticator. + * + * @param resolver dn resolver + * @param handler authentication handler + */ + public Authenticator(final DnResolver resolver, final AuthenticationHandler handler) { + setDnResolver(resolver); + setAuthenticationHandler(handler); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the DN resolver. + * + * @return DN resolver + */ + public DnResolver getDnResolver() { + return dnResolver; + } + + /** + * Sets the DN resolver. + * + * @param resolver for finding DNs + */ + public void setDnResolver(final DnResolver resolver) { + dnResolver = resolver; + } + + /** + * Returns the authentication handler. + * + * @return authentication handler + */ + public AuthenticationHandler getAuthenticationHandler() { + return authenticationHandler; + } + + /** + * Sets the authentication handler. + * + * @param handler for performing authentication + */ + public void setAuthenticationHandler(final AuthenticationHandler handler) { + authenticationHandler = handler; + } + + /** + * Returns the entry resolver. + * + * @return entry resolver + */ + public EntryResolver getEntryResolver() { + return entryResolver; + } + + /** + * Sets the entry resolver. + * + * @param resolver for finding entries + */ + public void setEntryResolver(final EntryResolver resolver) { + entryResolver = resolver; + } + + /** + * Returns whether to execute the entry resolver on authentication failure. + * + * @return whether to execute the entry resolver on authentication failure + */ + public boolean getResolveEntryOnFailure() { + return resolveEntryOnFailure; + } + + /** + * Sets whether to execute the entry resolver on authentication failure. + * + * @param b whether to execute the entry resolver + */ + public void setResolveEntryOnFailure(final boolean b) { + resolveEntryOnFailure = b; + } + + /** + * Returns the return attributes. + * + * @return attributes to return + */ + public String[] getReturnAttributes() { + return returnAttributes; + } + + /** + * Sets the return attributes. + * + * @param attrs return attributes + */ + public void setReturnAttributes(final String... attrs) { + returnAttributes = attrs; + } + + /** + * Returns the authentication request handlers. + * + * @return authentication request handlers + */ + public AuthenticationRequestHandler[] getRequestHandlers() { + return requestHandlers; + } + + /** + * Sets the authentication request handlers. + * + * @param handlers authentication request handlers + */ + public void setRequestHandlers(final AuthenticationRequestHandler... handlers) { + requestHandlers = handlers; + } + + /** + * Returns the authentication response handlers. + * + * @return authentication response handlers + */ + public AuthenticationResponseHandler[] getResponseHandlers() { + return responseHandlers; + } + + /** + * Sets the authentication response handlers. + * + * @param handlers authentication response handlers + */ + public void setResponseHandlers(final AuthenticationResponseHandler... handlers) { + responseHandlers = handlers; + } + + /** + * This will attempt to find the DN for the supplied user. {@link DnResolver#resolve(User)} is invoked to perform this + * operation. + * + * @param user to find DN for + * @return user DN + * @throws LdapException if an LDAP error occurs during resolution + */ + public String resolveDn(final User user) + throws LdapException { + return dnResolver.resolve(user); + } + + /** + * Authenticate the user in the supplied request. + * + * @param request authentication request + * @return response containing the ldap entry of the user authenticated + * @throws LdapException if an LDAP error occurs + */ + public AuthenticationResponse authenticate(final AuthenticationRequest request) + throws LdapException { + return authenticate(resolveDn(request.getUser()), request); + } + + /** + * Attempts to close any connection factories associated with this authenticator. Inspects the {@link #dnResolver}, + * {@link #authenticationHandler} and {@link #entryResolver} for type {@link ConnectionFactoryManager}. If found, + * those underlying connection factories are closed. {@link AggregateDnResolver}, {@link + * AggregateAuthenticationHandler} and {@link AggregateEntryResolver} are handled as well. + *

+ * Note that custom components that contain connection factories but do not implement {@link ConnectionFactoryManager} + * will not be closed by this method. + */ + public void close() { + final Set managers = new HashSet<>(); + if (dnResolver instanceof ConnectionFactoryManager) { + managers.add((ConnectionFactoryManager) dnResolver); + } else if (dnResolver instanceof AggregateDnResolver) { + final Map resolvers = ((AggregateDnResolver) dnResolver).getDnResolvers(); + if (resolvers != null) { + resolvers.values().stream() + .filter(ConnectionFactoryManager.class::isInstance) + .map(ConnectionFactoryManager.class::cast) + .forEach(managers::add); + } + } + if (authenticationHandler instanceof ConnectionFactoryManager) { + managers.add((ConnectionFactoryManager) authenticationHandler); + } else if (authenticationHandler instanceof AggregateAuthenticationHandler) { + final Map handlers = + ((AggregateAuthenticationHandler) authenticationHandler).getAuthenticationHandlers(); + if (handlers != null) { + handlers.values().stream() + .filter(ConnectionFactoryManager.class::isInstance) + .map(ConnectionFactoryManager.class::cast) + .forEach(managers::add); + } + } + if (entryResolver instanceof ConnectionFactoryManager) { + managers.add((ConnectionFactoryManager) entryResolver); + } else if (entryResolver instanceof AggregateEntryResolver) { + final Map resolvers = ((AggregateEntryResolver) entryResolver).getEntryResolvers(); + if (resolvers != null) { + resolvers.values().stream() + .filter(ConnectionFactoryManager.class::isInstance) + .map(ConnectionFactoryManager.class::cast) + .forEach(managers::add); + } + } + + if (!managers.isEmpty()) { + closeConnectionFactoryManagers(managers); + } + } + + /** + * Attempts to close all the connection factories in the supplied collection. + * + * @param managers to close connection factories for + */ + private void closeConnectionFactoryManagers(final Set managers) { + if (managers != null) { + managers.stream() + .filter(Objects::nonNull) + .map(ConnectionFactoryManager::getConnectionFactory) + .filter(Objects::nonNull) + .distinct() + .forEach(cf -> { + try { + cf.close(); + } catch (Exception e) { + // + } + }); + } + } + + /** + * Validates input and performs authentication using an {@link AuthenticationHandler}. Executes any configured {@link + * AuthenticationResponseHandler}. + * + * @param dn to authenticate as + * @param request containing authentication parameters + * @return ldap entry for the supplied DN + * @throws LdapException if an LDAP error occurs + */ + protected AuthenticationResponse authenticate(final String dn, final AuthenticationRequest request) + throws LdapException { + final AuthenticationResponse invalidInput = validateInput(dn, request); + if (invalidInput != null) { + return invalidInput; + } + + final LdapEntry entry; + final AuthenticationRequest processedRequest = processRequest(dn, request); + AuthenticationHandlerResponse response = null; + try { + final AuthenticationCriteria ac = new AuthenticationCriteria(dn, processedRequest); + + // attempt to authenticate as this dn + response = getAuthenticationHandler().authenticate(ac); + // resolve the entry + entry = resolveEntry(ac, response); + } finally { + if (response != null && response.getConnection() != null) { + response.getConnection().close(); + } + } + + final AuthenticationResponse authResponse = new AuthenticationResponse(response, dn, entry); + // execute authentication response handlers + if (getResponseHandlers() != null) { + for (AuthenticationResponseHandler ah : getResponseHandlers()) { + ah.handle(authResponse); + } + } + + return authResponse; + } + + /** + * Validates the authentication request and resolved DN. Returns an authentication response if validation failed. + * + * @param dn to validate + * @param request to validate + * @return authentication response if validation failed, otherwise null + */ + protected AuthenticationResponse validateInput(final String dn, final AuthenticationRequest request) { + AuthenticationResponse response = null; + final Credential credential = request.getCredential(); + if (credential == null || credential.getBytes() == null) { + response = AuthenticationResponse.builder() + .response( + AuthenticationHandlerResponse.builder() + .diagnosticMessage("Credential cannot be null") + .resultCode(AuthenticationResultCode.INVALID_CREDENTIAL).build()) + .dn(dn) + .build(); + } else if (credential.getBytes().length == 0) { + response = AuthenticationResponse.builder() + .response( + AuthenticationHandlerResponse.builder() + .diagnosticMessage("Credential cannot be empty") + .resultCode(AuthenticationResultCode.INVALID_CREDENTIAL).build()) + .dn(dn) + .build(); + } else if (dn == null) { + response = AuthenticationResponse.builder() + .response( + AuthenticationHandlerResponse.builder() + .diagnosticMessage("DN cannot be null") + .resultCode(AuthenticationResultCode.DN_RESOLUTION_FAILURE).build()) + .dn(dn) + .build(); + } else if (dn.isEmpty()) { + response = AuthenticationResponse.builder() + .response( + AuthenticationHandlerResponse.builder() + .diagnosticMessage("DN cannot be empty") + .resultCode(AuthenticationResultCode.DN_RESOLUTION_FAILURE).build()) + .dn(dn) + .build(); + } + return response; + } + + /** + * Creates a new authentication request applying any applicable configuration on this authenticator. Returns the + * supplied request if no configuration is applied. + * + * @param dn to process + * @param request to process + * @return authentication request + * @throws LdapException if an error occurs with a request handler + */ + protected AuthenticationRequest processRequest(final String dn, final AuthenticationRequest request) + throws LdapException { + if (returnAttributes == null && (getRequestHandlers() == null || getRequestHandlers().length == 0)) { + return request; + } + + final AuthenticationRequest newRequest = AuthenticationRequest.copy(request); + if (returnAttributes != null) { + if (newRequest.getReturnAttributes() == null || + ReturnAttributes.NONE.equalsAttributes(newRequest.getReturnAttributes())) { + newRequest.setReturnAttributes(returnAttributes); + } else { + newRequest.setReturnAttributes(LdapUtils.concatArrays(newRequest.getReturnAttributes(), returnAttributes)); + } + } + + // execute authentication request handlers + if (getRequestHandlers() != null) { + for (AuthenticationRequestHandler ah : getRequestHandlers()) { + ah.handle(dn, newRequest); + } + } + return newRequest; + } + + /** + * Attempts to find the ldap entry for the supplied DN. If an entry resolver has been configured it is used. A {@link + * SearchEntryResolver} is used if return attributes have been requested. If none of these criteria is met, a {@link + * NoOpDnResolver} is used. + * + * @param criteria needed by the entry resolver + * @param response from the authentication handler + * @return ldap entry + * @throws LdapException if an error occurs resolving the entry + */ + protected LdapEntry resolveEntry( + final AuthenticationCriteria criteria, + final AuthenticationHandlerResponse response) + throws LdapException { + LdapEntry entry = null; + final EntryResolver er; + if (resolveEntryOnFailure || response.isSuccess()) { + if (entryResolver != null) { + er = entryResolver; + } else if (!ReturnAttributes.NONE.equalsAttributes(criteria.getAuthenticationRequest().getReturnAttributes())) { + if (dnResolver instanceof AggregateDnResolver) { + er = ((AggregateDnResolver) dnResolver).createEntryResolver(new SearchEntryResolver()); + } else { + er = new SearchEntryResolver(); + } + } else { + er = NO_OP_RESOLVER; + } + try { + entry = er.resolve(criteria, response); + } catch (LdapException e) { + // + } + } + if (entry == null) { + entry = NO_OP_RESOLVER.resolve(criteria, response); + } + return entry; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "dnResolver=" + dnResolver + ", " + + "authenticationHandler=" + authenticationHandler + ", " + + "entryResolver=" + entryResolver + ", " + + "returnAttributes=" + Arrays.toString(returnAttributes) + ", " + + "requestHandlers=" + Arrays.toString(requestHandlers) + ", " + + "responseHandlers=" + Arrays.toString(responseHandlers) + "]"; + } + + /** + * Authenticator builder. + */ + public static class Builder { + + /** + * Authenticator to build. + */ + private final Authenticator object = new Authenticator(); + + + /** + * Default constructor. + */ + protected Builder() { + } + + + /** + * Sets the DN resolver. + * + * @param resolver DN resolver + * @return this builder + */ + public Builder dnResolver(final DnResolver resolver) { + object.setDnResolver(resolver); + return this; + } + + + /** + * Sets the authentication handler. + * + * @param handler authentication handler + * @return this builder + */ + public Builder authenticationHandler(final AuthenticationHandler handler) { + object.setAuthenticationHandler(handler); + return this; + } + + + /** + * Sets the entry resolver. + * + * @param resolver entry resolver + * @return this builder + */ + public Builder entryResolver(final EntryResolver resolver) { + object.setEntryResolver(resolver); + return this; + } + + + /** + * Sets the authentication request handlers. + * + * @param handlers request handlers + * @return this builder + */ + public Builder requestHandlers(final AuthenticationRequestHandler... handlers) { + object.setRequestHandlers(handlers); + return this; + } + + + /** + * Sets the authentication response handlers. + * + * @param handlers response handlers + * @return this builder + */ + public Builder responseHandlers(final AuthenticationResponseHandler... handlers) { + object.setResponseHandlers(handlers); + return this; + } + + + /** + * Sets the return attributes. + * + * @param attributes return attributes + * @return this builder + */ + public Builder returnAttributes(final String... attributes) { + object.setReturnAttributes(attributes); + return this; + } + + + /** + * Returns the authenticator. + * + * @return authenticator + */ + public Authenticator build() { + return object; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthorizationIdentityEntryResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthorizationIdentityEntryResolver.java new file mode 100644 index 0000000..75d5c60 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/AuthorizationIdentityEntryResolver.java @@ -0,0 +1,51 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.control.AuthorizationIdentityResponseControl; + +/** + * Reads the authorization identity response control, then performs an object level search on the result. Useful when + * users authenticate with some mapped identifier, like DIGEST-MD5. This resolver must be used with an {@link + * AuthenticationHandler} that is configured to send the {@link + * org.xbib.net.ldap.control.AuthorizationIdentityRequestControl}. + * + */ +public class AuthorizationIdentityEntryResolver extends AbstractSearchEntryResolver { + + + @Override + protected SearchResponse performLdapSearch( + final AuthenticationCriteria criteria, + final AuthenticationHandlerResponse response) + throws LdapException { + final AuthorizationIdentityResponseControl ctrl = (AuthorizationIdentityResponseControl) response.getControl( + AuthorizationIdentityResponseControl.OID); + if (ctrl == null) { + throw new IllegalStateException("Authorization Identity Response Control not found"); + } + + final String authzId = ctrl.getAuthorizationId(); + final String dn = authzId.split(":", 2)[1].trim(); + return response.getConnection().operation(createSearchRequest(criteria, dn)).execute(); + } + + + /** + * Returns a search request for an object level search for the supplied DN. + * + * @param ac authentication criteria containing return attributes + * @param dn from the who am i operation + * @return search request + */ + protected SearchRequest createSearchRequest(final AuthenticationCriteria ac, final String dn) { + final SearchRequest request = SearchRequest.objectScopeSearchRequest( + dn, + ac.getAuthenticationRequest().getReturnAttributes()); + request.setDerefAliases(getDerefAliases()); + request.setBinaryAttributes(getBinaryAttributes()); + return request; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/CompareAuthenticationHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/CompareAuthenticationHandler.java new file mode 100644 index 0000000..7f6f54e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/CompareAuthenticationHandler.java @@ -0,0 +1,219 @@ + +package org.xbib.net.ldap.auth; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import org.xbib.net.ldap.CompareRequest; +import org.xbib.net.ldap.CompareResponse; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.Credential; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ResultCode; + +/** + * Provides an LDAP authentication implementation that uses a compare operation against the userPassword attribute. The + * default password scheme used is 'SHA'. + * + */ +public class CompareAuthenticationHandler extends AbstractAuthenticationHandler { + + /** + * Default password scheme. Value is {@value}. + */ + protected static final String DEFAULT_SCHEME = "SHA:SHA"; + + /** + * Default password attribute. Value is {@value}. + */ + protected static final String DEFAULT_ATTRIBUTE = "userPassword"; + + /** + * Password scheme. + */ + private Scheme passwordScheme = new Scheme(DEFAULT_SCHEME); + + /** + * Password attribute. + */ + private String passwordAttribute = DEFAULT_ATTRIBUTE; + + + /** + * Default constructor. + */ + public CompareAuthenticationHandler() { + } + + + /** + * Creates a new compare authentication handler. + * + * @param cf connection factory + */ + public CompareAuthenticationHandler(final ConnectionFactory cf) { + setConnectionFactory(cf); + } + + + /** + * Returns the password scheme. + * + * @return password scheme + */ + public String getPasswordScheme() { + return passwordScheme.toString(); + } + + + /** + * Sets the password scheme. + * + * @param s password scheme + */ + public void setPasswordScheme(final String s) { + passwordScheme = new Scheme(s); + } + + + /** + * Returns the password attribute. + * + * @return password attribute + */ + public String getPasswordAttribute() { + return passwordAttribute; + } + + + /** + * Sets the password attribute. Must equal a readable attribute in LDAP scheme. + * + * @param s password attribute + */ + public void setPasswordAttribute(final String s) { + passwordAttribute = s; + } + + + @Override + protected AuthenticationHandlerResponse authenticateInternal( + final Connection c, + final AuthenticationCriteria criteria) + throws LdapException { + final byte[] hash = digestCredential(criteria.getCredential(), passwordScheme.getAlgorithm()); + final CompareResponse compareResponse = c.operation( + CompareRequest.builder() + .controls(processRequestControls(criteria)) + .dn(criteria.getDn()) + .name(passwordAttribute) + .value(String.format("{%s}%s", passwordScheme.getLabel(), LdapUtils.base64Encode(hash))).build()).execute(); + return + new AuthenticationHandlerResponse( + compareResponse, + compareResponse.isTrue() ? + AuthenticationResultCode.AUTHENTICATION_HANDLER_SUCCESS : + AuthenticationResultCode.AUTHENTICATION_HANDLER_FAILURE, + c); + } + + + /** + * Digests the supplied credential using the supplied algorithm. + * + * @param credential to digest + * @param algorithm type of digest to use + * @return digested credential + * @throws LdapException if the supplied algorithm cannot be found + */ + protected byte[] digestCredential(final Credential credential, final String algorithm) + throws LdapException { + try { + final MessageDigest md = MessageDigest.getInstance(algorithm); + md.update(credential.getBytes()); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new LdapException(ResultCode.AUTH_UNKNOWN, e); + } + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "factory=" + getConnectionFactory() + ", " + + "passwordAttribute=" + passwordAttribute + ", " + + "passwordScheme=" + passwordScheme + ", " + + "controls=" + Arrays.toString(getAuthenticationControls()) + "]"; + } + + + /** + * Represents a password scheme used for attribute comparison. + */ + public static class Scheme { + + /** + * Label of the scheme. + */ + private final String label; + + /** + * Algorithm used by this scheme. + */ + private final String algorithm; + + + /** + * Creates a new scheme. + * + * @param labelAndAlgorithm colon delimited label:algorithm + */ + public Scheme(final String labelAndAlgorithm) { + final String[] s = labelAndAlgorithm.split(":", 2); + label = s[0]; + algorithm = s.length == 2 ? s[1] : s[0]; + } + + + /** + * Creates a new scheme. + * + * @param l label + * @param a algorithm + */ + public Scheme(final String l, final String a) { + label = l; + algorithm = a; + } + + + /** + * Returns the scheme label. + * + * @return label + */ + public String getLabel() { + return label; + } + + + /** + * Returns the scheme algorithm. + * + * @return algorithm + */ + public String getAlgorithm() { + return algorithm; + } + + + @Override + public String toString() { + return String.format("%s:%s", label, algorithm); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/DnResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/DnResolver.java new file mode 100644 index 0000000..7a91023 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/DnResolver.java @@ -0,0 +1,22 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapException; + +/** + * Provides an interface for finding LDAP DNs with a user identifier. + * + */ +public interface DnResolver { + + + /** + * Attempts to find the LDAP DN for the supplied user. + * + * @param user to find DN for + * @return user DN + * @throws LdapException if an LDAP error occurs + */ + String resolve(User user) + throws LdapException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/EntryResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/EntryResolver.java new file mode 100644 index 0000000..e322d6d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/EntryResolver.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; + +/** + * Provides an interface for finding a user's ldap entry after a successful authentication. + * + */ +public interface EntryResolver { + + + /** + * Attempts to find the LDAP entry for the supplied authentication criteria and authentication handler response. The + * connection available in the response should not be closed in this method. + * + * @param criteria authentication criteria used to perform the authentication + * @param response produced by the authentication handler + * @return ldap entry + * @throws LdapException if an LDAP error occurs + */ + LdapEntry resolve(AuthenticationCriteria criteria, AuthenticationHandlerResponse response) + throws LdapException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/FormatDnResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/FormatDnResolver.java new file mode 100644 index 0000000..23480c0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/FormatDnResolver.java @@ -0,0 +1,164 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Arrays; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.dn.AttributeValueEscaper; +import org.xbib.net.ldap.dn.DefaultAttributeValueEscaper; + +/** + * Returns a DN by applying a formatter. See {@link java.util.Formatter}. + * + */ +public class FormatDnResolver implements DnResolver { + + /** + * attribute value escaper. + */ + private final AttributeValueEscaper attributeValueEscaper = new DefaultAttributeValueEscaper(); + + /** + * format of DN. + */ + private String formatString; + + /** + * format arguments. + */ + private Object[] formatArgs; + + /** + * whether to escape the user input. + */ + private boolean escapeUser = true; + + + /** + * Default constructor. + */ + public FormatDnResolver() { + } + + + /** + * Creates a new format DN resolver. + * + * @param format formatter string + */ + public FormatDnResolver(final String format) { + setFormat(format); + } + + + /** + * Creates a new format DN resolver with the supplied format and arguments. + * + * @param format to set formatter string + * @param args to set formatter arguments + */ + public FormatDnResolver(final String format, final Object[] args) { + setFormat(format); + setFormatArgs(args); + } + + + /** + * Returns the formatter string used to return the entry DN. + * + * @return user field + */ + public String getFormat() { + return formatString; + } + + + /** + * Sets the formatter string used to return the entry DN. + * + * @param format formatter string + */ + public void setFormat(final String format) { + formatString = format; + } + + + /** + * Returns the format arguments. + * + * @return format args + */ + public Object[] getFormatArgs() { + return formatArgs; + } + + + /** + * Sets the format arguments. + * + * @param args to set format arguments + */ + public void setFormatArgs(final Object[] args) { + formatArgs = args; + } + + + /** + * Returns whether the user input will be escaped using {@link #attributeValueEscaper}. + * + * @return whether the user input will be escaped. + */ + public boolean getEscapeUser() { + return escapeUser; + } + + + /** + * Sets whether the user input will be escaped using {@link #attributeValueEscaper}. + * + * @param b whether the user input will be escaped. + */ + public void setEscapeUser(final boolean b) { + escapeUser = b; + } + + + /** + * Returns a DN for the supplied user by applying it to a format string. + * + * @param user to format dn for + * @return user DN + * @throws LdapException never + */ + @Override + public String resolve(final User user) + throws LdapException { + if (formatString == null) { + throw new IllegalStateException("Format string cannot be null"); + } + String dn = null; + if (user != null && user.getIdentifier() != null && !"".equals(user.getIdentifier())) { + final String escapedUser = escapeUser ? attributeValueEscaper.escape(user.getIdentifier()) : user.getIdentifier(); + if (formatArgs != null && formatArgs.length > 0) { + final Object[] args = new Object[formatArgs.length + 1]; + args[0] = escapedUser; + System.arraycopy(formatArgs, 0, args, 1, formatArgs.length); + dn = String.format(formatString, args); + } else { + dn = String.format(formatString, escapedUser); + } + } else { + // + } + return dn; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "formatString=" + formatString + ", " + + "formatArgs=" + Arrays.toString(formatArgs) + ", " + + "escapeUser=" + escapeUser + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/NoOpDnResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/NoOpDnResolver.java new file mode 100644 index 0000000..c6ac77d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/NoOpDnResolver.java @@ -0,0 +1,31 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapException; + +/** + * Returns a DN that is the user identifier. + * + */ +public class NoOpDnResolver implements DnResolver { + + + /** + * Returns the user as the DN. + * + * @param user to set as DN + * @return user as DN + * @throws LdapException never + */ + @Override + public String resolve(final User user) + throws LdapException { + return user != null ? user.getIdentifier() : null; + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/NoOpEntryResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/NoOpEntryResolver.java new file mode 100644 index 0000000..faca123 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/NoOpEntryResolver.java @@ -0,0 +1,23 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapEntry; + +/** + * Returns an LDAP entry that contains only the DN that was supplied to it. + * + */ +public class NoOpEntryResolver implements EntryResolver { + + + @Override + public LdapEntry resolve(final AuthenticationCriteria criteria, final AuthenticationHandlerResponse response) { + return LdapEntry.builder().dn(criteria.getDn()).build(); + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/SearchDnResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/SearchDnResolver.java new file mode 100644 index 0000000..292bbfb --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/SearchDnResolver.java @@ -0,0 +1,478 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Arrays; +import java.util.Iterator; +import org.xbib.net.ldap.AbstractSearchOperationFactory; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.DerefAliases; +import org.xbib.net.ldap.FilterTemplate; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ReturnAttributes; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.SearchScope; + +/** + * Base implementation for search dn resolvers. + * + */ +public class SearchDnResolver extends AbstractSearchOperationFactory implements DnResolver { + + /** + * DN to search. + */ + private String baseDn = ""; + + /** + * Filter for searching for the user. + */ + private String userFilter; + + /** + * Filter parameters for searching for the user. + */ + private Object[] userFilterParameters; + + /** + * Whether to throw an exception if multiple DNs are found. + */ + private boolean allowMultipleDns; + + /** + * Whether to use a subtree search when resolving DNs. + */ + private boolean subtreeSearch; + + /** + * How to handle aliases. + */ + private DerefAliases derefAliases = DerefAliases.NEVER; + + /** + * Resolve DN from alternative attribute name + */ + private String resolveFromAttribute; + + /** + * Default constructor. + */ + public SearchDnResolver() { + } + + + /** + * Creates a new search dn resolver. + * + * @param cf connection factory + */ + public SearchDnResolver(final ConnectionFactory cf) { + setConnectionFactory(cf); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the base DN. + * + * @return base DN + */ + public String getBaseDn() { + return baseDn; + } + + /** + * Sets the base DN. + * + * @param dn base DN + */ + public void setBaseDn(final String dn) { + baseDn = dn; + } + + /** + * Returns the filter used to search for the user. + * + * @return filter for searching + */ + public String getUserFilter() { + return userFilter; + } + + /** + * Sets the filter used to search for the user. + * + * @param filter user filter + */ + public void setUserFilter(final String filter) { + userFilter = filter; + } + + /** + * Returns the filter parameters used to search for the user. + * + * @return filter parameters + */ + public Object[] getUserFilterParameters() { + return userFilterParameters; + } + + /** + * Sets the filter parameters used to search for the user. + * + * @param filterParams filter parameters + */ + public void setUserFilterParameters(final Object[] filterParams) { + userFilterParameters = filterParams; + } + + /** + * Returns whether DN resolution should fail if multiple DNs are found. + * + * @return whether an exception will be thrown if multiple DNs are found + */ + public boolean getAllowMultipleDns() { + return allowMultipleDns; + } + + /** + * Sets whether DN resolution should fail if multiple DNs are found. If false an exception will be thrown if {@link + * #resolve(User)} finds more than one DN matching its filter. Otherwise, the first DN found is returned. + * + * @param b whether multiple DNs are allowed + */ + public void setAllowMultipleDns(final boolean b) { + allowMultipleDns = b; + } + + /** + * Returns whether subtree searching will be used. + * + * @return whether the DN will be searched for over the entire base + */ + public boolean getSubtreeSearch() { + return subtreeSearch; + } + + /** + * Sets whether subtree searching will be used. If true, the DN used for authenticating will be searched for over the + * entire {@link #getBaseDn()}. Otherwise, the DN will be searched for in the {@link #getBaseDn()} context. + * + * @param b whether the DN will be searched for over the entire base + */ + public void setSubtreeSearch(final boolean b) { + subtreeSearch = b; + } + + /** + * Returns how to dereference aliases. + * + * @return how to dereference aliases + */ + public DerefAliases getDerefAliases() { + return derefAliases; + } + + /** + * Sets how to dereference aliases. + * + * @param da how to dereference aliases + */ + public void setDerefAliases(final DerefAliases da) { + derefAliases = da; + } + + /** + * Gets an attribute to use to resolve the DN, if the attribute is not present the resolution fails back on the + * entry's DN. + * + * @return the attribute name + */ + public String getResolveFromAttribute() { + return resolveFromAttribute; + } + + /** + * Sets the attribute to use to resolve the DN. If null, the resolver will use the entry's DN. + * + * @param attributeName attribute name + */ + public void setResolveFromAttribute(final String attributeName) { + resolveFromAttribute = attributeName; + } + + /** + * Attempts to find the DN for the supplied user. {@link #createFilterTemplate(User)} is used to create the search + * filter. If more than one entry matches the search, the result is controlled by {@link + * #setAllowMultipleDns(boolean)}. + * + * @param user to find DN for + * @return user DN + * @throws LdapException if the entry resolution fails + */ + @Override + public String resolve(final User user) + throws LdapException { + + String dn = null; + if (user != null) { + // create the filter template + final FilterTemplate filter = createFilterTemplate(user); + + if (filter != null && filter.getFilter() != null) { + final SearchResponse result = performLdapSearch(filter); + if (!result.isSuccess()) { + throw new LdapException( + "Error resolving DN for user " + user + " with filter " + filter + + ". Unsuccessful search response: " + result); + } + + final Iterator answer = result.getEntries().iterator(); + + // return first match, otherwise user doesn't exist + if (answer != null && answer.hasNext()) { + dn = resolveDn(answer.next()); + if (answer.hasNext()) { + if (!allowMultipleDns) { + throw new LdapException( + "Found " + result.entrySize() + " DNs for " + user + " : " + result.getEntryDns()); + } + } + } + } + } + return dn; + } + + /** + * Returns the DN for the supplied ldap entry. + * + * @param entry to retrieve the DN from + * @return dn + */ + protected String resolveDn(final LdapEntry entry) { + if (resolveFromAttribute != null) { + return performResolveFromAttribute(entry); + } + return entry.getDn(); + } + + /** + * Resolve DN from attribute in the resolveFromAttribute property. + * + * @param entry containing an attribute with the DN + * @return first and singled value in resolveFromAttribute, or null if not valid + */ + protected String performResolveFromAttribute(final LdapEntry entry) { + final LdapAttribute attr = entry.getAttribute(resolveFromAttribute); + if (attr.size() != 1) { + return null; + } + if (attr.isBinary()) { + return null; + } + return attr.getStringValue(); + } + + /** + * Returns a filter template using {@link #userFilter} and {@link #userFilterParameters}. The user parameter is + * injected as a named parameter of 'user'. + * + * @param user to resolve DN + * @return filter template + */ + protected FilterTemplate createFilterTemplate(final User user) { + final FilterTemplate filter = new FilterTemplate(); + if (user != null && user.getIdentifier() != null && !"".equals(user.getIdentifier())) { + if (userFilter != null) { + filter.setFilter(userFilter); + if (userFilterParameters != null) { + filter.setParameters(userFilterParameters); + } + // assign user as a named parameter + filter.setParameter("user", user.getIdentifier()); + // assign context as a named parameter + filter.setParameter("context", user.getContext()); + } + } + return filter; + } + + /** + * Returns a search request for searching for a single entry in an LDAP, returning no attributes. + * + * @param template to execute + * @return search request + */ + protected SearchRequest createSearchRequest(final FilterTemplate template) { + return SearchRequest.builder() + .dn(baseDn) + .filter(template) + .returnAttributes( + resolveFromAttribute == null ? ReturnAttributes.NONE.value() : new String[]{resolveFromAttribute}) + .scope(subtreeSearch ? SearchScope.SUBTREE : SearchScope.ONELEVEL) + .aliases(derefAliases) + .build(); + } + + /** + * Executes the ldap search operation with the supplied filter. + * + * @param template to execute + * @return ldap search result + * @throws LdapException if an error occurs + */ + protected SearchResponse performLdapSearch(final FilterTemplate template) + throws LdapException { + final SearchRequest request = createSearchRequest(template); + final SearchOperation op = createSearchOperation(); + return op.execute(request); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "factory=" + getConnectionFactory() + ", " + + "baseDn=" + baseDn + ", " + + "userFilter=" + userFilter + ", " + + "userFilterParameters=" + Arrays.toString(userFilterParameters) + ", " + + "allowMultipleDns=" + allowMultipleDns + ", " + + "subtreeSearch=" + subtreeSearch + ", " + + "derefAliases=" + derefAliases + ", " + + "resolveDnFromAttribute=" + resolveFromAttribute + "]"; + } + + /** + * Search DN resolver builder. + */ + public static class Builder { + + /** + * DN resolver to build. + */ + private final SearchDnResolver object = new SearchDnResolver(); + + + /** + * Default constructor. + */ + protected Builder() { + } + + + /** + * Sets the connection factory. + * + * @param factory connection factory + * @return this builder + */ + public Builder factory(final ConnectionFactory factory) { + object.setConnectionFactory(factory); + return this; + } + + + /** + * Sets the base DN. + * + * @param dn base DN + * @return this builder + */ + public Builder dn(final String dn) { + object.setBaseDn(dn); + return this; + } + + + /** + * Sets the user filter. + * + * @param filter suer filter + * @return this builder + */ + public Builder filter(final String filter) { + object.setUserFilter(filter); + return this; + } + + + /** + * Sets the user filter parameters. + * + * @param params filter parameters + * @return this builder + */ + public Builder filterParameters(final Object... params) { + object.setUserFilterParameters(params); + return this; + } + + + /** + * Sets whether to allow multiple DNs. + * + * @param multipleDns whether to allow multiple DNs + * @return this builder + */ + public Builder allowMultipleDns(final boolean multipleDns) { + object.setAllowMultipleDns(multipleDns); + return this; + } + + + /** + * Sets whether to perform a subtree search or a onelevel search. + * + * @param b whether to perform a subtree search or a onelevel search + * @return this builder + */ + public Builder subtreeSearch(final boolean b) { + object.setSubtreeSearch(b); + return this; + } + + + /** + * Sets the deref aliases flag. + * + * @param aliases deref aliases + * @return this builder + */ + public Builder aliases(final DerefAliases aliases) { + object.setDerefAliases(aliases); + return this; + } + + /** + * Sets the attribute to use to resolve the DN. + * + * @param attributeName attribute name + * @return this builder + */ + public Builder resolveFromAttribute(final String attributeName) { + object.setResolveFromAttribute(attributeName); + return this; + } + + + /** + * Returns the search DN resolver. + * + * @return search DN resolver + */ + public SearchDnResolver build() { + return object; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/SearchEntryResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/SearchEntryResolver.java new file mode 100644 index 0000000..38136fd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/SearchEntryResolver.java @@ -0,0 +1,65 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Arrays; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.ConnectionFactoryManager; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchResponse; + +/** + * Looks up the LDAP entry associated with a user. If a connection factory is configured it will be used to perform the + * search for user. The connection will be opened and closed for each resolution. If no connection factory is configured + * the search will occur using the connection that the bind was attempted on. + * + */ +public class SearchEntryResolver extends AbstractSearchEntryResolver implements ConnectionFactoryManager { + + + /** + * Default constructor. + */ + public SearchEntryResolver() { + } + + + /** + * Creates a new search entry resolver. + * + * @param cf connection factory + */ + public SearchEntryResolver(final ConnectionFactory cf) { + setConnectionFactory(cf); + } + + + @Override + public SearchResponse performLdapSearch( + final AuthenticationCriteria criteria, + final AuthenticationHandlerResponse response) + throws LdapException { + if (getConnectionFactory() == null) { + return response.getConnection().operation(createSearchRequest(criteria)).execute(); + } else { + final SearchOperation op = createSearchOperation(); + return op.execute(createSearchRequest(criteria)); + } + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "factory=" + getConnectionFactory() + ", " + + "baseDn=" + getBaseDn() + ", " + + "userFilter=" + getUserFilter() + ", " + + "userFilterParameters=" + Arrays.toString(getUserFilterParameters()) + ", " + + "allowMultipleEntries=" + getAllowMultipleEntries() + ", " + + "subtreeSearch=" + getSubtreeSearch() + ", " + + "derefAliases=" + getDerefAliases() + ", " + + "binaryAttributes=" + Arrays.toString(getBinaryAttributes()) + ", " + + "entryHandlers=" + Arrays.toString(getEntryHandlers()) + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/SimpleBindAuthenticationHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/SimpleBindAuthenticationHandler.java new file mode 100644 index 0000000..4c6c4c3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/SimpleBindAuthenticationHandler.java @@ -0,0 +1,60 @@ + +package org.xbib.net.ldap.auth; + +import java.util.Arrays; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.ConnectionFactoryManager; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.SimpleBindRequest; + +/** + * Provides an LDAP authentication implementation that leverages the LDAP bind operation. + * + */ +public class SimpleBindAuthenticationHandler extends AbstractAuthenticationHandler implements ConnectionFactoryManager { + + + /** + * Default constructor. + */ + public SimpleBindAuthenticationHandler() { + } + + + /** + * Creates a new simple bind authentication handler. + * + * @param cf connection factory + */ + public SimpleBindAuthenticationHandler(final ConnectionFactory cf) { + setConnectionFactory(cf); + } + + + @Override + protected AuthenticationHandlerResponse authenticateInternal( + final Connection c, + final AuthenticationCriteria criteria) + throws LdapException { + final SimpleBindRequest request = new SimpleBindRequest(criteria.getDn(), criteria.getCredential().getString()); + request.setControls(processRequestControls(criteria)); + final Result bindResult = c.operation(request).execute(); + return new AuthenticationHandlerResponse( + bindResult, + bindResult.isSuccess() ? + AuthenticationResultCode.AUTHENTICATION_HANDLER_SUCCESS : + AuthenticationResultCode.AUTHENTICATION_HANDLER_FAILURE, + c); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "factory=" + getConnectionFactory() + ", " + + "controls=" + Arrays.toString(getAuthenticationControls()) + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/User.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/User.java new file mode 100644 index 0000000..e91ad6d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/User.java @@ -0,0 +1,70 @@ + +package org.xbib.net.ldap.auth; + +/** + * Encapsulates the data needed to perform authentication for a user. + * + */ +public class User { + + /** + * User identifier. + */ + private final String identifier; + + /** + * User context. + */ + private final Object context; + + + /** + * Creates a new user. + * + * @param id user identifier + */ + public User(final String id) { + this(id, null); + } + + + /** + * Creates a new user. + * + * @param id user identifier + * @param ctx user context + */ + public User(final String id, final Object ctx) { + identifier = id; + context = ctx; + } + + + /** + * Returns the user identifier. + * + * @return user identifier + */ + public String getIdentifier() { + return identifier; + } + + + /** + * Returns the user context. + * + * @return user context + */ + public Object getContext() { + return context; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "identifier=" + identifier + ", " + + "context=" + context + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/WhoAmIEntryResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/WhoAmIEntryResolver.java new file mode 100644 index 0000000..4fea73d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/WhoAmIEntryResolver.java @@ -0,0 +1,54 @@ + +package org.xbib.net.ldap.auth; + +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.extended.ExtendedResponse; +import org.xbib.net.ldap.extended.WhoAmIRequest; +import org.xbib.net.ldap.extended.WhoAmIResponseParser; + +/** + * Executes the whoami extended operation on the authenticated connection, then performs an object level search + * on the result. Useful when users authenticate with some mapped identifier, like DIGEST-MD5. + * + */ +public class WhoAmIEntryResolver extends AbstractSearchEntryResolver { + + + @Override + protected SearchResponse performLdapSearch( + final AuthenticationCriteria criteria, + final AuthenticationHandlerResponse response) + throws LdapException { + final ExtendedResponse whoamiRes = response.getConnection().operation(new WhoAmIRequest()).execute(); + + if (!whoamiRes.isSuccess()) { + throw new LdapException("Unsuccessful WhoAmI operation: " + whoamiRes); + } + final String authzId = WhoAmIResponseParser.parse(whoamiRes); + if (authzId == null || !authzId.contains(":")) { + throw new IllegalStateException("WhoAmI operation returned illegal authorization ID: '" + authzId + "'"); + } + + final String dn = authzId.split(":", 2)[1].trim(); + return response.getConnection().operation(createSearchRequest(criteria, dn)).execute(); + } + + + /** + * Returns a search request for an object level search for the supplied DN. + * + * @param ac authentication criteria containing return attributes + * @param dn from the who am i operation + * @return search request + */ + protected SearchRequest createSearchRequest(final AuthenticationCriteria ac, final String dn) { + final SearchRequest request = SearchRequest.objectScopeSearchRequest( + dn, + ac.getAuthenticationRequest().getReturnAttributes()); + request.setDerefAliases(getDerefAliases()); + request.setBinaryAttributes(getBinaryAttributes()); + return request; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/ActiveDirectoryAccountState.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/ActiveDirectoryAccountState.java new file mode 100644 index 0000000..5d4c31a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/ActiveDirectoryAccountState.java @@ -0,0 +1,217 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.ZonedDateTime; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.security.auth.login.AccountException; +import javax.security.auth.login.AccountExpiredException; +import javax.security.auth.login.AccountLockedException; +import javax.security.auth.login.AccountNotFoundException; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.auth.AccountState; + +/** + * Represents the state of an Active Directory account. Note that the warning returned by this implementation always + * returns -1 for logins remaining. + * + */ +public class ActiveDirectoryAccountState extends AccountState { + + + /** + * active directory specific enum. + */ + private final Error adError; + + /** + * Creates a new active directory account state. + * + * @param exp account expiration + */ + public ActiveDirectoryAccountState(final ZonedDateTime exp) { + super(new AccountState.DefaultWarning(exp, -1)); + adError = null; + } + + + /** + * Creates a new active directory account state. + * + * @param error containing authentication failure details + */ + public ActiveDirectoryAccountState(final ActiveDirectoryAccountState.Error error) { + super(error); + adError = error; + } + + /** + * Returns the active directory error for this account state. + * + * @return active directory error + */ + public ActiveDirectoryAccountState.Error getActiveDirectoryError() { + return adError; + } + + + /** + * Enum to define active directory errors. See http://ldapwiki.willeke.com/wiki/ + * Common%20Active%20Directory%20Bind%20Errors + */ + public enum Error implements AccountState.Error { + + /** + * no such user. 0x525. + */ + NO_SUCH_USER(1317), + + /** + * logon failure. 0x52e. + */ + LOGON_FAILURE(1326), + + /** + * invalid logon hours. 0x530. + */ + INVALID_LOGON_HOURS(1328), + + /** + * invalid workstation. 0x531. + */ + INVALID_WORKSTATION(1329), + + /** + * password expired. 0x532. + */ + PASSWORD_EXPIRED(1330), + + /** + * account disabled. 0x533. + */ + ACCOUNT_DISABLED(1331), + + /** + * account expired. 0x701. + */ + ACCOUNT_EXPIRED(1793), + + /** + * password must change. 0x773. + */ + PASSWORD_MUST_CHANGE(1907), + + /** + * account locked out. 0x775. + */ + ACCOUNT_LOCKED_OUT(1909); + + /** + * hex radix for hex to decimal conversion. + */ + private static final int HEX_RADIX = 16; + + /** + * pattern to find hex code in active directory messages. + */ + private static final Pattern PATTERN = Pattern.compile("data (\\w+)"); + + /** + * underlying error code. + */ + private final int code; + + + /** + * Creates a new active directory error. + * + * @param i error code + */ + Error(final int i) { + code = i; + } + + /** + * Returns the error for the supplied integer constant. + * + * @param code to find error for + * @return error + */ + public static Error valueOf(final int code) { + for (Error e : Error.values()) { + if (e.getCode() == code) { + return e; + } + } + return null; + } + + /** + * Parses the supplied error messages and returns the corresponding error enum. Attempts to find {@link #PATTERN} + * and parses the first group match as a hexadecimal integer. + * + * @param message to parse + * @return active directory error + */ + public static Error parse(final String message) { + if (message != null) { + final Matcher matcher = PATTERN.matcher(message); + if (matcher.find()) { + try { + return Error.valueOf(Integer.parseInt(LdapUtils.toUpperCaseAscii(matcher.group(1)), HEX_RADIX)); + } catch (NumberFormatException e) { + // + } + } + } + return null; + } + + @Override + public int getCode() { + return code; + } + + @Override + public String getMessage() { + return name(); + } + + @Override + public void throwSecurityException() + throws LoginException { + switch (this) { + + case NO_SUCH_USER: + throw new AccountNotFoundException(name()); + + case LOGON_FAILURE: + throw new FailedLoginException(name()); + + case INVALID_LOGON_HOURS: + + case ACCOUNT_DISABLED: + + case ACCOUNT_LOCKED_OUT: + throw new AccountLockedException(name()); + + case INVALID_WORKSTATION: + throw new AccountException(name()); + + case PASSWORD_EXPIRED: + + case PASSWORD_MUST_CHANGE: + throw new CredentialExpiredException(name()); + + case ACCOUNT_EXPIRED: + throw new AccountExpiredException(name()); + + default: + throw new IllegalStateException("Unknown active directory error: " + this); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/ActiveDirectoryAuthenticationResponseHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/ActiveDirectoryAuthenticationResponseHandler.java new file mode 100644 index 0000000..d31e1d9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/ActiveDirectoryAuthenticationResponseHandler.java @@ -0,0 +1,153 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.Period; +import java.time.ZonedDateTime; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.ad.transcode.FileTimeValueTranscoder; +import org.xbib.net.ldap.auth.AuthenticationResponse; +import org.xbib.net.ldap.auth.AuthenticationResponseHandler; + +/** + * Attempts to parse the authentication response message and set the account state using data associated with active + * directory. If this handler is assigned a {@link #expirationPeriod}, then the {@link org.xbib.net.ldap.auth.Authenticator} + * should be configured to return the 'pwdLastSet' attribute, so it can be consumed by this handler. This will cause the + * handler to emit a warning for the pwdLastSet value plus the expiration amount. The scope of that warning can be + * further narrowed by providing a {@link #warningPeriod}. By default, if the msDS-UserPasswordExpiryTimeComputed + * attribute is found, expirationPeriod is ignored. + * + */ +public class ActiveDirectoryAuthenticationResponseHandler implements AuthenticationResponseHandler { + + /** + * Attributes needed to enforce password policy. + */ + public static final String[] ATTRIBUTES = new String[]{"msDS-UserPasswordExpiryTimeComputed", "pwdLastSet",}; + + /** + * Amount of time since a password was set until it will expire. Used if msDS-UserPasswordExpiryTimeComputed cannot + * be read. + */ + private Period expirationPeriod; + + /** + * Amount of time before expiration to produce a warning. + */ + private Period warningPeriod; + + + /** + * Default constructor. + */ + public ActiveDirectoryAuthenticationResponseHandler() { + } + + + /** + * Creates a new active directory authentication response handler. + * + * @param warning length of time before expiration that should produce a warning + */ + public ActiveDirectoryAuthenticationResponseHandler(final Period warning) { + setWarningPeriod(warning); + } + + + /** + * Creates a new active directory authentication response handler. + * + * @param expiration length of time that a password is valid + * @param warning length of time before expiration that should produce a warning + */ + public ActiveDirectoryAuthenticationResponseHandler(final Period expiration, final Period warning) { + setExpirationPeriod(expiration); + setWarningPeriod(warning); + } + + + @Override + public void handle(final AuthenticationResponse response) { + if (response.isSuccess()) { + final LdapEntry entry = response.getLdapEntry(); + final LdapAttribute expTime = entry.getAttribute("msDS-UserPasswordExpiryTimeComputed"); + final LdapAttribute pwdLastSet = entry.getAttribute("pwdLastSet"); + + ZonedDateTime exp = null; + // ignore expTime if account is set to never expire + if (expTime != null && !"9223372036854775807".equals(expTime.getStringValue())) { + exp = expTime.getValue(new FileTimeValueTranscoder().decoder()); + } else if (expirationPeriod != null && pwdLastSet != null) { + exp = pwdLastSet.getValue(new FileTimeValueTranscoder().decoder()).plus(expirationPeriod); + } + + if (exp != null) { + if (warningPeriod != null) { + final ZonedDateTime warn = exp.minus(warningPeriod); + if (ZonedDateTime.now().isAfter(warn)) { + response.setAccountState(new ActiveDirectoryAccountState(exp)); + } + } else { + response.setAccountState(new ActiveDirectoryAccountState(exp)); + } + } + } else { + if (response.getDiagnosticMessage() != null) { + final ActiveDirectoryAccountState.Error adError = ActiveDirectoryAccountState.Error.parse( + response.getDiagnosticMessage()); + if (adError != null) { + response.setAccountState(new ActiveDirectoryAccountState(adError)); + } + } + } + } + + + /** + * Returns the amount of time since a password was set until it will expire. + * + * @return expiration period + */ + public Period getExpirationPeriod() { + return expirationPeriod; + } + + + /** + * Sets amount of time since a password was set until it will expire. + * + * @param period expiration period + */ + public void setExpirationPeriod(final Period period) { + expirationPeriod = period; + } + + + /** + * Returns the amount of time before expiration to produce a warning. + * + * @return warning period + */ + public Period getWarningPeriod() { + return warningPeriod; + } + + + /** + * Sets the amount of time before expiration to produce a warning. + * + * @param period warning period + */ + public void setWarningPeriod(final Period period) { + warningPeriod = period; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "expirationPeriod=" + expirationPeriod + ", " + + "warningPeriod=" + warningPeriod + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/EDirectoryAccountState.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/EDirectoryAccountState.java new file mode 100644 index 0000000..341dae9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/EDirectoryAccountState.java @@ -0,0 +1,192 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.ZonedDateTime; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.security.auth.login.AccountExpiredException; +import javax.security.auth.login.AccountLockedException; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import org.xbib.net.ldap.auth.AccountState; + +/** + * Represents the state of an eDirectory account. + * + */ +public class EDirectoryAccountState extends AccountState { + + /** + * edirectory specific enum. + */ + private final Error edError; + + /** + * Creates a new edirectory account state. + * + * @param exp account expiration + * @param remaining number of logins available + */ + public EDirectoryAccountState(final ZonedDateTime exp, final int remaining) { + super(new AccountState.DefaultWarning(exp, remaining)); + edError = null; + } + + + /** + * Creates a new edirectory account state. + * + * @param error containing authentication failure details + */ + public EDirectoryAccountState(final EDirectoryAccountState.Error error) { + super(error); + edError = error; + } + + /** + * Returns the edirectory error for this account state. + * + * @return edirectory error + */ + public EDirectoryAccountState.Error getEDirectoryError() { + return edError; + } + + + /** + * Enum to define edirectory errors. See http://support.novell.com/docs/Tids/Solutions/10067240.html and + * http://www.novell.com/documentation/nwec/nwec_enu/nwec_nds_error_codes.html + */ + public enum Error implements AccountState.Error { + + /** + * failed authentication. + */ + FAILED_AUTHENTICATION(-669), + + /** + * password expired. binds still succeed. + */ + PASSWORD_EXPIRED(-223), + + /** + * bad password. + */ + BAD_PASSWORD(-222), + + /** + * account expired. + */ + ACCOUNT_EXPIRED(-220), + + /** + * maximum logins exceeded. + */ + MAXIMUM_LOGINS_EXCEEDED(-217), + + /** + * login time limited. + */ + LOGIN_TIME_LIMITED(-218), + + /** + * login lockout. + */ + LOGIN_LOCKOUT(-197); + + /** + * pattern to find decimal code in edirectory messages. + */ + private static final Pattern PATTERN = Pattern.compile("NDS error: (.+) \\((-\\d+)\\)"); + + /** + * underlying error code. + */ + private final int code; + + + /** + * Creates a new edirectory error. + * + * @param i error code + */ + Error(final int i) { + code = i; + } + + /** + * Returns the error for the supplied integer constant. + * + * @param code to find error for + * @return error + */ + public static Error valueOf(final int code) { + for (Error e : Error.values()) { + if (e.getCode() == code) { + return e; + } + } + return null; + } + + /** + * Parses the supplied error messages and returns the corresponding error enum. Attempts to find {@link #PATTERN} + * and parses the second group match as a decimal integer. + * + * @param message to parse + * @return edirectory error + */ + public static Error parse(final String message) { + if (message != null) { + final Matcher matcher = PATTERN.matcher(message); + if (matcher.find()) { + try { + return Error.valueOf(Integer.parseInt(matcher.group(2))); + } catch (NumberFormatException e) { + // + } + } + } + return null; + } + + @Override + public int getCode() { + return code; + } + + @Override + public String getMessage() { + return name(); + } + + @Override + public void throwSecurityException() + throws LoginException { + switch (this) { + + case FAILED_AUTHENTICATION: + + case BAD_PASSWORD: + throw new FailedLoginException(name()); + + case PASSWORD_EXPIRED: + throw new CredentialExpiredException(name()); + + case ACCOUNT_EXPIRED: + throw new AccountExpiredException(name()); + + case MAXIMUM_LOGINS_EXCEEDED: + + case LOGIN_TIME_LIMITED: + + case LOGIN_LOCKOUT: + throw new AccountLockedException(name()); + + default: + throw new IllegalStateException("Unknown edirectory error: " + this); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/EDirectoryAuthenticationResponseHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/EDirectoryAuthenticationResponseHandler.java new file mode 100644 index 0000000..dd92f38 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/EDirectoryAuthenticationResponseHandler.java @@ -0,0 +1,104 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.Period; +import java.time.ZonedDateTime; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.auth.AuthenticationResponse; +import org.xbib.net.ldap.auth.AuthenticationResponseHandler; +import org.xbib.net.ldap.transcode.GeneralizedTimeValueTranscoder; + +/** + * Attempts to parse the authentication response and set the account state using data associated with eDirectory. The + * {@link org.xbib.net.ldap.auth.Authenticator} should be configured to return 'passwordExpirationTime' and + * 'loginGraceRemaining' attributes, so they can be consumed by this handler. If this handler is assigned a {@link + * #warningPeriod}, this handler will only emit warnings during that window before password expiration. Otherwise, + * a warning is always emitted if passwordExpirationTime is set. + * + */ +public class EDirectoryAuthenticationResponseHandler implements AuthenticationResponseHandler { + + /** + * Attributes needed to enforce password policy. + */ + public static final String[] ATTRIBUTES = new String[]{"passwordExpirationTime", "loginGraceRemaining",}; + + /** + * Amount of time before expiration to produce a warning. + */ + private Period warningPeriod; + + + /** + * Default constructor. + */ + public EDirectoryAuthenticationResponseHandler() { + } + + + /** + * Creates a new edirectory authentication response handler. + * + * @param warning length of time before expiration that should produce a warning + */ + public EDirectoryAuthenticationResponseHandler(final Period warning) { + setWarningPeriod(warning); + } + + + @Override + public void handle(final AuthenticationResponse response) { + if (response.getDiagnosticMessage() != null) { + final EDirectoryAccountState.Error edError = EDirectoryAccountState.Error.parse(response.getDiagnosticMessage()); + if (edError != null) { + response.setAccountState(new EDirectoryAccountState(edError)); + } + } else if (response.isSuccess()) { + final LdapEntry entry = response.getLdapEntry(); + final LdapAttribute expTime = entry.getAttribute("passwordExpirationTime"); + final LdapAttribute loginRemaining = entry.getAttribute("loginGraceRemaining"); + final int loginRemainingValue = loginRemaining != null ? Integer.parseInt(loginRemaining.getStringValue()) : 0; + + if (expTime != null) { + final ZonedDateTime exp = expTime.getValue(new GeneralizedTimeValueTranscoder().decoder()); + if (warningPeriod != null) { + final ZonedDateTime warn = exp.minus(warningPeriod); + if (ZonedDateTime.now().isAfter(warn)) { + response.setAccountState(new EDirectoryAccountState(exp, loginRemainingValue)); + } + } else { + response.setAccountState(new EDirectoryAccountState(exp, loginRemainingValue)); + } + } else if (loginRemaining != null) { + response.setAccountState(new EDirectoryAccountState(null, loginRemainingValue)); + } + } + } + + + /** + * Returns the amount of time before expiration to produce a warning. + * + * @return warning period + */ + public Period getWarningPeriod() { + return warningPeriod; + } + + + /** + * Sets the amount of time before expiration to produce a warning. + * + * @param period warning period + */ + public void setWarningPeriod(final Period period) { + warningPeriod = period; + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "warningPeriod=" + warningPeriod + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/FreeIPAAccountState.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/FreeIPAAccountState.java new file mode 100644 index 0000000..c96fceb --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/FreeIPAAccountState.java @@ -0,0 +1,222 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.ZonedDateTime; +import javax.security.auth.login.AccountExpiredException; +import javax.security.auth.login.AccountLockedException; +import javax.security.auth.login.AccountNotFoundException; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.auth.AccountState; + +/** + * Represents the state of a FreeIPA account. + * + * @author tduehr + */ +public class FreeIPAAccountState extends AccountState { + + + /** + * freeipa specific enum. + */ + private final Error fError; + + /** + * Creates a new freeipa account state. + * + * @param exp account expiration + * @param remaining number of logins available + */ + public FreeIPAAccountState(final ZonedDateTime exp, final int remaining) { + super(new AccountState.DefaultWarning(exp, remaining)); + fError = null; + } + + + /** + * Creates a new freeipa account state. + * + * @param error containing authentication failure details + */ + public FreeIPAAccountState(final FreeIPAAccountState.Error error) { + super(error); + fError = error; + } + + /** + * Returns the freeipa error for this account state. + * + * @return freeipa error + */ + public FreeIPAAccountState.Error getFreeIPAError() { + return fError; + } + + + /** + * Enum to define FreeIPA errors. + */ + public enum Error implements AccountState.Error { + + /** + * unknown state. + */ + UNKNOWN(-1), + + /** + * failed authentication. + */ + FAILED_AUTHENTICATION(1), + + /** + * password expired. + */ + PASSWORD_EXPIRED(2), + + /** + * account expired. + */ + ACCOUNT_EXPIRED(3), + + /** + * maximum logins exceeded. + */ + MAXIMUM_LOGINS_EXCEEDED(4), + + /** + * login time limited. + */ + LOGIN_TIME_LIMITED(5), + + /** + * login lockout. + */ + LOGIN_LOCKOUT(6), + + /** + * account not found. + */ + ACCOUNT_NOT_FOUND(7), + + /** + * credential not found. + */ + CREDENTIAL_NOT_FOUND(8), + + /** + * account disabled. + */ + ACCOUNT_DISABLED(9); + + /** + * underlying error code. + */ + private final int code; + + + /** + * Creates a new freeipa error. + * + * @param i error code + */ + Error(final int i) { + code = i; + } + + /** + * Returns the error for the supplied integer constant. + * + * @param code to find error for + * @return error + */ + public static Error valueOf(final int code) { + for (Error e : Error.values()) { + if (e.getCode() == code) { + return e; + } + } + return ResultCode.valueOf(code) == ResultCode.SUCCESS ? null : UNKNOWN; + } + + /** + * Parses the supplied error messages and returns the corresponding error enum. + * + * @param rc result code + * @param message to parse + * @return freeipa error + */ + public static Error parse(final ResultCode rc, final String message) { + Error error = null; + if (rc != null && rc != ResultCode.SUCCESS) { + if (rc == ResultCode.NO_SUCH_OBJECT) { + error = ACCOUNT_NOT_FOUND; + } else if (rc == ResultCode.INVALID_CREDENTIALS) { + error = CREDENTIAL_NOT_FOUND; + } else if (rc == ResultCode.INSUFFICIENT_ACCESS_RIGHTS) { + error = FAILED_AUTHENTICATION; + } else if (rc == ResultCode.UNWILLING_TO_PERFORM) { + if ("Entry permanently locked.\n".equals(message)) { + error = LOGIN_LOCKOUT; + } else if ("Too many failed logins.\n".equals(message)) { + error = MAXIMUM_LOGINS_EXCEEDED; + } else if ("Account (Kerberos principal) is expired".equals(message)) { + error = ACCOUNT_EXPIRED; + } else if ("Account inactivated. Contact system administrator.".equals(message)) { + error = ACCOUNT_DISABLED; + } + } else { + error = UNKNOWN; + } + } + return error; + } + + @Override + public int getCode() { + return code; + } + + @Override + public String getMessage() { + return name(); + } + + @Override + public void throwSecurityException() + throws LoginException { + switch (this) { + + case ACCOUNT_NOT_FOUND: + throw new AccountNotFoundException(name()); + + case FAILED_AUTHENTICATION: + + case ACCOUNT_DISABLED: + + case CREDENTIAL_NOT_FOUND: + + case UNKNOWN: + throw new FailedLoginException(name()); + + case PASSWORD_EXPIRED: + throw new CredentialExpiredException(name()); + + case ACCOUNT_EXPIRED: + throw new AccountExpiredException(name()); + + case MAXIMUM_LOGINS_EXCEEDED: + + case LOGIN_TIME_LIMITED: + + case LOGIN_LOCKOUT: + throw new AccountLockedException(name()); + + default: + throw new IllegalStateException("Unknown FreeIPA error: " + this); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/FreeIPAAuthenticationResponseHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/FreeIPAAuthenticationResponseHandler.java new file mode 100644 index 0000000..712f164 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/FreeIPAAuthenticationResponseHandler.java @@ -0,0 +1,198 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.Period; +import java.time.ZonedDateTime; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.auth.AuthenticationResponse; +import org.xbib.net.ldap.auth.AuthenticationResponseHandler; +import org.xbib.net.ldap.transcode.GeneralizedTimeValueTranscoder; + + +/** + * Attempts to parse the authentication response and set the account state using data associated with FreeIPA. The + * {@link org.xbib.net.ldap.auth.Authenticator} should be configured to return 'krbPasswordExpiration', + * 'krbLoginFailedCount' and 'krbLastPwdChange' attributes, so they can be consumed by this handler. + * + * @author tduehr + */ +public class FreeIPAAuthenticationResponseHandler implements AuthenticationResponseHandler { + + /** + * Attributes needed to enforce password policy. + */ + public static final String[] ATTRIBUTES = new String[]{ + "krbPasswordExpiration", + "krbLoginFailedCount", + "krbLastPwdChange", + }; + + /** + * Amount of time since a password was set until it will expire. Used if krbPasswordExpiration cannot be read. + */ + private Period expirationPeriod; + + /** + * Amount of time before expiration to produce a warning. + */ + private Period warningPeriod; + + /** + * Maximum number of login failures to allow. + */ + private int maxLoginFailures; + + + /** + * Default constructor. + */ + public FreeIPAAuthenticationResponseHandler() { + } + + + /** + * Creates a new freeipa authentication response handler. + * + * @param warning length of time before expiration that should produce a warning + * @param loginFailures number of login failures to allow + */ + public FreeIPAAuthenticationResponseHandler(final Period warning, final int loginFailures) { + setWarningPeriod(warning); + setMaxLoginFailures(loginFailures); + } + + + /** + * Creates a new freeipa authentication response handler. + * + * @param expiration length of time that a password is valid + * @param warning length of time before expiration that should produce a warning + * @param loginFailures number of login failures to allow + */ + public FreeIPAAuthenticationResponseHandler(final Period expiration, final Period warning, final int loginFailures) { + setExpirationPeriod(expiration); + setWarningPeriod(warning); + setMaxLoginFailures(loginFailures); + } + + + @Override + public void handle(final AuthenticationResponse response) { + if (response.getResultCode() != ResultCode.SUCCESS) { + final FreeIPAAccountState.Error fError = FreeIPAAccountState.Error.parse( + response.getResultCode(), + response.getDiagnosticMessage()); + if (fError != null) { + response.setAccountState(new FreeIPAAccountState(fError)); + } + } else if (response.isSuccess()) { + final LdapEntry entry = response.getLdapEntry(); + final LdapAttribute expTime = entry.getAttribute("krbPasswordExpiration"); + final LdapAttribute failedLogins = entry.getAttribute("krbLoginFailedCount"); + final LdapAttribute lastPwdChange = entry.getAttribute("krbLastPwdChange"); + ZonedDateTime exp = null; + + Integer loginRemaining = null; + if (failedLogins != null && maxLoginFailures > 0) { + loginRemaining = maxLoginFailures - Integer.parseInt(failedLogins.getStringValue()); + } + + if (expTime != null) { + exp = expTime.getValue(new GeneralizedTimeValueTranscoder().decoder()); + } else if (expirationPeriod != null && lastPwdChange != null) { + exp = lastPwdChange.getValue(new GeneralizedTimeValueTranscoder().decoder()).plus(expirationPeriod); + } + if (exp != null) { + if (warningPeriod != null) { + final ZonedDateTime warn = exp.minus(warningPeriod); + if (ZonedDateTime.now().isAfter(warn)) { + response.setAccountState( + new FreeIPAAccountState(exp, loginRemaining != null ? loginRemaining : 0)); + } + } else { + response.setAccountState( + new FreeIPAAccountState(exp, loginRemaining != null ? loginRemaining : 0)); + } + } else if (loginRemaining != null && loginRemaining < maxLoginFailures) { + response.setAccountState(new FreeIPAAccountState(null, loginRemaining)); + } + } + } + + + /** + * Returns the maximum login failures. + * + * @return maximum login failures before lockout. + */ + public int getMaxLoginFailures() { + return maxLoginFailures; + } + + + /** + * Sets the maximum login failures. + * + * @param loginFailures before lockout. + */ + public void setMaxLoginFailures(final int loginFailures) { + if (loginFailures < 0) { + throw new IllegalArgumentException("Login failures must be >= 0"); + } + maxLoginFailures = loginFailures; + } + + + /** + * Returns the amount of time since a password was set until it will expire. Only used if the krbPasswordExpiration + * attribute cannot be read from the directory. + * + * @return expiration period + */ + public Period getExpirationPeriod() { + return expirationPeriod; + } + + + /** + * Sets the amount of time since a password was set until it will expire. Only used if the krbPasswordExpiration + * attribute cannot be read from the directory. + * + * @param period expiration period + */ + public void setExpirationPeriod(final Period period) { + expirationPeriod = period; + } + + + /** + * Returns the amount of time before expiration to produce a warning. + * + * @return warning period + */ + public Period getWarningPeriod() { + return warningPeriod; + } + + + /** + * Sets the amount of time before expiration to produce a warning. + * + * @param period warning period + */ + public void setWarningPeriod(final Period period) { + warningPeriod = period; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "expirationPeriod=" + expirationPeriod + ", " + + "warningPeriod=" + warningPeriod + ", " + + "maxLoginFailures=" + maxLoginFailures + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordExpirationAccountState.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordExpirationAccountState.java new file mode 100644 index 0000000..f389cd5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordExpirationAccountState.java @@ -0,0 +1,83 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.ZonedDateTime; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.LoginException; +import org.xbib.net.ldap.auth.AccountState; + +/** + * Represents the state of an account in a directory that implements: + * http://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00. Note that the warning returned by this implementation + * always returns -1 for logins remaining as this specification doesn't include that feature. + * + */ +public class PasswordExpirationAccountState extends AccountState { + + + /** + * error enum. + */ + private final Error nError; + + /** + * Creates a new password expiration account state. + * + * @param exp account expiration + */ + public PasswordExpirationAccountState(final ZonedDateTime exp) { + super(new AccountState.DefaultWarning(exp, -1)); + nError = null; + } + + + /** + * Creates a new password expiration account state. + * + * @param error containing authentication failure details + */ + public PasswordExpirationAccountState(final PasswordExpirationAccountState.Error error) { + super(error); + nError = error; + } + + /** + * Returns the password expiration error for this account state. + * + * @return password expiration error + */ + public PasswordExpirationAccountState.Error getPasswordExpirationError() { + return nError; + } + + + /** + * Enum to define password expiration error. + */ + public enum Error implements AccountState.Error { + + /** + * password expired. + */ + PASSWORD_EXPIRED; + + + @Override + public int getCode() { + return 0; + } + + + @Override + public String getMessage() { + return name(); + } + + + @Override + public void throwSecurityException() + throws LoginException { + throw new CredentialExpiredException(name()); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordExpirationAuthenticationResponseHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordExpirationAuthenticationResponseHandler.java new file mode 100644 index 0000000..c038ebf --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordExpirationAuthenticationResponseHandler.java @@ -0,0 +1,40 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.ZonedDateTime; +import org.xbib.net.ldap.auth.AuthenticationResponse; +import org.xbib.net.ldap.auth.AuthenticationResponseHandler; +import org.xbib.net.ldap.control.PasswordExpiredControl; +import org.xbib.net.ldap.control.PasswordExpiringControl; + +/** + * Attempts to parse the authentication response and set the account state using data associated with the password + * expiring and password expired controls. See http://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00. + * + */ +public class PasswordExpirationAuthenticationResponseHandler implements AuthenticationResponseHandler { + + + @Override + public void handle(final AuthenticationResponse response) { + final PasswordExpiringControl expiringControl = (PasswordExpiringControl) response.getControl( + PasswordExpiringControl.OID); + if (expiringControl != null) { + if (expiringControl.getTimeBeforeExpiration() > 0) { + final ZonedDateTime exp = ZonedDateTime.now().plusSeconds(expiringControl.getTimeBeforeExpiration()); + response.setAccountState(new PasswordExpirationAccountState(exp)); + } else { + // + } + } + + if (response.getAccountState() == null) { + final PasswordExpiredControl expiredControl = (PasswordExpiredControl) response.getControl( + PasswordExpiredControl.OID); + if (expiredControl != null) { + response.setAccountState( + new PasswordExpirationAccountState(PasswordExpirationAccountState.Error.PASSWORD_EXPIRED)); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAccountState.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAccountState.java new file mode 100644 index 0000000..bb7908e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAccountState.java @@ -0,0 +1,51 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.ZonedDateTime; +import org.xbib.net.ldap.auth.AccountState; +import org.xbib.net.ldap.control.PasswordPolicyControl; + +/** + * Represents the state of an account as described by a password policy control. + * + */ +public class PasswordPolicyAccountState extends AccountState { + + /** + * password policy specific enum. + */ + private final PasswordPolicyControl.Error ppError; + + + /** + * Creates a new password policy account state. + * + * @param exp account expiration + * @param remaining number of logins available + */ + public PasswordPolicyAccountState(final ZonedDateTime exp, final int remaining) { + super(new AccountState.DefaultWarning(exp, remaining)); + ppError = null; + } + + + /** + * Creates a new password policy account state. + * + * @param error containing password policy error details + */ + public PasswordPolicyAccountState(final PasswordPolicyControl.Error error) { + super(error); + ppError = error; + } + + + /** + * Returns the password policy error for this account state. + * + * @return password policy error + */ + public PasswordPolicyControl.Error getPasswordPolicyError() { + return ppError; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAuthenticationRequestHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAuthenticationRequestHandler.java new file mode 100644 index 0000000..271bba9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAuthenticationRequestHandler.java @@ -0,0 +1,21 @@ + +package org.xbib.net.ldap.auth.ext; + +import org.xbib.net.ldap.auth.AddControlAuthenticationRequestHandler; +import org.xbib.net.ldap.control.PasswordPolicyControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Adds the {@link PasswordPolicyControl} to the {@link org.xbib.net.ldap.auth.AuthenticationRequest}. + * + */ +public class PasswordPolicyAuthenticationRequestHandler extends AddControlAuthenticationRequestHandler { + + + /** + * Creates a new password policy authentication request handler + */ + public PasswordPolicyAuthenticationRequestHandler() { + super((dn, user) -> new RequestControl[]{new PasswordPolicyControl()}); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAuthenticationResponseHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAuthenticationResponseHandler.java new file mode 100644 index 0000000..8d32a0f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/auth/ext/PasswordPolicyAuthenticationResponseHandler.java @@ -0,0 +1,34 @@ + +package org.xbib.net.ldap.auth.ext; + +import java.time.ZonedDateTime; +import org.xbib.net.ldap.auth.AuthenticationResponse; +import org.xbib.net.ldap.auth.AuthenticationResponseHandler; +import org.xbib.net.ldap.control.PasswordPolicyControl; + +/** + * Attempts to parse the authentication response message and set the account state using data associated with a password + * policy control. + * + */ +public class PasswordPolicyAuthenticationResponseHandler implements AuthenticationResponseHandler { + + + @Override + public void handle(final AuthenticationResponse response) { + final PasswordPolicyControl ppc = (PasswordPolicyControl) response.getControl(PasswordPolicyControl.OID); + if (ppc != null) { + if (ppc.getError() != null) { + response.setAccountState(new PasswordPolicyAccountState(ppc.getError())); + } else { + ZonedDateTime exp = null; + if (ppc.getTimeBeforeExpiration() >= 0) { + exp = ZonedDateTime.now().plusSeconds(ppc.getTimeBeforeExpiration()); + } + if (exp != null || ppc.getGraceAuthNsRemaining() >= 0) { + response.setAccountState(new PasswordPolicyAccountState(exp, ppc.getGraceAuthNsRemaining())); + } + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/AbstractOperationWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/AbstractOperationWorker.java new file mode 100644 index 0000000..1a53291 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/AbstractOperationWorker.java @@ -0,0 +1,104 @@ + +package org.xbib.net.ldap.concurrent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Operation; +import org.xbib.net.ldap.OperationHandle; +import org.xbib.net.ldap.Request; +import org.xbib.net.ldap.Result; + +/** + * Base class for worker operations. + * + * @param type of operation + * @param type of ldap request + * @param type of ldap response + */ +public abstract class AbstractOperationWorker, Q extends Request, S extends Result> + implements OperationWorker { + + /** + * operation to execute. + */ + private T operation; + + + /** + * Creates a new abstract operation worker. + * + * @param op operation + */ + public AbstractOperationWorker(final T op) { + setOperation(op); + } + + + /** + * Returns the underlying operation. + * + * @return operation + */ + public T getOperation() { + return operation; + } + + + /** + * Sets the underlying operation. + * + * @param op to set + */ + public void setOperation(final T op) { + operation = op; + } + + + /** + * Execute an ldap operation for each request on a separate thread. + * + * @param requests containing the data required by this operation + * @return future responses for this operation + */ + @Override + public Collection> send(final Q[] requests) { + final List> results = new ArrayList<>(requests.length); + for (Q request : requests) { + try { + results.add(operation.send(request)); + } catch (LdapException e) { + // + } + } + return results; + } + + + /** + * Execute an ldap operation for each request on a separate thread and waits for all operations to complete. + * + * @param requests containing the data required by this operation + * @return responses for this operation + */ + @Override + public Collection execute(final Q[] requests) { + final List responses = new ArrayList<>(requests.length); + final Collection> handles = send(requests); + for (OperationHandle handle : handles) { + try { + responses.add(handle.await()); + } catch (LdapException e) { + // + } + } + return responses; + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + "operation=" + operation; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/AddOperationWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/AddOperationWorker.java new file mode 100644 index 0000000..4371ea1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/AddOperationWorker.java @@ -0,0 +1,31 @@ + +package org.xbib.net.ldap.concurrent; + +import org.xbib.net.ldap.AddOperation; +import org.xbib.net.ldap.AddRequest; +import org.xbib.net.ldap.AddResponse; + +/** + * Executes multiple ldap add operations asynchronously. + * + */ +public class AddOperationWorker extends AbstractOperationWorker { + + + /** + * Default constructor. + */ + public AddOperationWorker() { + super(new AddOperation()); + } + + + /** + * Creates a new add operation worker. + * + * @param op add operation to execute + */ + public AddOperationWorker(final AddOperation op) { + super(op); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/CallableWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/CallableWorker.java new file mode 100644 index 0000000..b122012 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/CallableWorker.java @@ -0,0 +1,118 @@ + +package org.xbib.net.ldap.concurrent; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Executes callable tasks asynchronously. + * + * @param type of result from the callable + */ +public class CallableWorker { + + /** + * Default size of the thread pool. + */ + private static final int DEFAULT_NUM_THREADS = Runtime.getRuntime().availableProcessors() * 2; + + /** + * Executor service. + */ + private final ExecutorService executorService; + + + /** + * Creates a new callable worker with a fixed sized thread pool. The size of the thread pool is set to twice the + * number of available processors. See {@link Runtime#availableProcessors()}. + * + * @param poolName name to designate on the thread pool + */ + public CallableWorker(final String poolName) { + this(poolName, DEFAULT_NUM_THREADS); + } + + + /** + * Creates a new callable worker with a fixed sized thread pool. + * + * @param poolName name to designate on the thread pool + * @param numThreads size of the thread pool + */ + public CallableWorker(final String poolName, final int numThreads) { + executorService = Executors.newFixedThreadPool( + numThreads, + r -> { + final Thread t = new Thread(r, "org.xbib.net.ldap-" + poolName + "@" + hashCode()); + t.setDaemon(true); + return t; + }); + } + + + /** + * Creates a new callable worker. + * + * @param es executor service to run callables + */ + public CallableWorker(final ExecutorService es) { + executorService = es; + } + + + /** + * Executes all callables and provides each result to the supplied consumer. + * + * @param callable callable to execute + * @param count number of times to execute the supplied callable + * @param consumer to process callable results + * @return list of exceptions thrown during the execution + */ + public List execute(final Callable callable, final int count, final Consumer consumer) { + return execute(IntStream.range(0, count).mapToObj(i -> callable).collect(Collectors.toList()), consumer); + } + + + /** + * Executes all callables and provides each result to the supplied consumer. Note that the consumer is invoked in a + * synchronous fashion, waiting for each result from the callables. + * + * @param callables callables to execute + * @param consumer to process callable results + * @return list of exceptions thrown during the execution + */ + public List execute(final List> callables, final Consumer consumer) { + final CompletionService cs = new ExecutorCompletionService<>(executorService); + callables.forEach(cs::submit); + final List exceptions = new ArrayList<>(callables.size()); + for (int i = 0; i < callables.size(); i++) { + try { + // blocks until a result is received + final T result = cs.take().get(); + consumer.accept(result); + } catch (ExecutionException e) { + exceptions.add(e); + } catch (InterruptedException e) { + exceptions.add(new ExecutionException(e)); + } + } + return exceptions; + } + + + /** + * Shutdown the underlying executor service. + */ + public void shutdown() { + executorService.shutdown(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/CompareOperationWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/CompareOperationWorker.java new file mode 100644 index 0000000..c06c0a9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/CompareOperationWorker.java @@ -0,0 +1,31 @@ + +package org.xbib.net.ldap.concurrent; + +import org.xbib.net.ldap.CompareOperation; +import org.xbib.net.ldap.CompareRequest; +import org.xbib.net.ldap.CompareResponse; + +/** + * Executes multiple ldap compare operations asynchronously. + * + */ +public class CompareOperationWorker extends AbstractOperationWorker { + + + /** + * Default constructor. + */ + public CompareOperationWorker() { + super(new CompareOperation()); + } + + + /** + * Creates a new compare operation worker. + * + * @param op compare operation to execute + */ + public CompareOperationWorker(final CompareOperation op) { + super(op); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/DeleteOperationWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/DeleteOperationWorker.java new file mode 100644 index 0000000..668562f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/DeleteOperationWorker.java @@ -0,0 +1,31 @@ + +package org.xbib.net.ldap.concurrent; + +import org.xbib.net.ldap.DeleteOperation; +import org.xbib.net.ldap.DeleteRequest; +import org.xbib.net.ldap.DeleteResponse; + +/** + * Executes multiple ldap delete operations asynchronously. + * + */ +public class DeleteOperationWorker extends AbstractOperationWorker { + + + /** + * Default constructor. + */ + public DeleteOperationWorker() { + super(new DeleteOperation()); + } + + + /** + * Creates a new delete operation worker. + * + * @param op delete operation to execute + */ + public DeleteOperationWorker(final DeleteOperation op) { + super(op); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/ModifyDnOperationWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/ModifyDnOperationWorker.java new file mode 100644 index 0000000..4d9a0c3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/ModifyDnOperationWorker.java @@ -0,0 +1,32 @@ + +package org.xbib.net.ldap.concurrent; + +import org.xbib.net.ldap.ModifyDnOperation; +import org.xbib.net.ldap.ModifyDnRequest; +import org.xbib.net.ldap.ModifyDnResponse; + +/** + * Executes multiple ldap modify DN operations asynchronously. + * + */ +public class ModifyDnOperationWorker + extends AbstractOperationWorker { + + + /** + * Default constructor. + */ + public ModifyDnOperationWorker() { + super(new ModifyDnOperation()); + } + + + /** + * Creates a new modify dn operation worker. + * + * @param op modify dn operation to execute + */ + public ModifyDnOperationWorker(final ModifyDnOperation op) { + super(op); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/ModifyOperationWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/ModifyOperationWorker.java new file mode 100644 index 0000000..9cf4b57 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/ModifyOperationWorker.java @@ -0,0 +1,31 @@ + +package org.xbib.net.ldap.concurrent; + +import org.xbib.net.ldap.ModifyOperation; +import org.xbib.net.ldap.ModifyRequest; +import org.xbib.net.ldap.ModifyResponse; + +/** + * Executes multiple ldap modify operations asynchronously. + * + */ +public class ModifyOperationWorker extends AbstractOperationWorker { + + + /** + * Default constructor. + */ + public ModifyOperationWorker() { + super(new ModifyOperation()); + } + + + /** + * Creates a new modify operation worker. + * + * @param op modify operation to execute + */ + public ModifyOperationWorker(final ModifyOperation op) { + super(op); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/OperationWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/OperationWorker.java new file mode 100644 index 0000000..528a084 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/OperationWorker.java @@ -0,0 +1,35 @@ + +package org.xbib.net.ldap.concurrent; + +import java.util.Collection; +import org.xbib.net.ldap.OperationHandle; +import org.xbib.net.ldap.Request; +import org.xbib.net.ldap.Result; + +/** + * Interface for ldap operation workers. These interface is meant to facilitate executing multiple requests and + * processing multiple responses. + * + * @param type of ldap request + * @param type of ldap response + */ +public interface OperationWorker { + + + /** + * Execute an ldap operation for each request. + * + * @param requests containing the data required by this operation + * @return handle responses for this operation + */ + Collection> send(Q[] requests); + + + /** + * Execute an ldap operation for each request and waits for each operation to complete. + * + * @param requests containing the data required by this operation + * @return responses for this operation + */ + Collection execute(Q[] requests); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/SearchOperationWorker.java b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/SearchOperationWorker.java new file mode 100644 index 0000000..49a8d78 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/concurrent/SearchOperationWorker.java @@ -0,0 +1,97 @@ + +package org.xbib.net.ldap.concurrent; + +import java.util.Collection; +import org.xbib.net.ldap.FilterTemplate; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; + +/** + * Executes multiple ldap search operations asynchronously. + * + */ +public class SearchOperationWorker extends AbstractOperationWorker { + + + /** + * Default constructor. + */ + public SearchOperationWorker() { + super(new SearchOperation()); + } + + + /** + * Creates a new search operation worker. + * + * @param op search operation to execute + */ + public SearchOperationWorker(final SearchOperation op) { + super(op); + } + + + /** + * Performs search operations for the supplied filters. + * + * @param filters to search with + * @return search results + */ + public Collection execute(final String... filters) { + final FilterTemplate[] templates = new FilterTemplate[filters.length]; + for (int i = 0; i < filters.length; i++) { + templates[i] = new FilterTemplate(filters[i]); + } + return execute(templates, (String[]) null); + } + + + /** + * Performs search operations for the supplied filters. + * + * @param templates to search with + * @return search results + */ + public Collection execute(final FilterTemplate... templates) { + return execute(templates, (String[]) null); + } + + + /** + * Performs search operations for the supplied filters with the supplied return attributes + * + * @param filters to search with + * @param attrs attributes to return + * @return search results + */ + public Collection execute(final String[] filters, final String... attrs) { + final FilterTemplate[] templates = new FilterTemplate[filters.length]; + for (int i = 0; i < filters.length; i++) { + templates[i] = new FilterTemplate(filters[i]); + } + return execute(templates, attrs); + } + + + /** + * Performs search operations for the supplied filters with the supplied return attributes + * + * @param templates to search with + * @param attrs attributes to return + * @return search results + */ + public Collection execute(final FilterTemplate[] templates, final String... attrs) { + final SearchRequest[] requests = new SearchRequest[templates.length]; + for (int i = 0; i < templates.length; i++) { + requests[i] = SearchRequest.copy(getOperation().getRequest()); + if (templates[i] != null) { + requests[i].setFilter(templates[i]); + } + if (attrs != null) { + requests[i].setReturnAttributes(attrs); + } + } + return execute(requests); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/AbstractControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/AbstractControl.java new file mode 100644 index 0000000..6a8901c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/AbstractControl.java @@ -0,0 +1,86 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Base class for ldap controls. + * + */ +public abstract class AbstractControl implements Control { + + /** + * control oid. + */ + private final String oid; + + /** + * is control critical. + */ + private final boolean criticality; + + + /** + * Creates a new abstract control. + * + * @param id OID of this control + */ + public AbstractControl(final String id) { + oid = id; + criticality = false; + } + + + /** + * Creates a new abstract control. + * + * @param id OID of this control + * @param b whether this control is critical + */ + public AbstractControl(final String id, final boolean b) { + oid = id; + criticality = b; + } + + + @Override + public String getOID() { + return oid; + } + + + @Override + public boolean getCriticality() { + return criticality; + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "criticality=" + criticality + "]"; + } + + + // CheckStyle:EqualsHashCode OFF + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AbstractControl v) { + return LdapUtils.areEqual(getOID(), v.getOID()) && + LdapUtils.areEqual(getCriticality(), v.getCriticality()); + } + return false; + } + // CheckStyle:EqualsHashCode ON + + + /** + * Returns the hash code for this object. + * + * @return hash code + */ + @Override + public abstract int hashCode(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/AuthorizationIdentityRequestControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/AuthorizationIdentityRequestControl.java new file mode 100644 index 0000000..a644eb8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/AuthorizationIdentityRequestControl.java @@ -0,0 +1,66 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Request control for authorization identity. See RFC 3829. + * + */ +public class AuthorizationIdentityRequestControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.16"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 7013; + + + /** + * Default constructor. + */ + public AuthorizationIdentityRequestControl() { + super(OID); + } + + + /** + * Creates a new ManageDsaIT control. + * + * @param critical whether this control is critical + */ + public AuthorizationIdentityRequestControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof AuthorizationIdentityRequestControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/AuthorizationIdentityResponseControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/AuthorizationIdentityResponseControl.java new file mode 100644 index 0000000..5585e1e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/AuthorizationIdentityResponseControl.java @@ -0,0 +1,122 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.OctetStringType; + +/** + * Response control for authorization identity. See RFC 3829. Control value contains the authorizationId. + * + */ +public class AuthorizationIdentityResponseControl extends AbstractControl implements ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.15"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 7019; + + /** + * Authorization identity. + */ + private String authorizationId; + + + /** + * Default constructor. + */ + public AuthorizationIdentityResponseControl() { + super(OID); + } + + + /** + * Creates a new authorization identity response control. + * + * @param critical whether this control is critical + */ + public AuthorizationIdentityResponseControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new authorization identity response control. + * + * @param id authorization id + */ + public AuthorizationIdentityResponseControl(final String id) { + this(id, false); + } + + + /** + * Creates a new authorization identity response control. + * + * @param id authorization id + * @param critical whether this control is critical + */ + public AuthorizationIdentityResponseControl(final String id, final boolean critical) { + super(OID, critical); + setAuthorizationId(id); + } + + + /** + * Returns the authorization id. + * + * @return authorization id + */ + public String getAuthorizationId() { + return authorizationId; + } + + + /** + * Sets the authorization identity. + * + * @param id authorization id + */ + public void setAuthorizationId(final String id) { + authorizationId = id; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AuthorizationIdentityResponseControl && super.equals(o)) { + final AuthorizationIdentityResponseControl v = (AuthorizationIdentityResponseControl) o; + return LdapUtils.areEqual(authorizationId, v.authorizationId); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), authorizationId); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "authorizationId=" + authorizationId + "]"; + } + + + @Override + public void decode(final DERBuffer encoded) { + setAuthorizationId(OctetStringType.decode(encoded)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/Control.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/Control.java new file mode 100644 index 0000000..ff8ab09 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/Control.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.control; + +/** + * Marker interface for ldap controls. + * + */ +public interface Control { + + + /** + * Returns the OID for this control. + * + * @return oid + */ + String getOID(); + + + /** + * Returns whether the control is critical. + * + * @return whether the control is critical + */ + boolean getCriticality(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/ControlFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/ControlFactory.java new file mode 100644 index 0000000..eebe4ec --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/ControlFactory.java @@ -0,0 +1,105 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.ad.control.DirSyncControl; +import org.xbib.net.ldap.ad.control.GetStatsControl; +import org.xbib.asn1.DERBuffer; + +/** + * Utility class for creating controls. + * + */ +public final class ControlFactory { + + + /** + * Default constructor. + */ + private ControlFactory() { + } + + + /** + * Creates a response control from the supplied control data. + * + * @param oid of the control + * @param critical whether the control is critical + * @param encoded BER encoding of the control + * @return response control + */ + public static ResponseControl createResponseControl(final String oid, final boolean critical, final DERBuffer encoded) { + final ResponseControl ctl; + switch (oid) { + + case SortResponseControl.OID: + ctl = new SortResponseControl(critical); + ctl.decode(encoded); + break; + + case PagedResultsControl.OID: + ctl = new PagedResultsControl(critical); + ctl.decode(encoded); + break; + + case VirtualListViewResponseControl.OID: + ctl = new VirtualListViewResponseControl(critical); + ctl.decode(encoded); + break; + + case PasswordPolicyControl.OID: + ctl = new PasswordPolicyControl(critical); + ctl.decode(encoded); + break; + + case SyncStateControl.OID: + ctl = new SyncStateControl(critical); + ctl.decode(encoded); + break; + + case SyncDoneControl.OID: + ctl = new SyncDoneControl(critical); + ctl.decode(encoded); + break; + + case DirSyncControl.OID: + ctl = new DirSyncControl(critical); + ctl.decode(encoded); + break; + + case EntryChangeNotificationControl.OID: + ctl = new EntryChangeNotificationControl(critical); + ctl.decode(encoded); + break; + + case GetStatsControl.OID: + ctl = new GetStatsControl(critical); + ctl.decode(encoded); + break; + + case PasswordExpiredControl.OID: + ctl = new PasswordExpiredControl(critical); + ctl.decode(encoded); + break; + + case PasswordExpiringControl.OID: + ctl = new PasswordExpiringControl(critical); + ctl.decode(encoded); + break; + + case AuthorizationIdentityResponseControl.OID: + ctl = new AuthorizationIdentityResponseControl(critical); + ctl.decode(encoded); + break; + + case SessionTrackingControl.OID: + ctl = new SessionTrackingControl(critical); + ctl.decode(encoded); + break; + + default: + ctl = new GenericControl(oid, critical, encoded); + break; + } + return ctl; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/EntryChangeNotificationControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/EntryChangeNotificationControl.java new file mode 100644 index 0000000..07b008b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/EntryChangeNotificationControl.java @@ -0,0 +1,318 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * Response control for persistent search. See http://tools.ietf.org/id/draft-ietf-ldapext-psearch-03.txt. Control is + * defined as: + * + *

+ * EntryChangeNotification ::= SEQUENCE {
+ * changeType ENUMERATED {
+ * add             (1),
+ * delete          (2),
+ * modify          (4),
+ * modDN           (8)
+ * },
+ * previousDN   LDAPDN OPTIONAL,     -- modifyDN ops. only
+ * changeNumber INTEGER OPTIONAL     -- if supported
+ * }
+ * 
+ * + */ +public class EntryChangeNotificationControl extends AbstractControl implements ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.7"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 773; + + /** + * change type. + */ + private PersistentSearchChangeType changeType; + + /** + * previous dn. + */ + private String previousDn; + + /** + * change number. + */ + private long changeNumber = -1; + + + /** + * Default constructor. + */ + public EntryChangeNotificationControl() { + super(OID); + } + + + /** + * Creates a new entry change notification control. + * + * @param critical whether this control is critical + */ + public EntryChangeNotificationControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new entry change notification control. + * + * @param type persistent search change type + */ + public EntryChangeNotificationControl(final PersistentSearchChangeType type) { + this(type, false); + } + + + /** + * Creates a new entry change notification control. + * + * @param type persistent search change type + * @param critical whether this control is critical + */ + public EntryChangeNotificationControl(final PersistentSearchChangeType type, final boolean critical) { + super(OID, critical); + setChangeType(type); + } + + + /** + * Creates a new entry change notification control. + * + * @param type persistent search change type + * @param dn previous dn + * @param number change number + */ + public EntryChangeNotificationControl(final PersistentSearchChangeType type, final String dn, final long number) { + this(type, dn, number, false); + } + + + /** + * Creates a new entry change notification control. + * + * @param type persistent search change type + * @param dn previous dn + * @param number change number + * @param critical whether this control is critical + */ + public EntryChangeNotificationControl( + final PersistentSearchChangeType type, + final String dn, + final long number, + final boolean critical) { + super(OID, critical); + setChangeType(type); + setPreviousDn(dn); + setChangeNumber(number); + } + + + /** + * Returns the change type. + * + * @return change type + */ + public PersistentSearchChangeType getChangeType() { + return changeType; + } + + + /** + * Sets the change type. + * + * @param type change type + */ + public void setChangeType(final PersistentSearchChangeType type) { + changeType = type; + } + + + /** + * Returns the previous dn. + * + * @return previous dn + */ + public String getPreviousDn() { + return previousDn; + } + + + /** + * Sets the previous dn. + * + * @param dn previous dn + */ + public void setPreviousDn(final String dn) { + previousDn = dn; + } + + + /** + * Returns the change number. + * + * @return change number + */ + public long getChangeNumber() { + return changeNumber; + } + + + /** + * Sets the change number. + * + * @param number change number + */ + public void setChangeNumber(final long number) { + changeNumber = number; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof EntryChangeNotificationControl v && super.equals(o)) { + return LdapUtils.areEqual(changeType, v.changeType) && + LdapUtils.areEqual(previousDn, v.previousDn) && + LdapUtils.areEqual(changeNumber, v.changeNumber); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), changeType, previousDn, changeNumber); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "changeType=" + changeType + ", " + + "previousDn=" + previousDn + ", " + + "changeNumber=" + changeNumber + "]"; + } + + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(ChangeTypeHandler.PATH, new ChangeTypeHandler(this)); + parser.registerHandler(PreviousDnHandler.PATH, new PreviousDnHandler(this)); + parser.registerHandler(ChangeNumberHandler.PATH, new ChangeNumberHandler(this)); + parser.parse(encoded); + } + + + /** + * Parse handler implementation for the change type. + */ + private static class ChangeTypeHandler extends AbstractParseHandler { + + /** + * DER path to change type. + */ + public static final DERPath PATH = new DERPath("/SEQ/ENUM[0]"); + + + /** + * Creates a new change type handler. + * + * @param control to configure + */ + ChangeTypeHandler(final EntryChangeNotificationControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final int typeValue = IntegerType.decode(encoded).intValue(); + final PersistentSearchChangeType ct = PersistentSearchChangeType.valueOf(typeValue); + if (ct == null) { + throw new IllegalArgumentException("Unknown change type code " + typeValue); + } + getObject().setChangeType(ct); + } + } + + + /** + * Parse handler implementation for the previous dn. + */ + private static class PreviousDnHandler extends AbstractParseHandler { + + /** + * DER path to previous dn. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[1]"); + + + /** + * Creates a new previous dn handler. + * + * @param control to configure + */ + PreviousDnHandler(final EntryChangeNotificationControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setPreviousDn(OctetStringType.decode(encoded)); + } + } + + + /** + * Parse handler implementation for the change number. + */ + private static class ChangeNumberHandler extends AbstractParseHandler { + + /** + * DER path to change number. + */ + public static final DERPath PATH = new DERPath("/SEQ/INT[2]"); + + + /** + * Creates a new change number handler. + * + * @param control to configure + */ + ChangeNumberHandler(final EntryChangeNotificationControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setChangeNumber(IntegerType.decode(encoded).intValue()); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/GenericControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/GenericControl.java new file mode 100644 index 0000000..a7b3fc6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/GenericControl.java @@ -0,0 +1,123 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.DERBuffer; + +/** + * LDAP control defined as: + * + *
+ * Control ::= SEQUENCE {
+ * controlType             LDAPOID,
+ * criticality             BOOLEAN DEFAULT FALSE,
+ * controlValue            OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class GenericControl extends AbstractControl implements RequestControl, ResponseControl { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 7039; + + /** + * control value. + */ + private byte[] value; + + + /** + * Creates a new generic control. + * + * @param oid control OID + * @param encoded control value + */ + public GenericControl(final String oid, final byte[] encoded) { + this(oid, false, encoded); + } + + + /** + * Creates a new generic control. + * + * @param oid control OID + * @param encoded control value + */ + public GenericControl(final String oid, final DERBuffer encoded) { + this(oid, false, encoded); + } + + + /** + * Creates a new generic control. + * + * @param oid control OID + * @param critical whether this control is critical + * @param encoded control value + */ + public GenericControl(final String oid, final boolean critical, final byte[] encoded) { + super(oid, critical); + value = encoded; + } + + + /** + * Creates a new generic control. + * + * @param oid control OID + * @param critical whether this control is critical + * @param encoded control value + */ + public GenericControl(final String oid, final boolean critical, final DERBuffer encoded) { + super(oid, critical); + decode(encoded); + } + + + @Override + public boolean hasValue() { + return value != null; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof GenericControl v && super.equals(o)) { + return LdapUtils.areEqual(value, v.value); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), value); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "oid=" + getOID() + ", " + + "value=" + LdapUtils.base64Encode(value) + "]"; + } + + + @Override + public byte[] encode() { + return value; + } + + + @Override + public void decode(final DERBuffer encoded) { + value = encoded.getRemainingBytes(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/ManageDsaITControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/ManageDsaITControl.java new file mode 100644 index 0000000..070746e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/ManageDsaITControl.java @@ -0,0 +1,66 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Request control for ManageDsaIT. See RFC 3296. + * + */ +public class ManageDsaITControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.2"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 701; + + + /** + * Default constructor. + */ + public ManageDsaITControl() { + super(OID); + } + + + /** + * Creates a new ManageDsaIT control. + * + * @param critical whether this control is critical + */ + public ManageDsaITControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ManageDsaITControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/MatchedValuesRequestControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/MatchedValuesRequestControl.java new file mode 100644 index 0000000..67bfe35 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/MatchedValuesRequestControl.java @@ -0,0 +1,217 @@ + +package org.xbib.net.ldap.control; + +import java.util.Arrays; +import java.util.stream.Stream; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.UniversalDERTag; +import org.xbib.net.ldap.filter.ExtensibleFilter; +import org.xbib.net.ldap.filter.Filter; +import org.xbib.net.ldap.filter.FilterParseException; +import org.xbib.net.ldap.filter.FilterParser; +import org.xbib.net.ldap.filter.FilterSet; + +/** + * Request control for limiting the attribute values returned by a search request. + * See https://tools.ietf.org/html/rfc3876. Control is defined as: + * + *
+ * ValuesReturnFilter ::= SEQUENCE OF SimpleFilterItem
+ *
+ * SimpleFilterItem ::= CHOICE {
+ * equalityMatch   [3] AttributeValueAssertion,
+ * substrings      [4] SubstringFilter,
+ * greaterOrEqual  [5] AttributeValueAssertion,
+ * lessOrEqual     [6] AttributeValueAssertion,
+ * present         [7] AttributeDescription,
+ * approxMatch     [8] AttributeValueAssertion,
+ * extensibleMatch [9] SimpleMatchingAssertion }
+ *
+ * SimpleMatchingAssertion ::= SEQUENCE {
+ * matchingRule    [1] MatchingRuleId OPTIONAL,
+ * type            [2] AttributeDescription OPTIONAL,
+ * --- at least one of the above must be present
+ * matchValue      [3] AssertionValue}
+ * 
+ * + */ +public class MatchedValuesRequestControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.826.0.1.3344810.2.3"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 7057; + + /** + * list of matched values filters. + */ + private Filter[] matchedValuesFilters; + + + /** + * Default constructor. + */ + public MatchedValuesRequestControl() { + super(OID); + } + + + /** + * Creates a new matched values request control. + * + * @param filters to use for value matching + */ + public MatchedValuesRequestControl(final String... filters) { + this(filters, false); + } + + + /** + * Creates a new matched values request control. + * + * @param filters to use for value matching + * @param critical whether this control is critical + */ + public MatchedValuesRequestControl(final String[] filters, final boolean critical) { + super(OID, critical); + setMatchedValuesFilters(filters); + } + + + /** + * Creates a new matched values request control. + * + * @param filters to use for value matching + */ + public MatchedValuesRequestControl(final Filter... filters) { + this(filters, false); + } + + + /** + * Creates a new matched values request control. + * + * @param filters to use for value matching + * @param critical whether this control is critical + */ + public MatchedValuesRequestControl(final Filter[] filters, final boolean critical) { + super(OID, critical); + setMatchedValuesFilters(filters); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the filters to use for matching values. + * + * @return matched values filters + */ + public Filter[] getMatchedValuesFilters() { + return matchedValuesFilters; + } + + + /** + * Sets the filters to use for matching values. + * + * @param filters for matching values + * @throws IllegalArgumentException if the filter cannot be parsed or is not allowed + */ + public void setMatchedValuesFilters(final String... filters) { + final Filter[] parsedFilters = new Filter[filters.length]; + for (int i = 0; i < filters.length; i++) { + try { + parsedFilters[i] = FilterParser.parse(filters[i]); + } catch (FilterParseException e) { + throw new IllegalArgumentException(e); + } + } + setMatchedValuesFilters(parsedFilters); + } + + + /** + * Sets the filters to use for matching values. + * + * @param filters for matching values + * @throws IllegalArgumentException if the filter is not allowed + */ + public void setMatchedValuesFilters(final Filter... filters) { + for (Filter filter : filters) { + validateFilter(filter); + } + matchedValuesFilters = filters; + } + + + /** + * Throws if the supplied filter is not a valid type for the matched values request control. + * + * @param filter to validate + * @throws IllegalArgumentException if the filter is null or not a valid type + */ + private void validateFilter(final Filter filter) { + if (filter == null) { + throw new IllegalArgumentException("Filter cannot be null"); + } else if (filter instanceof FilterSet) { + throw new IllegalArgumentException( + "MatchedValuesRequestControl does not support AND, OR and NOT filter types"); + } else if (filter instanceof ExtensibleFilter && ((ExtensibleFilter) filter).getDnAttributes()) { + throw new IllegalArgumentException( + "MatchedValuesRequestControl does not support an extensible filter with dnAttributes"); + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof MatchedValuesRequestControl v && super.equals(o)) { + return LdapUtils.areEqual(matchedValuesFilters, v.matchedValuesFilters); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getOID(), + getCriticality(), + matchedValuesFilters); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "matchedValuesFilters=" + Arrays.toString(matchedValuesFilters) + "]"; + } + + + @Override + public byte[] encode() { + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + Stream.of(matchedValuesFilters).map(Filter::getEncoder).toArray(DEREncoder[]::new)); + return se.encode(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/PagedResultsControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/PagedResultsControl.java new file mode 100644 index 0000000..6ed1108 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/PagedResultsControl.java @@ -0,0 +1,264 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * Request/response control for PagedResults. See RFC 2696. Control is defined as: + * + *
+ * realSearchControlValue ::= SEQUENCE {
+ * size            INTEGER (0..maxInt),
+ * -- requested page size from client
+ * -- result set size estimate from server
+ * cookie          OCTET STRING
+ * }
+ * 
+ * + */ +public class PagedResultsControl extends AbstractControl implements RequestControl, ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.319"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 709; + + /** + * Empty byte array used for null cookies. + */ + private static final byte[] EMPTY_COOKIE = new byte[0]; + + /** + * paged results size. + */ + private int resultSize; + + /** + * server generated cookie. + */ + private byte[] cookie; + + + /** + * Default constructor. + */ + public PagedResultsControl() { + super(OID); + } + + + /** + * Creates a new paged results control. + * + * @param critical whether this control is critical + */ + public PagedResultsControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new paged results control. + * + * @param size paged results size + */ + public PagedResultsControl(final int size) { + super(OID); + setSize(size); + } + + + /** + * Creates a new paged results control. + * + * @param size paged results size + * @param critical whether this control is critical + */ + public PagedResultsControl(final int size, final boolean critical) { + super(OID, critical); + setSize(size); + } + + + /** + * Creates a new paged results control. + * + * @param size paged results size + * @param value paged results cookie + * @param critical whether this control is critical + */ + public PagedResultsControl(final int size, final byte[] value, final boolean critical) { + super(OID, critical); + setSize(size); + setCookie(value); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the paged results size. For requests this is the requested page size. For responses this is the result size + * estimate from the server. + * + * @return paged results size + */ + public int getSize() { + return resultSize; + } + + + /** + * Sets the paged results size. For requests this is the requested page size. For responses this is the result size + * estimate from the server. + * + * @param size paged results size + */ + public void setSize(final int size) { + resultSize = size; + } + + + /** + * Returns the paged results cookie. + * + * @return paged results cookie + */ + public byte[] getCookie() { + return cookie; + } + + + /** + * Sets the paged results cookie. + * + * @param value paged results cookie + */ + public void setCookie(final byte[] value) { + cookie = value; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof PagedResultsControl && super.equals(o)) { + final PagedResultsControl v = (PagedResultsControl) o; + return LdapUtils.areEqual(resultSize, v.resultSize) && + LdapUtils.areEqual(cookie, v.cookie); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), resultSize, cookie); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "size=" + resultSize + ", " + + "cookie=" + LdapUtils.base64Encode(cookie) + "]"; + } + + + @Override + public byte[] encode() { + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(getSize()), + new OctetStringType(getCookie() != null ? getCookie() : EMPTY_COOKIE)); + return se.encode(); + } + + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(SizeHandler.PATH, new SizeHandler(this)); + parser.registerHandler(CookieHandler.PATH, new CookieHandler(this)); + parser.parse(encoded); + } + + + /** + * Parse handler implementation for the size. + */ + private static class SizeHandler extends AbstractParseHandler { + + /** + * DER path to result size. + */ + public static final DERPath PATH = new DERPath("/SEQ/INT[0]"); + + + /** + * Creates a new size handler. + * + * @param control to configure + */ + SizeHandler(final PagedResultsControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setSize(IntegerType.decode(encoded).intValue()); + } + } + + + /** + * Parse handler implementation for the cookie. + */ + private static class CookieHandler extends AbstractParseHandler { + + /** + * DER path to cookie value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[1]"); + + + /** + * Creates a new cookie handler. + * + * @param control to configure + */ + CookieHandler(final PagedResultsControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setCookie(cookie); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordExpiredControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordExpiredControl.java new file mode 100644 index 0000000..bb1544d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordExpiredControl.java @@ -0,0 +1,70 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.OctetStringType; + +/** + * Response control indicating an expired password. See http://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00. + * Control is defined as: + * + *
+ * controlValue ::= OCTET STRING  -- always "0"
+ * 
+ * + */ +public class PasswordExpiredControl extends AbstractControl implements ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.4"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 787; + + + /** + * Default constructor. + */ + public PasswordExpiredControl() { + super(OID); + } + + + /** + * Creates a new password expired control. + * + * @param critical whether this control is critical + */ + public PasswordExpiredControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof PasswordExpiredControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public void decode(final DERBuffer encoded) { + final String value = OctetStringType.decode(encoded); + if (!"0".equals(value)) { + throw new IllegalArgumentException("Response control value should always be '0'"); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordExpiringControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordExpiringControl.java new file mode 100644 index 0000000..389938c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordExpiringControl.java @@ -0,0 +1,128 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.OctetStringType; + +/** + * Response control indicating a password that will expire. See + * http://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00. Control is defined as: + * + *
+ * controlValue ::= secondsUntilExpiration  OCTET STRING
+ * 
+ * + */ +public class PasswordExpiringControl extends AbstractControl implements ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.5"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 797; + + /** + * time in seconds until expiration. + */ + private int timeBeforeExpiration; + + + /** + * Default constructor. + */ + public PasswordExpiringControl() { + super(OID); + } + + + /** + * Creates a new password expiring control. + * + * @param critical whether this control is critical + */ + public PasswordExpiringControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new password expiring control. + * + * @param time in seconds until expiration + */ + public PasswordExpiringControl(final int time) { + super(OID); + setTimeBeforeExpiration(time); + } + + + /** + * Creates a new password expiring control. + * + * @param time in seconds until expiration + * @param critical whether this control is critical + */ + public PasswordExpiringControl(final int time, final boolean critical) { + super(OID, critical); + setTimeBeforeExpiration(time); + } + + + /** + * Returns the time in seconds until password expiration. + * + * @return time in seconds until expiration + */ + public int getTimeBeforeExpiration() { + return timeBeforeExpiration; + } + + + /** + * Sets the time in seconds until password expiration. + * + * @param time in seconds until expiration + */ + public void setTimeBeforeExpiration(final int time) { + timeBeforeExpiration = time; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof PasswordExpiringControl v && super.equals(o)) { + return LdapUtils.areEqual(timeBeforeExpiration, v.timeBeforeExpiration); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), timeBeforeExpiration); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "timeBeforeExpiration=" + timeBeforeExpiration + "]"; + } + + + @Override + public void decode(final DERBuffer encoded) { + final String time = OctetStringType.decode(encoded); + setTimeBeforeExpiration(Integer.parseInt(time)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordPolicyControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordPolicyControl.java new file mode 100644 index 0000000..a510fbc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/PasswordPolicyControl.java @@ -0,0 +1,407 @@ + +package org.xbib.net.ldap.control; + +import javax.security.auth.login.AccountException; +import javax.security.auth.login.AccountLockedException; +import javax.security.auth.login.CredentialException; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.LoginException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; +import org.xbib.net.ldap.auth.AccountState; + +/** + * Request/response control for password policy. See http://tools.ietf.org/html/draft-behera-ldap-password-policy-11. + * Control is defined as: + * + *
+ * PasswordPolicyResponseValue ::= SEQUENCE {
+ * warning [0] CHOICE {
+ * timeBeforeExpiration [0] INTEGER (0 .. maxInt),
+ * graceAuthNsRemaining [1] INTEGER (0 .. maxInt) } OPTIONAL,
+ * error   [1] ENUMERATED {
+ * passwordExpired             (0),
+ * accountLocked               (1),
+ * changeAfterReset            (2),
+ * passwordModNotAllowed       (3),
+ * mustSupplyOldPassword       (4),
+ * insufficientPasswordQuality (5),
+ * passwordTooShort            (6),
+ * passwordTooYoung            (7),
+ * passwordInHistory           (8),
+ * passwordTooLong             (9) } OPTIONAL }
+ * 
+ * + */ +public class PasswordPolicyControl extends AbstractControl implements RequestControl, ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 719; + /** + * Ppolicy warning. + */ + private int timeBeforeExpiration = -1; + /** + * Ppolicy warning. + */ + private int graceAuthNsRemaining = -1; + /** + * Ppolicy error. + */ + private Error error; + + /** + * Default constructor. + */ + public PasswordPolicyControl() { + super(OID); + } + + + /** + * Creates a new password policy control. + * + * @param critical whether this control is critical + */ + public PasswordPolicyControl(final boolean critical) { + super(OID, critical); + } + + @Override + public boolean hasValue() { + return false; + } + + /** + * Returns the time before expiration in seconds. + * + * @return time before expiration + */ + public int getTimeBeforeExpiration() { + return timeBeforeExpiration; + } + + /** + * Sets the time before expiration in seconds. + * + * @param time before expiration + */ + public void setTimeBeforeExpiration(final int time) { + timeBeforeExpiration = time; + } + + /** + * Returns the number of grace authentications remaining. + * + * @return number of grace authentications remaining + */ + public int getGraceAuthNsRemaining() { + return graceAuthNsRemaining; + } + + /** + * Sets the number of grace authentications remaining. + * + * @param count number of grace authentications remaining + */ + public void setGraceAuthNsRemaining(final int count) { + graceAuthNsRemaining = count; + } + + /** + * Returns the password policy error. + * + * @return password policy error + */ + public Error getError() { + return error; + } + + /** + * Sets the password policy error. + * + * @param e password policy error + */ + public void setError(final Error e) { + error = e; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof PasswordPolicyControl v && super.equals(o)) { + return LdapUtils.areEqual(timeBeforeExpiration, v.timeBeforeExpiration) && + LdapUtils.areEqual(graceAuthNsRemaining, v.graceAuthNsRemaining) && + LdapUtils.areEqual(error, v.error); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getOID(), + getCriticality(), + timeBeforeExpiration, + graceAuthNsRemaining, + error); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "timeBeforeExpiration=" + timeBeforeExpiration + ", " + + "graceAuthNsRemaining=" + graceAuthNsRemaining + ", " + + "error=" + error + "]"; + } + + @Override + public byte[] encode() { + return null; + } + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(TimeBeforeExpirationHandler.PATH, new TimeBeforeExpirationHandler(this)); + parser.registerHandler(GraceAuthnsRemainingHandler.PATH, new GraceAuthnsRemainingHandler(this)); + parser.registerHandler(ErrorHandler.PATH, new ErrorHandler(this)); + parser.parse(encoded); + } + + + /** + * Enum for ppolicy errors. + */ + public enum Error implements AccountState.Error { + + /** + * password expired. + */ + PASSWORD_EXPIRED(0), + + /** + * account locked. + */ + ACCOUNT_LOCKED(1), + + /** + * change after reset. + */ + CHANGE_AFTER_RESET(2), + + /** + * password modification not allowed. + */ + PASSWORD_MOD_NOT_ALLOWED(3), + + /** + * must supply old password. + */ + MUST_SUPPLY_OLD_PASSWORD(4), + + /** + * insufficient password quality. + */ + INSUFFICIENT_PASSWORD_QUALITY(5), + + /** + * password too short. + */ + PASSWORD_TOO_SHORT(6), + + /** + * password too young. + */ + PASSWORD_TOO_YOUNG(7), + + /** + * password in history. + */ + PASSWORD_IN_HISTORY(8), + + /** + * password too long. + */ + PASSWORD_TOO_LONG(9); + + /** + * underlying error code. + */ + private final int code; + + + /** + * Creates a new error. + * + * @param i error code + */ + Error(final int i) { + code = i; + } + + /** + * Returns the error for the supplied integer constant. + * + * @param code to find error for + * @return error + */ + public static Error valueOf(final int code) { + for (Error e : Error.values()) { + if (e.getCode() == code) { + return e; + } + } + return null; + } + + @Override + public int getCode() { + return code; + } + + @Override + public String getMessage() { + return name(); + } + + @Override + public void throwSecurityException() + throws LoginException { + switch (this) { + + case PASSWORD_EXPIRED: + + case CHANGE_AFTER_RESET: + throw new CredentialExpiredException(name()); + + case ACCOUNT_LOCKED: + throw new AccountLockedException(name()); + + case PASSWORD_MOD_NOT_ALLOWED: + + case MUST_SUPPLY_OLD_PASSWORD: + throw new AccountException(name()); + + case INSUFFICIENT_PASSWORD_QUALITY: + + case PASSWORD_TOO_SHORT: + + case PASSWORD_TOO_YOUNG: + + case PASSWORD_IN_HISTORY: + + case PASSWORD_TOO_LONG: + throw new CredentialException(name()); + + default: + throw new IllegalStateException("Unknown password policy error: " + this); + } + } + } + + /** + * Parse handler implementation for the time before expiration. + */ + private static class TimeBeforeExpirationHandler extends AbstractParseHandler { + + /** + * DER path to warning. + */ + public static final DERPath PATH = new DERPath("/SEQ/CTX(0)/CTX(0)"); + + + /** + * Creates a new time before expiration handler. + * + * @param control to configure + */ + TimeBeforeExpirationHandler(final PasswordPolicyControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setTimeBeforeExpiration(IntegerType.decode(encoded).intValue()); + } + } + + + /** + * Parse handler implementation for the grace authns remaining. + */ + private static class GraceAuthnsRemainingHandler extends AbstractParseHandler { + + /** + * DER path to warning. + */ + public static final DERPath PATH = new DERPath("/SEQ/CTX(0)/CTX(1)"); + + + /** + * Creates a new grace authns remaining handler. + * + * @param control to configure + */ + GraceAuthnsRemainingHandler(final PasswordPolicyControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setGraceAuthNsRemaining(IntegerType.decode(encoded).intValue()); + } + } + + + /** + * Parse handler implementation for the error. + */ + private static class ErrorHandler extends AbstractParseHandler { + + /** + * DER path to error. + */ + public static final DERPath PATH = new DERPath("/SEQ/CTX(1)"); + + + /** + * Creates a new error handler. + * + * @param control to configure + */ + ErrorHandler(final PasswordPolicyControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final int errValue = IntegerType.decode(encoded).intValue(); + final PasswordPolicyControl.Error e = PasswordPolicyControl.Error.valueOf(errValue); + if (e == null) { + throw new IllegalArgumentException("Unknown error code " + errValue); + } + getObject().setError(e); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/PersistentSearchChangeType.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/PersistentSearchChangeType.java new file mode 100644 index 0000000..e6f8e42 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/PersistentSearchChangeType.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.control; + +/** + * The set of change types available for use with the {@link PersistentSearchRequestControl} and returned by the {@link + * EntryChangeNotificationControl}. See http://tools.ietf.org/id/draft-ietf-ldapext-psearch-03.txt. + * + */ +public enum PersistentSearchChangeType { + + /** + * add. + */ + ADD(1), + + /** + * delete. + */ + DELETE(2), + + /** + * modify. + */ + MODIFY(4), + + /** + * modify dn. + */ + MODDN(8); + + /** + * underlying value. + */ + private final int value; + + + /** + * Creates a new persistent search change type. + * + * @param i value + */ + PersistentSearchChangeType(final int i) { + value = i; + } + + /** + * Returns the persistent search change type for the supplied integer constant. + * + * @param i to find change type for + * @return persistent search change type + */ + public static PersistentSearchChangeType valueOf(final int i) { + for (PersistentSearchChangeType ct : PersistentSearchChangeType.values()) { + if (ct.value() == i) { + return ct; + } + } + return null; + } + + /** + * Returns the value. + * + * @return enum value + */ + public int value() { + return value; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/PersistentSearchRequestControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/PersistentSearchRequestControl.java new file mode 100644 index 0000000..f9479c3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/PersistentSearchRequestControl.java @@ -0,0 +1,232 @@ + +package org.xbib.net.ldap.control; + +import java.util.EnumSet; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.UniversalDERTag; + +/** + * Request control for persistent search. See http://tools.ietf.org/id/draft-ietf-ldapext-psearch-03.txt. Control is + * defined as: + * + *
+ * PersistentSearch ::= SEQUENCE {
+ * changeTypes INTEGER,
+ * changesOnly BOOLEAN,
+ * returnECs BOOLEAN }
+ * 
+ * + */ +public class PersistentSearchRequestControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.3"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 761; + + /** + * persistent search change types. + */ + private EnumSet changeTypes; + + /** + * whether to return only changed entries. + */ + private boolean changesOnly; + + /** + * whether to return an Entry Change Notification control. + */ + private boolean returnEcs; + + + /** + * Default constructor. + */ + public PersistentSearchRequestControl() { + super(OID); + } + + + /** + * Creates a new persistent search request control. + * + * @param types persistent search change types + */ + public PersistentSearchRequestControl(final EnumSet types) { + super(OID); + setChangeTypes(types); + } + + + /** + * Creates a new persistent search request control. + * + * @param types persistent search change types + * @param critical whether this control is critical + */ + public PersistentSearchRequestControl(final EnumSet types, final boolean critical) { + super(OID, critical); + setChangeTypes(types); + } + + + /** + * Creates a new persistent search request control. + * + * @param types persistent search change types + * @param co whether only changed entries are returned + * @param re return an Entry Change Notification control + */ + public PersistentSearchRequestControl( + final EnumSet types, + final boolean co, + final boolean re) { + super(OID); + setChangeTypes(types); + setChangesOnly(co); + setReturnEcs(re); + } + + + /** + * Creates a new persistent search request control. + * + * @param types persistent search change types + * @param co whether only changed entries are returned + * @param re return an Entry Change Notification control + * @param critical whether this control is critical + */ + public PersistentSearchRequestControl( + final EnumSet types, + final boolean co, + final boolean re, + final boolean critical) { + super(OID, critical); + setChangeTypes(types); + setChangesOnly(co); + setReturnEcs(re); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the persistent search change types. + * + * @return persistent search change types + */ + public EnumSet getChangeTypes() { + return changeTypes; + } + + + /** + * Sets the persistent search change types. + * + * @param types persistent search change types + */ + public void setChangeTypes(final EnumSet types) { + changeTypes = types; + } + + + /** + * Returns whether only changed entries are returned. + * + * @return whether only changed entries are returned + */ + public boolean getChangesOnly() { + return changesOnly; + } + + + /** + * Sets whether only changed entries are returned. + * + * @param b whether only changed entries are returned + */ + public void setChangesOnly(final boolean b) { + changesOnly = b; + } + + + /** + * Returns whether to return an Entry Change Notification control. + * + * @return whether to return an Entry Change Notification control + */ + public boolean getReturnEcs() { + return returnEcs; + } + + + /** + * Sets whether to return an Entry Change Notification control. + * + * @param b return an Entry Change Notification control + */ + public void setReturnEcs(final boolean b) { + returnEcs = b; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof PersistentSearchRequestControl && super.equals(o)) { + final PersistentSearchRequestControl v = (PersistentSearchRequestControl) o; + return LdapUtils.areEqual(changeTypes, v.changeTypes) && + LdapUtils.areEqual(changesOnly, v.changesOnly) && + LdapUtils.areEqual(returnEcs, v.returnEcs); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), changeTypes, changesOnly, returnEcs); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "changeTypes=" + changeTypes + ", " + + "changesOnly=" + changesOnly + ", " + + "returnEcs=" + returnEcs + "]"; + } + + + @Override + public byte[] encode() { + int types = 0; + for (PersistentSearchChangeType type : getChangeTypes()) { + types |= type.value(); + } + + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(types), + new BooleanType(getChangesOnly()), + new BooleanType(getReturnEcs())); + return se.encode(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/ProxyAuthorizationControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/ProxyAuthorizationControl.java new file mode 100644 index 0000000..4fe8d33 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/ProxyAuthorizationControl.java @@ -0,0 +1,115 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.OctetStringType; + +/** + * Request control for proxy authorization. See RFC 4370. Control is defined as: + * + *
+ * controlValue ::= OCTET STRING  -- authorizationId
+ * 
+ * + */ +public class ProxyAuthorizationControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.18"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 7001; + + /** + * empty byte array used for anonymous authz. + */ + private static final byte[] EMPTY_AUTHZ = new byte[0]; + + /** + * authorization identity. + */ + private String authorizationId; + + + /** + * Default constructor. + */ + public ProxyAuthorizationControl() { + super(OID, true); + } + + + /** + * Creates a new proxy authorization control. + * + * @param id authorization identity + */ + public ProxyAuthorizationControl(final String id) { + super(OID, true); + setAuthorizationId(id); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the authorization identity. + * + * @return authorization identity + */ + public String getAuthorizationId() { + return authorizationId; + } + + + /** + * Sets the authorization identity. + * + * @param id authorization identity + */ + public void setAuthorizationId(final String id) { + authorizationId = id; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof ProxyAuthorizationControl && super.equals(o)) { + final ProxyAuthorizationControl v = (ProxyAuthorizationControl) o; + return LdapUtils.areEqual(authorizationId, v.authorizationId); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), authorizationId); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "authorizationId=" + authorizationId + "]"; + } + + + @Override + public byte[] encode() { + return getAuthorizationId() != null ? OctetStringType.toBytes(getAuthorizationId()) : EMPTY_AUTHZ; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/RelaxControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/RelaxControl.java new file mode 100644 index 0000000..42cba15 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/RelaxControl.java @@ -0,0 +1,56 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Relax request control. See https://tools.ietf.org/html/draft-zeilenga-ldap-relax-03. + * + */ +public class RelaxControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.3.6.1.4.1.4203.666.5.12"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10711; + + + /** + * Default constructor. + */ + public RelaxControl() { + super(OID, true); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof RelaxControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/RequestControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/RequestControl.java new file mode 100644 index 0000000..748c68b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/RequestControl.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.control; + +/** + * Marker interface for ldap request controls. + * + */ +public interface RequestControl extends Control { + + + /** + * Provides the BER encoding of this control. + * + * @return BER encoded request control + */ + byte[] encode(); + + + /** + * Returns whether the control has a value associated with it. + * + * @return whether the control has a value + */ + boolean hasValue(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/ResponseControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/ResponseControl.java new file mode 100644 index 0000000..96324d7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/ResponseControl.java @@ -0,0 +1,19 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.asn1.DERBuffer; + +/** + * Marker interface for ldap response controls. + * + */ +public interface ResponseControl extends Control { + + + /** + * Initializes this response control with the supplied BER encoded data. + * + * @param encoded BER encoded response control + */ + void decode(DERBuffer encoded); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/SessionTrackingControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/SessionTrackingControl.java new file mode 100644 index 0000000..90b78d1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/SessionTrackingControl.java @@ -0,0 +1,403 @@ + +package org.xbib.net.ldap.control; + +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * Request/response control for session tracking. See https://tools.ietf.org/html/draft-wahl-ldap-session-03. + * Control is defined as: + * + *
+ * LDAPString ::= OCTET STRING -- UTF-8 encoded
+ * LDAPOID ::= OCTET STRING -- Constrained to numericoid
+ *
+ * SessionIdentifierControlValue ::= SEQUENCE {
+ * sessionSourceIp                 LDAPString,
+ * sessionSourceName               LDAPString,
+ * formatOID                       LDAPOID,
+ * sessionTrackingIdentifier       LDAPString
+ * }
+ * 
+ *

+ * Note that criticality must be either false or absent. + * + */ +public class SessionTrackingControl extends AbstractControl implements RequestControl, ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "1.3.6.1.4.1.21008.108.63.1"; + + /** + * OID for the Acct-Session-Id RADIUS attribute format. + */ + public static final String RADIUS_ACCT_OID = "1.3.6.1.4.1.21008.108.63.1.1"; + + /** + * OID for the Acct-Multi-Session-Id RADIUS attribute format. + */ + public static final String RADIUS_ACCT_MULTI_OID = "1.3.6.1.4.1.21008.108.63.1.2"; + + /** + * OID for the SASL authorization identity string format. + */ + public static final String USERNAME_ACCT_OID = "1.3.6.1.4.1.21008.108.63.1.3"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 7027; + + /** + * Session source ip. + */ + private String sessionSourceIp; + + /** + * Session source name. + */ + private String sessionSourceName; + + /** + * Format OID. + */ + private String formatOID; + + /** + * Session tracking identifier. + */ + private String sessionTrackingIdentifier; + + + /** + * Default constructor. + */ + public SessionTrackingControl() { + super(OID); + } + + + /** + * Creates a new session tracking control. + * + * @param critical whether this control is critical + */ + public SessionTrackingControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new session tracking control. + * + * @param sourceIP session source ip + * @param sourceName session source name + * @param oid format OID + * @param trackingIdentifier session tracking identifier + */ + public SessionTrackingControl( + final String sourceIP, + final String sourceName, + final String oid, + final String trackingIdentifier) { + this(sourceIP, sourceName, oid, trackingIdentifier, false); + } + + + /** + * Creates a new session tracking control. + * + * @param sourceIP session source ip + * @param sourceName session source name + * @param oid format OID + * @param trackingIdentifier session tracking identifier + * @param critical whether this control is critical + */ + public SessionTrackingControl( + final String sourceIP, + final String sourceName, + final String oid, + final String trackingIdentifier, + final boolean critical) { + super(OID, critical); + setSessionSourceIp(sourceIP); + setSessionSourceName(sourceName); + setFormatOID(oid); + setSessionTrackingIdentifier(trackingIdentifier); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the session source ip. + * + * @return session source ip + */ + public String getSessionSourceIp() { + return sessionSourceIp; + } + + + /** + * Sets the session source ip. + * + * @param s session source ip + */ + public void setSessionSourceIp(final String s) { + sessionSourceIp = s; + } + + + /** + * Returns the session source name. + * + * @return session source name + */ + public String getSessionSourceName() { + return sessionSourceName; + } + + + /** + * Sets the session source name. + * + * @param s session source name + */ + public void setSessionSourceName(final String s) { + sessionSourceName = s; + } + + + /** + * Returns the format OID. + * + * @return format OID + */ + public String getFormatOID() { + return formatOID; + } + + + /** + * Sets the format OID. + * + * @param s format OID + */ + public void setFormatOID(final String s) { + formatOID = s; + } + + + /** + * Returns the session tracking identifier. + * + * @return session tracking identifier + */ + public String getSessionTrackingIdentifier() { + return sessionTrackingIdentifier; + } + + + /** + * Sets the session tracking identifier. + * + * @param s session tracking identifier + */ + public void setSessionTrackingIdentifier(final String s) { + sessionTrackingIdentifier = s; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SessionTrackingControl && super.equals(o)) { + final SessionTrackingControl v = (SessionTrackingControl) o; + return LdapUtils.areEqual(sessionSourceIp, v.sessionSourceIp) && + LdapUtils.areEqual(sessionSourceName, v.sessionSourceName) && + LdapUtils.areEqual(formatOID, v.formatOID) && + LdapUtils.areEqual(sessionTrackingIdentifier, v.sessionTrackingIdentifier); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getOID(), + getCriticality(), + sessionSourceIp, + sessionSourceName, + formatOID, + sessionTrackingIdentifier); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "sessionSourceIp=" + sessionSourceIp + ", " + + "sessionSourceName=" + sessionSourceName + ", " + + "formatOID=" + formatOID + ", " + + "sessionTrackingIdentifier=" + sessionTrackingIdentifier + "]"; + } + + + @Override + public byte[] encode() { + final List l = new ArrayList<>(); + l.add(new OctetStringType(getSessionSourceIp())); + l.add(new OctetStringType(getSessionSourceName())); + l.add(new OctetStringType(getFormatOID())); + l.add(new OctetStringType(getSessionTrackingIdentifier())); + + final ConstructedDEREncoder se = new ConstructedDEREncoder(UniversalDERTag.SEQ, l.toArray(new DEREncoder[0])); + return se.encode(); + } + + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(SourceIpHandler.PATH, new SourceIpHandler(this)); + parser.registerHandler(SourceNameHandler.PATH, new SourceNameHandler(this)); + parser.registerHandler(FormatOIDHandler.PATH, new FormatOIDHandler(this)); + parser.registerHandler(TrackingIdentifierHandler.PATH, new TrackingIdentifierHandler(this)); + parser.parse(encoded); + } + + + /** + * Parse handler implementation for the source ip. + */ + private static class SourceIpHandler extends AbstractParseHandler { + + /** + * DER path to source ip value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[0]"); + + + /** + * Creates a new source ip handler. + * + * @param control to configure + */ + SourceIpHandler(final SessionTrackingControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setSessionSourceIp(OctetStringType.decode(encoded)); + } + } + + + /** + * Parse handler implementation for the source name. + */ + private static class SourceNameHandler extends AbstractParseHandler { + + /** + * DER path to source name value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[1]"); + + + /** + * Creates a new source name handler. + * + * @param control to configure + */ + SourceNameHandler(final SessionTrackingControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setSessionSourceName(OctetStringType.decode(encoded)); + } + } + + + /** + * Parse handler implementation for the format oid. + */ + private static class FormatOIDHandler extends AbstractParseHandler { + + /** + * DER path to format oid value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[2]"); + + + /** + * Creates a new format oid handler. + * + * @param control to configure + */ + FormatOIDHandler(final SessionTrackingControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setFormatOID(OctetStringType.decode(encoded)); + } + } + + + /** + * Parse handler implementation for the tracking identifier. + */ + private static class TrackingIdentifierHandler extends AbstractParseHandler { + + /** + * DER path to source name value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[3]"); + + + /** + * Creates a new tracking identifier handler. + * + * @param control to configure + */ + TrackingIdentifierHandler(final SessionTrackingControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setSessionTrackingIdentifier(OctetStringType.decode(encoded)); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/SortKey.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/SortKey.java new file mode 100644 index 0000000..c55ecf4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/SortKey.java @@ -0,0 +1,165 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Used by {@link SortRequestControl} to declare how sorting should occur. See RFC 3698 for the definition of + * matchingRuleId. + * + */ +public class SortKey { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 739; + + /** + * attribute description. + */ + private String attributeDescription; + + /** + * matching rule id. + */ + private String matchingRuleId; + + /** + * reverse order. + */ + private boolean reverseOrder; + + + /** + * Default constructor. + */ + public SortKey() { + } + + + /** + * Creates a new sort key. + * + * @param attrDescription attribute description + */ + public SortKey(final String attrDescription) { + setAttributeDescription(attrDescription); + } + + + /** + * Creates a new sort key. + * + * @param attrDescription attribute description + * @param ruleId matching rule id + */ + public SortKey(final String attrDescription, final String ruleId) { + setAttributeDescription(attrDescription); + setMatchingRuleId(ruleId); + } + + + /** + * Creates a new sort key. + * + * @param attrDescription attribute description + * @param ruleId matching rule id + * @param reverse reverse order + */ + public SortKey(final String attrDescription, final String ruleId, final boolean reverse) { + setAttributeDescription(attrDescription); + setMatchingRuleId(ruleId); + setReverseOrder(reverse); + } + + + /** + * Returns the attribute description. + * + * @return attribute description + */ + public String getAttributeDescription() { + return attributeDescription; + } + + + /** + * Sets the attribute description. + * + * @param s attribute description + */ + public void setAttributeDescription(final String s) { + attributeDescription = s; + } + + + /** + * Returns the matching rule id. + * + * @return matching rule id + */ + public String getMatchingRuleId() { + return matchingRuleId; + } + + + /** + * Sets the matching rule id. + * + * @param s matching rule id + */ + public void setMatchingRuleId(final String s) { + matchingRuleId = s; + } + + + /** + * Returns whether results should be in reverse sorted order. + * + * @return whether results should be in reverse sorted order + */ + public boolean getReverseOrder() { + return reverseOrder; + } + + + /** + * Sets whether results should be in reverse sorted order. + * + * @param b whether results should be in reverse sorted order + */ + public void setReverseOrder(final boolean b) { + reverseOrder = b; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SortKey v) { + return LdapUtils.areEqual(attributeDescription, v.attributeDescription) && + LdapUtils.areEqual(matchingRuleId, v.matchingRuleId) && + LdapUtils.areEqual(reverseOrder, v.reverseOrder); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, attributeDescription, matchingRuleId, reverseOrder); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "attributeDescription=" + attributeDescription + ", " + + "matchingRuleId=" + matchingRuleId + ", " + + "reverseOrder=" + reverseOrder + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/SortRequestControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/SortRequestControl.java new file mode 100644 index 0000000..4e89d5e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/SortRequestControl.java @@ -0,0 +1,145 @@ + +package org.xbib.net.ldap.control; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextType; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * Request control for server side sorting. See RFC 2891. Control is defined as: + * + *

+ * SortKeyList ::= SEQUENCE OF SEQUENCE {
+ * attributeType   AttributeDescription,
+ * orderingRule    [0] MatchingRuleId OPTIONAL,
+ * reverseOrder    [1] BOOLEAN DEFAULT FALSE }
+ * 
+ * + */ +public class SortRequestControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.473"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 727; + + /** + * sort keys. + */ + private SortKey[] sortKeys; + + + /** + * Default constructor. + */ + public SortRequestControl() { + super(OID); + } + + + /** + * Creates a new sort request control. + * + * @param keys sort keys + */ + public SortRequestControl(final SortKey[] keys) { + super(OID); + setSortKeys(keys); + } + + + /** + * Creates a new sort request control. + * + * @param keys sort keys + * @param critical whether this control is critical + */ + public SortRequestControl(final SortKey[] keys, final boolean critical) { + super(OID, critical); + setSortKeys(keys); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the sort keys. + * + * @return sort keys + */ + public SortKey[] getSortKeys() { + return sortKeys; + } + + + /** + * Sets the sort keys. + * + * @param keys sort keys + */ + public void setSortKeys(final SortKey[] keys) { + sortKeys = keys; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SortRequestControl v && super.equals(o)) { + return LdapUtils.areEqual(sortKeys, v.sortKeys); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), sortKeys); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "sortKeys=" + Arrays.toString(sortKeys) + "]"; + } + + + @Override + public byte[] encode() { + final DEREncoder[] keyEncoders = new DEREncoder[sortKeys.length]; + for (int i = 0; i < sortKeys.length; i++) { + final List l = new ArrayList<>(); + l.add(new OctetStringType(sortKeys[i].getAttributeDescription())); + if (sortKeys[i].getMatchingRuleId() != null) { + l.add(new ContextType(0, sortKeys[i].getMatchingRuleId())); + } + if (sortKeys[i].getReverseOrder()) { + l.add(new ContextType(1, sortKeys[i].getReverseOrder())); + } + keyEncoders[i] = new ConstructedDEREncoder(UniversalDERTag.SEQ, l.toArray(new DEREncoder[0])); + } + + final ConstructedDEREncoder se = new ConstructedDEREncoder(UniversalDERTag.SEQ, keyEncoders); + return se.encode(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/SortResponseControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/SortResponseControl.java new file mode 100644 index 0000000..c4aa489 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/SortResponseControl.java @@ -0,0 +1,247 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ResultCode; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * Response control for server side sorting. See RFC 2891. Control is defined as: + * + *
+ * SortResult ::= SEQUENCE {
+ * sortResult  ENUMERATED {
+ * success                   (0), -- results are sorted
+ * operationsError           (1), -- server internal failure
+ * timeLimitExceeded         (3), -- timelimit reached before
+ * -- sorting was completed
+ * strongAuthRequired        (8), -- refused to return sorted
+ * -- results via insecure
+ * -- protocol
+ * adminLimitExceeded       (11), -- too many matching entries
+ * -- for the server to sort
+ * noSuchAttribute          (16), -- unrecognized attribute
+ * -- type in sort key
+ * inappropriateMatching    (18), -- unrecognized or
+ * -- inappropriate matching
+ * -- rule in sort key
+ * insufficientAccessRights (50), -- refused to return sorted
+ * -- results to this client
+ * busy                     (51), -- too busy to process
+ * unwillingToPerform       (53), -- unable to sort
+ * other                    (80)
+ * },
+ * attributeType [0] AttributeDescription OPTIONAL }
+ * 
+ * + */ +public class SortResponseControl extends AbstractControl implements ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.474"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 733; + + /** + * Result of the server side sorting. + */ + private ResultCode sortResult; + + /** + * Failed attribute name. + */ + private String attributeName; + + + /** + * Default constructor. + */ + public SortResponseControl() { + super(OID); + } + + + /** + * Creates a new sort response control. + * + * @param critical whether this control is critical + */ + public SortResponseControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new sort response control. + * + * @param code result of the sort + * @param critical whether this control is critical + */ + public SortResponseControl(final ResultCode code, final boolean critical) { + super(OID, critical); + setSortResult(code); + } + + + /** + * Creates a new sort response control. + * + * @param code result of the sort + * @param attrName name of the failed attribute + * @param critical whether this control is critical + */ + public SortResponseControl(final ResultCode code, final String attrName, final boolean critical) { + super(OID, critical); + setSortResult(code); + setAttributeName(attrName); + } + + + /** + * Returns the result code of the server side sort. + * + * @return result code + */ + public ResultCode getSortResult() { + return sortResult; + } + + + /** + * Sets the result code of the server side sort. + * + * @param code result code + */ + public void setSortResult(final ResultCode code) { + sortResult = code; + } + + + /** + * Returns the attribute name that caused the sort to fail. + * + * @return attribute name + */ + public String getAttributeName() { + return attributeName; + } + + + /** + * Sets the attribute name that caused the sort to fail. + * + * @param name of an attribute + */ + public void setAttributeName(final String name) { + attributeName = name; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SortResponseControl v && super.equals(o)) { + return LdapUtils.areEqual(sortResult, v.sortResult) && + LdapUtils.areEqual(attributeName, v.attributeName); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), sortResult, attributeName); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "sortResult=" + sortResult + ", " + + "attributeName=" + attributeName + "]"; + } + + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(SortResultHandler.PATH, new SortResultHandler(this)); + parser.registerHandler(AttributeTypeHandler.PATH, new AttributeTypeHandler(this)); + parser.parse(encoded); + } + + + /** + * Parse handler implementation for the sort result. + */ + private static class SortResultHandler extends AbstractParseHandler { + + /** + * DER path to result code. + */ + public static final DERPath PATH = new DERPath("/SEQ/ENUM[0]"); + + + /** + * Creates a new sort result handler. + * + * @param control to configure + */ + SortResultHandler(final SortResponseControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final int resultValue = IntegerType.decode(encoded).intValue(); + final ResultCode rc = ResultCode.valueOf(resultValue); + if (rc == null) { + throw new IllegalArgumentException("Unknown result code " + resultValue); + } + getObject().setSortResult(rc); + } + } + + + /** + * Parse handler implementation for the attribute type. + */ + private static class AttributeTypeHandler extends AbstractParseHandler { + + /** + * DER path to attr value. + */ + public static final DERPath PATH = new DERPath("/SEQ/CTX(1)"); + + + /** + * Creates a new attribute type handler. + * + * @param control to configure + */ + AttributeTypeHandler(final SortResponseControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setAttributeName(OctetStringType.decode(encoded)); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncDoneControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncDoneControl.java new file mode 100644 index 0000000..1e98f44 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncDoneControl.java @@ -0,0 +1,235 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; + +/** + * Response control for ldap content synchronization. See RFC 4533. Control is defined as: + * + *
+ * syncDoneValue ::= SEQUENCE {
+ * cookie          syncCookie OPTIONAL,
+ * refreshDeletes  BOOLEAN DEFAULT FALSE
+ * }
+ * 
+ * + */ +public class SyncDoneControl extends AbstractControl implements ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "1.3.6.1.4.1.4203.1.9.1.3"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 757; + + /** + * server generated cookie. + */ + private byte[] cookie; + + /** + * refresh deletes. + */ + private boolean refreshDeletes; + + + /** + * Default constructor. + */ + public SyncDoneControl() { + super(OID); + } + + + /** + * Creates a new sync done control. + * + * @param critical whether this control is critical + */ + public SyncDoneControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new sync done control. + * + * @param value sync done cookie + */ + public SyncDoneControl(final byte[] value) { + super(OID); + setCookie(value); + } + + + /** + * Creates a new sync done control. + * + * @param value sync done cookie + * @param critical whether this control is critical + */ + public SyncDoneControl(final byte[] value, final boolean critical) { + super(OID, critical); + setCookie(value); + } + + + /** + * Creates a new sync done control. + * + * @param value sync done cookie + * @param refresh whether to refresh deletes + * @param critical whether this control is critical + */ + public SyncDoneControl(final byte[] value, final boolean refresh, final boolean critical) { + super(OID, critical); + setCookie(value); + setRefreshDeletes(refresh); + } + + + /** + * Returns the sync done cookie. + * + * @return sync done cookie + */ + public byte[] getCookie() { + return cookie; + } + + + /** + * Sets the sync done cookie. + * + * @param value sync done cookie + */ + public void setCookie(final byte[] value) { + cookie = value; + } + + + /** + * Returns whether to refresh deletes. + * + * @return refresh deletes + */ + public boolean getRefreshDeletes() { + return refreshDeletes; + } + + + /** + * Sets whether to refresh deletes. + * + * @param b refresh deletes + */ + public void setRefreshDeletes(final boolean b) { + refreshDeletes = b; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SyncDoneControl v && super.equals(o)) { + return LdapUtils.areEqual(cookie, v.cookie) && + LdapUtils.areEqual(refreshDeletes, v.refreshDeletes); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), cookie, refreshDeletes); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "cookie=" + LdapUtils.base64Encode(cookie) + ", " + + "refreshDeletes=" + refreshDeletes + "]"; + } + + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(CookieHandler.PATH, new CookieHandler(this)); + parser.registerHandler(RefreshDeletesHandler.PATH, new RefreshDeletesHandler(this)); + parser.parse(encoded); + } + + + /** + * Parse handler implementation for the cookie. + */ + private static class CookieHandler extends AbstractParseHandler { + + /** + * DER path to cookie value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[0]"); + + + /** + * Creates a new cookie handler. + * + * @param control to configure + */ + CookieHandler(final SyncDoneControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setCookie(cookie); + } + } + } + + + /** + * Parse handler implementation for the refresh deletes flag. + */ + private static class RefreshDeletesHandler extends AbstractParseHandler { + + /** + * DER path to the boolean. + */ + public static final DERPath PATH = new DERPath("/SEQ/BOOL[1]"); + + + /** + * Creates a new refresh deletes handler. + * + * @param control to configure + */ + RefreshDeletesHandler(final SyncDoneControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setRefreshDeletes(BooleanType.decode(encoded)); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncRequestControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncRequestControl.java new file mode 100644 index 0000000..0d361d4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncRequestControl.java @@ -0,0 +1,272 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * Request control for ldap content synchronization. See RFC 4533. Control is defined as: + * + *
+ * syncRequestValue ::= SEQUENCE {
+ * mode ENUMERATED {
+ * -- 0 unused
+ * refreshOnly       (1),
+ * -- 2 reserved
+ * refreshAndPersist (3)
+ * },
+ * cookie     syncCookie OPTIONAL,
+ * reloadHint BOOLEAN DEFAULT FALSE
+ * }
+ * 
+ * + */ +public class SyncRequestControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.3.6.1.4.1.4203.1.9.1.1"; + + /** + * hash value seed. + */ + private static final int HASH_CODE_SEED = 743; + /** + * request mode. + */ + private Mode requestMode = Mode.REFRESH_ONLY; + /** + * server generated cookie. + */ + private byte[] cookie; + /** + * reload hint. + */ + private boolean reloadHint; + + /** + * Default constructor. + */ + public SyncRequestControl() { + super(OID); + } + + + /** + * Creates a new sync request control. + * + * @param mode request mode + */ + public SyncRequestControl(final Mode mode) { + super(OID); + setRequestMode(mode); + } + + + /** + * Creates a new sync request control. + * + * @param mode request mode + * @param critical whether this control is critical + */ + public SyncRequestControl(final Mode mode, final boolean critical) { + super(OID, critical); + setRequestMode(mode); + } + + + /** + * Creates a new sync request control. + * + * @param mode request mode + * @param value sync request cookie + * @param critical whether this control is critical + */ + public SyncRequestControl(final Mode mode, final byte[] value, final boolean critical) { + super(OID, critical); + setRequestMode(mode); + setCookie(value); + } + + + /** + * Creates a new sync request control. + * + * @param mode request mode + * @param value sync request cookie + * @param hint reload hint + * @param critical whether this control is critical + */ + public SyncRequestControl(final Mode mode, final byte[] value, final boolean hint, final boolean critical) { + super(OID, critical); + setRequestMode(mode); + setCookie(value); + setReloadHint(hint); + } + + @Override + public boolean hasValue() { + return true; + } + + /** + * Returns the request mode. + * + * @return request mode + */ + public Mode getRequestMode() { + return requestMode; + } + + /** + * Sets the request mode. + * + * @param mode request mode + */ + public void setRequestMode(final Mode mode) { + requestMode = mode; + } + + /** + * Returns the sync request cookie. + * + * @return sync request cookie + */ + public byte[] getCookie() { + return cookie; + } + + /** + * Sets the sync request cookie. + * + * @param value sync request cookie + */ + public void setCookie(final byte[] value) { + cookie = value; + } + + /** + * Returns the reload hint. + * + * @return reload hint + */ + public boolean getReloadHint() { + return reloadHint; + } + + /** + * Sets the reload hint. + * + * @param b reload hint + */ + public void setReloadHint(final boolean b) { + reloadHint = b; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SyncRequestControl v && super.equals(o)) { + return LdapUtils.areEqual(requestMode, v.requestMode) && + LdapUtils.areEqual(cookie, v.cookie) && + LdapUtils.areEqual(reloadHint, v.reloadHint); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), requestMode, cookie, reloadHint); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "requestMode=" + requestMode + ", " + + "cookie=" + LdapUtils.base64Encode(cookie) + ", " + + "reloadHint=" + reloadHint + "]"; + } + + @Override + public byte[] encode() { + final ConstructedDEREncoder se; + if (getCookie() != null) { + se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(getRequestMode().value()), + new OctetStringType(getCookie()), + new BooleanType(getReloadHint())); + } else { + se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + new IntegerType(getRequestMode().value()), + new BooleanType(getReloadHint())); + } + return se.encode(); + } + + + /** + * Types of request modes. + */ + public enum Mode { + + /** + * refresh only. + */ + REFRESH_ONLY(1), + + /** + * refresh and persist. + */ + REFRESH_AND_PERSIST(3); + + /** + * underlying value. + */ + private final int value; + + + /** + * Creates a new mode. + * + * @param i value + */ + Mode(final int i) { + value = i; + } + + /** + * Returns the mode for the supplied integer constant. + * + * @param i to find mode for + * @return mode + */ + public static Mode valueOf(final int i) { + for (Mode m : Mode.values()) { + if (m.value() == i) { + return m; + } + } + return null; + } + + /** + * Returns the value. + * + * @return enum value + */ + public int value() { + return value; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncStateControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncStateControl.java new file mode 100644 index 0000000..56518f0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/SyncStateControl.java @@ -0,0 +1,373 @@ + +package org.xbib.net.ldap.control; + +import java.util.UUID; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.UuidType; + +/** + * Response control for ldap content synchronization. See RFC 4533. Control is defined as: + * + *
+ * syncStateValue ::= SEQUENCE {
+ * state ENUMERATED {
+ * present (0),
+ * add (1),
+ * modify (2),
+ * delete (3)
+ * },
+ * entryUUID syncUUID,
+ * cookie    syncCookie OPTIONAL
+ * }
+ * 
+ * + */ +public class SyncStateControl extends AbstractControl implements ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "1.3.6.1.4.1.4203.1.9.1.2"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 751; + /** + * sync state. + */ + private State syncState; + /** + * sync UUID. + */ + private UUID entryUuid; + /** + * server generated cookie. + */ + private byte[] cookie; + + /** + * Default constructor. + */ + public SyncStateControl() { + super(OID); + } + + + /** + * Creates a new sync state control. + * + * @param critical whether this control is critical + */ + public SyncStateControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new sync state control. + * + * @param state sync state + */ + public SyncStateControl(final State state) { + super(OID); + setSyncState(state); + } + + + /** + * Creates a new sync state control. + * + * @param state sync state + * @param critical whether this control is critical + */ + public SyncStateControl(final State state, final boolean critical) { + super(OID, critical); + setSyncState(state); + } + + + /** + * Creates a new sync state control. + * + * @param state sync state + * @param uuid sync entry uuid + * @param critical whether this control is critical + */ + public SyncStateControl(final State state, final UUID uuid, final boolean critical) { + super(OID, critical); + setSyncState(state); + setEntryUuid(uuid); + } + + + /** + * Creates a new sync state control. + * + * @param state sync state + * @param uuid sync entry uuid + * @param value sync state cookie + * @param critical whether this control is critical + */ + public SyncStateControl(final State state, final UUID uuid, final byte[] value, final boolean critical) { + super(OID, critical); + setSyncState(state); + setEntryUuid(uuid); + setCookie(value); + } + + /** + * Returns the sync state. + * + * @return sync state + */ + public State getSyncState() { + return syncState; + } + + /** + * Sets the sync state. + * + * @param state sync state + */ + public void setSyncState(final State state) { + syncState = state; + } + + /** + * Returns the entry uuid. + * + * @return entry uuid + */ + public UUID getEntryUuid() { + return entryUuid; + } + + /** + * Sets the entry uuid. + * + * @param uuid entry uuid + */ + public void setEntryUuid(final UUID uuid) { + entryUuid = uuid; + } + + /** + * Returns the sync state cookie. + * + * @return sync state cookie + */ + public byte[] getCookie() { + return cookie; + } + + /** + * Sets the sync state cookie. + * + * @param value sync state cookie + */ + public void setCookie(final byte[] value) { + cookie = value; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SyncStateControl v && super.equals(o)) { + return LdapUtils.areEqual(syncState, v.syncState) && + LdapUtils.areEqual(entryUuid, v.entryUuid) && + LdapUtils.areEqual(cookie, v.cookie); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality(), syncState, entryUuid, cookie); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "syncState=" + syncState + ", " + + "entryUuid=" + entryUuid + ", " + + "cookie=" + LdapUtils.base64Encode(cookie) + "]"; + } + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(StateHandler.PATH, new StateHandler(this)); + parser.registerHandler(EntryUuidHandler.PATH, new EntryUuidHandler(this)); + parser.registerHandler(CookieHandler.PATH, new CookieHandler(this)); + parser.parse(encoded); + } + + + /** + * Types of states. + */ + public enum State { + + /** + * present. + */ + PRESENT(0), + + /** + * add. + */ + ADD(1), + + /** + * modify. + */ + MODIFY(2), + + /** + * delete. + */ + DELETE(3); + + /** + * underlying value. + */ + private final int value; + + + /** + * Creates a new mode. + * + * @param i value + */ + State(final int i) { + value = i; + } + + /** + * Returns the state for the supplied integer constant. + * + * @param i to find state for + * @return state + */ + public static State valueOf(final int i) { + for (State s : State.values()) { + if (s.value() == i) { + return s; + } + } + return null; + } + + /** + * Returns the value. + * + * @return enum value + */ + public int value() { + return value; + } + } + + /** + * Parse handler implementation for the sync state. + */ + private static class StateHandler extends AbstractParseHandler { + + /** + * DER path to the state. + */ + public static final DERPath PATH = new DERPath("/SEQ/ENUM[0]"); + + + /** + * Creates a new state handler. + * + * @param control to configure + */ + StateHandler(final SyncStateControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final int stateValue = IntegerType.decode(encoded).intValue(); + final State s = State.valueOf(stateValue); + if (s == null) { + throw new IllegalArgumentException("Unknown state value " + stateValue); + } + getObject().setSyncState(s); + } + } + + + /** + * Parse handler implementation for the entry uuid. + */ + private static class EntryUuidHandler extends AbstractParseHandler { + + /** + * DER path to the uuid. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[1]"); + + + /** + * Creates a new entry uuid handler. + * + * @param control to configure + */ + EntryUuidHandler(final SyncStateControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + if (encoded.hasRemaining()) { + getObject().setEntryUuid(UuidType.decode(encoded)); + } + } + } + + + /** + * Parse handler implementation for the cookie. + */ + private static class CookieHandler extends AbstractParseHandler { + + /** + * DER path to cookie value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[2]"); + + + /** + * Creates a new cookie handler. + * + * @param control to configure + */ + CookieHandler(final SyncStateControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setCookie(cookie); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/TreeDeleteControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/TreeDeleteControl.java new file mode 100644 index 0000000..4c61ca3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/TreeDeleteControl.java @@ -0,0 +1,66 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Request control for TreeDelete. See draft-armijo-ldap-treedelete. + * + */ +public class TreeDeleteControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "1.2.840.113556.1.4.805"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 7043; + + + /** + * Default constructor. + */ + public TreeDeleteControl() { + super(OID); + } + + + /** + * Creates a new tree delete control. + * + * @param critical whether this control is critical + */ + public TreeDeleteControl(final boolean critical) { + super(OID, critical); + } + + + @Override + public boolean hasValue() { + return false; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof TreeDeleteControl && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, getOID(), getCriticality()); + } + + + @Override + public byte[] encode() { + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/VirtualListViewRequestControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/VirtualListViewRequestControl.java new file mode 100644 index 0000000..24f0381 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/VirtualListViewRequestControl.java @@ -0,0 +1,451 @@ + +package org.xbib.net.ldap.control; + +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * Request control for virtual list view. See http://tools.ietf.org/html/draft-ietf-ldapext-ldapv3-vlv-09. Control is + * defined as: + * + *
+ * VirtualListViewRequest ::= SEQUENCE {
+ * beforeCount    INTEGER (0..maxInt),
+ * afterCount     INTEGER (0..maxInt),
+ * target         CHOICE {
+ * byOffset           [0] SEQUENCE {
+ * offset             INTEGER (1 .. maxInt),
+ * contentCount       INTEGER (0 .. maxInt) },
+ * greaterThanOrEqual [1] AssertionValue },
+ * contextID      OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class VirtualListViewRequestControl extends AbstractControl implements RequestControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.9"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 769; + + /** + * number of entries before the target entry the server should send. + */ + private int beforeCount; + + /** + * number of entries after the target entry the server should send. + */ + private int afterCount; + + /** + * target entry's offset within the ordered search result set. + */ + private int targetOffset; + + /** + * server's estimate of the current number of entries in the ordered search result set. + */ + private int contentCount; + + /** + * value to match against the ordering matching rule for the attributeDescription in the sort control. + */ + private String assertionValue; + + /** + * value that clients should send back to the server to indicate that the server is willing to return contiguous data + * from a subsequent search request which uses the same search criteria. + */ + private byte[] contextID; + + + /** + * Default constructor. + */ + public VirtualListViewRequestControl() { + super(OID); + } + + + /** + * Creates a new virtual list view request control. + * + * @param offset target entry offset + * @param before number of entries before the target + * @param after number of entries after the target + */ + public VirtualListViewRequestControl(final int offset, final int before, final int after) { + this(offset, before, after, 0, null, false); + } + + + /** + * Creates a new virtual list view request control. + * + * @param offset target entry offset + * @param before number of entries before the target + * @param after number of entries after the target + * @param critical whether this control is critical + */ + public VirtualListViewRequestControl(final int offset, final int before, final int after, final boolean critical) { + this(offset, before, after, 0, null, critical); + } + + + /** + * Creates a new virtual list view request control. + * + * @param offset target entry offset + * @param before number of entries before the target + * @param after number of entries after the target + * @param context server context id + * @param critical whether this control is critical + */ + public VirtualListViewRequestControl( + final int offset, + final int before, + final int after, + final byte[] context, + final boolean critical) { + this(offset, before, after, 0, context, critical); + } + + + /** + * Creates a new virtual list view request control. + * + * @param offset target entry offset + * @param before number of entries before the target + * @param after number of entries after the target + * @param count server estimate of the number of entries + * @param context server context id + */ + public VirtualListViewRequestControl( + final int offset, + final int before, + final int after, + final int count, + final byte[] context) { + this(offset, before, after, count, context, false); + } + + + /** + * Creates a new virtual list view request control. + * + * @param offset target entry offset + * @param before number of entries before the target + * @param after number of entries after the target + * @param count server estimate of the number of entries + * @param context server context id + * @param critical whether this control is critical + */ + public VirtualListViewRequestControl( + final int offset, + final int before, + final int after, + final int count, + final byte[] context, + final boolean critical) { + super(OID, critical); + setTargetOffset(offset); + setBeforeCount(before); + setAfterCount(after); + setContentCount(count); + setContextID(context); + } + + + /** + * Creates a new virtual list view request control. + * + * @param assertion value to match in the sort control + * @param before number of entries before the target + * @param after number of entries after the target + */ + public VirtualListViewRequestControl(final String assertion, final int before, final int after) { + this(assertion, before, after, null, false); + } + + + /** + * Creates a new virtual list view request control. + * + * @param assertion value to match in the sort control + * @param before number of entries before the target + * @param after number of entries after the target + * @param critical whether this control is critical + */ + public VirtualListViewRequestControl( + final String assertion, + final int before, + final int after, + final boolean critical) { + this(assertion, before, after, null, critical); + } + + + /** + * Creates a new virtual list view request control. + * + * @param assertion value to match in the sort control + * @param before number of entries before the target + * @param after number of entries after the target + * @param context server context id + */ + public VirtualListViewRequestControl(final String assertion, final int before, final int after, final byte[] context) { + this(assertion, before, after, context, false); + } + + + /** + * Creates a new virtual list view request control. + * + * @param assertion value to match in the sort control + * @param before number of entries before the target + * @param after number of entries after the target + * @param context server context id + * @param critical whether this control is critical + */ + public VirtualListViewRequestControl( + final String assertion, + final int before, + final int after, + final byte[] context, + final boolean critical) { + super(OID, critical); + setAssertionValue(assertion); + setBeforeCount(before); + setAfterCount(after); + setContextID(context); + } + + + @Override + public boolean hasValue() { + return true; + } + + + /** + * Returns the before count. This indicates how many entries before the target entry the client wants the server to + * send. + * + * @return before count + */ + public int getBeforeCount() { + return beforeCount; + } + + + /** + * Sets the before count. + * + * @param count before count + */ + public void setBeforeCount(final int count) { + beforeCount = count; + } + + + /** + * Returns the after count. This indicates how many entries after the target entry the client wants the server to + * send. + * + * @return after count + */ + public int getAfterCount() { + return afterCount; + } + + + /** + * Sets the after count. + * + * @param count after count + */ + public void setAfterCount(final int count) { + afterCount = count; + } + + + /** + * Returns the target offset. This indicates the return entry's offset within the ordered search result set. + * + * @return target offset + */ + public int getTargetOffset() { + return targetOffset; + } + + + /** + * Sets the target offset. + * + * @param offset target offset + */ + public void setTargetOffset(final int offset) { + targetOffset = offset; + } + + + /** + * Returns the content count. From the RFC: + * + *

contentCount gives the server's estimate of the current number of entries in the list. Together these give + * sufficient information for the client to update a list box slider position to match the newly retrieved entries and + * identify the target entry. The contentCount value returned SHOULD be used in a subsequent VirtualListViewRequest + * control.

+ * + * @return content count + */ + public int getContentCount() { + return contentCount; + } + + + /** + * Sets the content count. + * + * @param count content count + */ + public void setContentCount(final int count) { + contentCount = count; + } + + + /** + * Returns the assertion value. From the RFC: + * + *

The assertion value is encoded according to the ORDERING matching rule for the attributeDescription in the sort + * control [SSS]. If present, the value supplied in greaterThanOrEqual is used to determine the target entry by + * comparison with the values of the attribute specified as the primary sort key. The first list entry who's value is + * no less than (less than or equal to when the sort order is reversed) the supplied value is the target entry.

+ * + * @return assertion value + */ + public String getAssertionValue() { + return assertionValue; + } + + + /** + * Sets the assertion value. + * + * @param value assertion value + */ + public void setAssertionValue(final String value) { + assertionValue = value; + } + + + /** + * Returns the context id. From the RFC: + * + *

The contextID is a server-defined octet string. If present, the contents of the contextID field SHOULD be + * returned to the server by a client in a subsequent virtual list request. The presence of a contextID here indicates + * that the server is willing to return contiguous data from a subsequent search request which uses the same search + * criteria, accompanied by a VirtualListViewRequest which indicates that the client wishes to receive an adjoining + * page of data.

+ * + * @return context id + */ + public byte[] getContextID() { + return contextID; + } + + + /** + * Sets the context id. + * + * @param id context id + */ + public void setContextID(final byte[] id) { + contextID = id; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof VirtualListViewRequestControl v && super.equals(o)) { + return LdapUtils.areEqual(beforeCount, v.beforeCount) && + LdapUtils.areEqual(afterCount, v.afterCount) && + LdapUtils.areEqual(targetOffset, v.targetOffset) && + LdapUtils.areEqual(contentCount, v.contentCount) && + LdapUtils.areEqual(assertionValue, v.assertionValue) && + LdapUtils.areEqual(contextID, v.contextID); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getOID(), + getCriticality(), + beforeCount, + afterCount, + targetOffset, + contentCount, + assertionValue, + contextID); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "beforeCount=" + beforeCount + ", " + + "afterCount=" + afterCount + ", " + + "targetOffset=" + targetOffset + ", " + + "contentCount=" + contentCount + ", " + + "assertionValue=" + assertionValue + ", " + + "contextID=" + LdapUtils.base64Encode(contextID) + "]"; + } + + + @Override + public byte[] encode() { + final List l = new ArrayList<>(); + l.add(new IntegerType(getBeforeCount())); + l.add(new IntegerType(getAfterCount())); + if (getAssertionValue() != null) { + l.add(new OctetStringType(new ContextDERTag(1, false), getAssertionValue())); + } else { + l.add( + new ConstructedDEREncoder( + new ContextDERTag(0, true), + new IntegerType(getTargetOffset()), + new IntegerType(getContentCount()))); + } + if (getContextID() != null) { + l.add(new OctetStringType(getContextID())); + } + + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + l.toArray(new DEREncoder[0])); + return se.encode(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/VirtualListViewResponseControl.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/VirtualListViewResponseControl.java new file mode 100644 index 0000000..4158bb8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/VirtualListViewResponseControl.java @@ -0,0 +1,390 @@ + +package org.xbib.net.ldap.control; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ResultCode; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.IntegerType; + +/** + * Response control for virtual list view. See http://tools.ietf.org/html/draft-ietf-ldapext-ldapv3-vlv-09. Control is + * defined as: + * + *
+ * VirtualListViewResponse ::= SEQUENCE {
+ * targetPosition    INTEGER (0 .. maxInt),
+ * contentCount      INTEGER (0 .. maxInt),
+ * virtualListViewResult ENUMERATED {
+ * success (0),
+ * operationsError (1),
+ * protocolError (2),
+ * unwillingToPerform (53),
+ * insufficientAccessRights (50),
+ * timeLimitExceeded (3),
+ * adminLimitExceeded (11),
+ * innapropriateMatching (18),
+ * sortControlMissing (60),
+ * offsetRangeError (61),
+ * other(80),
+ * ... },
+ * contextID         OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class VirtualListViewResponseControl extends AbstractControl implements ResponseControl { + + /** + * OID of this control. + */ + public static final String OID = "2.16.840.1.113730.3.4.10"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10709; + + /** + * list offset for the target entry. + */ + private int targetPosition; + + /** + * server's estimate of the current number of entries in the ordered search result set. + */ + private int contentCount; + + /** + * Result of the vlv operation. + */ + private ResultCode viewResult; + + /** + * value that clients should send back to the server to indicate that the server is willing to return contiguous data + * from a subsequent search request which uses the same search criteria. + */ + private byte[] contextID; + + + /** + * Default constructor. + */ + public VirtualListViewResponseControl() { + super(OID); + } + + + /** + * Creates a new virtual list view response control. + * + * @param critical whether this control is critical + */ + public VirtualListViewResponseControl(final boolean critical) { + super(OID, critical); + } + + + /** + * Creates a new virtual list view response control. + * + * @param position offset for the target entry + * @param count server estimate of the number of entries + * @param code operation result code + * @param context server context id + */ + public VirtualListViewResponseControl( + final int position, + final int count, + final ResultCode code, + final byte[] context) { + this(position, count, code, context, false); + } + + + /** + * Creates a new virtual list view response control. + * + * @param position offset for the target entry + * @param count server estimate of the number of entries + * @param code operation result code + * @param context server context id + * @param critical whether this control is critical + */ + public VirtualListViewResponseControl( + final int position, + final int count, + final ResultCode code, + final byte[] context, + final boolean critical) { + super(OID, critical); + setTargetPosition(position); + setContentCount(count); + setViewResult(code); + setContextID(context); + } + + + /** + * Returns the target position. This indicates the list offset for the target entry. + * + * @return target position + */ + public int getTargetPosition() { + return targetPosition; + } + + + /** + * Sets the target position. + * + * @param position target position + */ + public void setTargetPosition(final int position) { + targetPosition = position; + } + + + /** + * Returns the content count. From the RFC: + * + *

contentCount gives the server's estimate of the current number of entries in the list. Together these give + * sufficient information for the client to update a list box slider position to match the newly retrieved entries and + * identify the target entry. The contentCount value returned SHOULD be used in a subsequent VirtualListViewRequest + * control.

+ * + * @return content count + */ + public int getContentCount() { + return contentCount; + } + + + /** + * Sets the content count. + * + * @param count content count + */ + public void setContentCount(final int count) { + contentCount = count; + } + + + /** + * Returns the result code of the virtual list view. + * + * @return result code + */ + public ResultCode getViewResult() { + return viewResult; + } + + + /** + * Sets the result code of the virtual list view. + * + * @param code result code + */ + public void setViewResult(final ResultCode code) { + viewResult = code; + } + + + /** + * Returns the context id. From the RFC: + * + *

The contextID is a server-defined octet string. If present, the contents of the contextID field SHOULD be + * returned to the server by a client in a subsequent virtual list request. The presence of a contextID here indicates + * that the server is willing to return contiguous data from a subsequent search request which uses the same search + * criteria, accompanied by a VirtualListViewRequest which indicates that the client wishes to receive an adjoining + * page of data.

+ * + * @return context id + */ + public byte[] getContextID() { + return contextID; + } + + + /** + * Sets the context id. + * + * @param id context id + */ + public void setContextID(final byte[] id) { + contextID = id; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof VirtualListViewResponseControl v && super.equals(o)) { + return LdapUtils.areEqual(targetPosition, v.targetPosition) && + LdapUtils.areEqual(contentCount, v.contentCount) && + LdapUtils.areEqual(viewResult, v.viewResult) && + LdapUtils.areEqual(contextID, v.contextID); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getOID(), + getCriticality(), + targetPosition, + contentCount, + viewResult, + contextID); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "criticality=" + getCriticality() + ", " + + "targetPosition=" + targetPosition + ", " + + "contentCount=" + contentCount + ", " + + "viewResult=" + viewResult + ", " + + "contextID=" + LdapUtils.base64Encode(contextID) + "]"; + } + + + @Override + public void decode(final DERBuffer encoded) { + final DERParser parser = new DERParser(); + parser.registerHandler(TargetPositionHandler.PATH, new TargetPositionHandler(this)); + parser.registerHandler(ContentCountHandler.PATH, new ContentCountHandler(this)); + parser.registerHandler(ViewResultHandler.PATH, new ViewResultHandler(this)); + parser.registerHandler(ContextIDHandler.PATH, new ContextIDHandler(this)); + parser.parse(encoded); + } + + + /** + * Parse handler implementation for the target position. + */ + private static class TargetPositionHandler extends AbstractParseHandler { + + /** + * DER path to target position. + */ + public static final DERPath PATH = new DERPath("/SEQ/INT[0]"); + + + /** + * Creates a new target position handler. + * + * @param control to configure + */ + TargetPositionHandler(final VirtualListViewResponseControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setTargetPosition(IntegerType.decode(encoded).intValue()); + } + } + + + /** + * Parse handler implementation for the content count. + */ + private static class ContentCountHandler extends AbstractParseHandler { + + /** + * DER path to content count. + */ + public static final DERPath PATH = new DERPath("/SEQ/INT[1]"); + + + /** + * Creates a new content count handler. + * + * @param control to configure + */ + ContentCountHandler(final VirtualListViewResponseControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setContentCount(IntegerType.decode(encoded).intValue()); + } + } + + + /** + * Parse handler implementation for the view result. + */ + private static class ViewResultHandler extends AbstractParseHandler { + + /** + * DER path to result code. + */ + public static final DERPath PATH = new DERPath("/SEQ/ENUM[2]"); + + + /** + * Creates a new view result handler. + * + * @param control to configure + */ + ViewResultHandler(final VirtualListViewResponseControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final int resultValue = IntegerType.decode(encoded).intValue(); + final ResultCode rc = ResultCode.valueOf(resultValue); + if (rc == null) { + throw new IllegalArgumentException("Unknown result code " + resultValue); + } + getObject().setViewResult(rc); + } + } + + + /** + * Parse handler implementation for the context ID. + */ + private static class ContextIDHandler extends AbstractParseHandler { + + /** + * DER path to context value. + */ + public static final DERPath PATH = new DERPath("/SEQ/OCTSTR[3]"); + + + /** + * Creates a new context ID handler. + * + * @param control to configure + */ + ContextIDHandler(final VirtualListViewResponseControl control) { + super(control); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setContextID(cookie); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/CookieManager.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/CookieManager.java new file mode 100644 index 0000000..70515a2 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/CookieManager.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.control.util; + +/** + * Interface for the reading and writing of control related cookies. + * + */ +public interface CookieManager { + + + /** + * Read and return a cookie from storage. + * + * @return cookie read from storage + */ + byte[] readCookie(); + + + /** + * Writes a cookie to storage. + * + * @param cookie to write + */ + void writeCookie(byte[] cookie); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/DefaultCookieManager.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/DefaultCookieManager.java new file mode 100644 index 0000000..ed1661b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/DefaultCookieManager.java @@ -0,0 +1,43 @@ + +package org.xbib.net.ldap.control.util; + +/** + * Cookie manager that stores a cookie in memory. + * + */ +public class DefaultCookieManager implements CookieManager { + + /** + * Control cookie. + */ + private byte[] cookie; + + + /** + * Creates a new default cookie manager. + */ + public DefaultCookieManager() { + } + + + /** + * Creates a new default cookie manager. + * + * @param b control cookie + */ + public DefaultCookieManager(final byte[] b) { + cookie = b; + } + + + @Override + public byte[] readCookie() { + return cookie; + } + + + @Override + public void writeCookie(final byte[] b) { + cookie = b; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/PagedResultsClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/PagedResultsClient.java new file mode 100644 index 0000000..06d7d4d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/PagedResultsClient.java @@ -0,0 +1,238 @@ + +package org.xbib.net.ldap.control.util; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.xbib.net.ldap.AbstractSearchOperationFactory; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.control.PagedResultsControl; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Client that simplifies using the paged results control. + * + */ +public class PagedResultsClient extends AbstractSearchOperationFactory { + /** + * Results page size. + */ + private final int resultSize; + + + /** + * Creates a new paged results client. + * + * @param cf to get a connection from + * @param size the results page size to request + */ + public PagedResultsClient(final ConnectionFactory cf, final int size) { + setConnectionFactory(cf); + resultSize = size; + } + + + /** + * Performs a search operation with the {@link PagedResultsControl}. The supplied request is modified in the following + * way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * PagedResultsControl}
  • + *
+ * + * @param request search request to execute + * @return search operation response + * @throws LdapException if the search fails + */ + public SearchResponse execute(final SearchRequest request) + throws LdapException { + return execute(request, new DefaultCookieManager()); + } + + + /** + * Performs a search operation with the {@link PagedResultsControl}. The supplied request is modified in the following + * way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * PagedResultsControl}
  • + *
+ * + *

The cookie is extracted from the supplied response and replayed in the request.

+ * + * @param request search request to execute + * @param result of a previous paged results operation + * @return search operation response + * @throws LdapException if the search fails + */ + public SearchResponse execute(final SearchRequest request, final SearchResponse result) + throws LdapException { + final byte[] cookie = getPagedResultsCookie(result); + if (cookie == null) { + throw new IllegalArgumentException("Response does not contain a paged results cookie"); + } + + return execute(request, new DefaultCookieManager(cookie)); + } + + + /** + * Performs a search operation with the {@link PagedResultsControl}. The supplied request is modified in the following + * way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * PagedResultsControl}
  • + *
+ * + *

The cookie used in the request is read from the cookie manager and written to the cookie manager after a + * successful search, if the response contains a cookie.

+ * + * @param request search request to execute + * @param manager for reading and writing cookies + * @return search operation response + * @throws LdapException if the search fails + */ + public SearchResponse execute(final SearchRequest request, final CookieManager manager) + throws LdapException { + final SearchOperation search = createSearchOperation(); + if (request.getControls() != null && request.getControls().length > 0) { + final List requestControls = Arrays.stream( + request.getControls()).filter(c -> !(c instanceof PagedResultsControl)).collect(Collectors.toList()); + requestControls.add(new PagedResultsControl(resultSize, manager.readCookie(), true)); + request.setControls(requestControls.toArray(RequestControl[]::new)); + } else { + request.setControls(new PagedResultsControl(resultSize, manager.readCookie(), true)); + } + final SearchResponse result = search.execute(request); + final byte[] cookie = getPagedResultsCookie(result); + if (cookie != null) { + manager.writeCookie(cookie); + } + return result; + } + + + /** + * Returns whether {@link #execute(SearchRequest, SearchResponse)} can be invoked again. + * + * @param result of a previous paged results operation + * @return whether more paged search results can be retrieved from the server + */ + public boolean hasMore(final SearchResponse result) { + return getPagedResultsCookie(result) != null; + } + + + /** + * Performs a search operation with the {@link PagedResultsControl}. The supplied request is modified in the following + * way: + * + *
    + *
  • {@link SearchRequest#setControls(RequestControl...)} is invoked with {@link PagedResultsControl} and any + * other controls previously set on the search request.
  • + *
+ * + *

This method will continue to execute search operations until all paged search results have been retrieved from + * the server. The returned response contains the response data of the last paged result operation plus the entries + * and references returned by all previous search operations.

+ * + * @param request search request to execute + * @return search operation response of the last paged result operation + * @throws LdapException if the search fails + */ + public SearchResponse executeToCompletion(final SearchRequest request) + throws LdapException { + return executeToCompletion(request, new DefaultCookieManager()); + } + + + /** + * Performs a search operation with the {@link PagedResultsControl}. The supplied request is modified in the following + * way: + * + *
    + *
  • {@link SearchRequest#setControls(RequestControl...)} is invoked with {@link PagedResultsControl} and any + * other controls previously set on the search request.
  • + *
+ * + *

This method will continue to execute search operations until all paged search results have been retrieved from + * the server. The returned response contains the response data of the last paged result operation plus the entries + * and references returned by all previous search operations.

+ * + *

The cookie used for each request is read from the cookie manager and written to the cookie manager after a + * successful search, if the response contains a cookie.

+ * + *

This method builds a synthetic response which contains the results of all search operations. Any ordering + * imposed by result handlers may be lost by this process.

+ * + * @param request search request to execute + * @param manager for reading and writing cookies + * @return search operation response of the last paged result operation + * @throws LdapException if the search fails + */ + public SearchResponse executeToCompletion(final SearchRequest request, final CookieManager manager) + throws LdapException { + SearchResponse result = null; + final SearchResponse combinedResults = new SearchResponse(); + final SearchOperation search = createSearchOperation(); + byte[] cookie = manager.readCookie(); + do { + if (result != null) { + combinedResults.addEntries(result.getEntries()); + combinedResults.addReferences(result.getReferences()); + } + if (request.getControls() != null && request.getControls().length > 0) { + final List requestControls = Arrays.stream( + request.getControls()).filter(c -> !(c instanceof PagedResultsControl)).collect(Collectors.toList()); + requestControls.add(new PagedResultsControl(resultSize, cookie, true)); + request.setControls(requestControls.toArray(RequestControl[]::new)); + } else { + request.setControls(new PagedResultsControl(resultSize, cookie, true)); + } + result = search.execute(request); + cookie = getPagedResultsCookie(result); + if (cookie != null) { + manager.writeCookie(cookie); + } + } while (cookie != null); + result.addEntries(combinedResults.getEntries()); + result.addReferences(combinedResults.getReferences()); + return result; + } + + + /** + * Returns the {@link PagedResultsControl} in the supplied response. + * + * @param result to inspect for a response control + * @return paged results response control or null if it does not exist + */ + public PagedResultsControl getResponseControl(final SearchResponse result) { + return (PagedResultsControl) result.getControl(PagedResultsControl.OID); + } + + + /** + * Returns the paged results cookie in the supplied response or null if no cookie exists. + * + * @param result of a previous paged results operation + * @return paged results cookie or null + */ + protected byte[] getPagedResultsCookie(final SearchResponse result) { + byte[] cookie = null; + final PagedResultsControl ctl = (PagedResultsControl) result.getControl(PagedResultsControl.OID); + if (ctl != null) { + if (ctl.getCookie() != null && ctl.getCookie().length > 0) { + cookie = ctl.getCookie(); + } + } + return cookie; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/PersistentSearchClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/PersistentSearchClient.java new file mode 100644 index 0000000..076d43f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/PersistentSearchClient.java @@ -0,0 +1,174 @@ + +package org.xbib.net.ldap.control.util; + +import java.util.EnumSet; +import java.util.function.Consumer; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchOperationHandle; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.control.PersistentSearchChangeType; +import org.xbib.net.ldap.control.PersistentSearchRequestControl; + +/** + * Client that simplifies using the persistent search control. + * + */ +public class PersistentSearchClient { + + /** + * Connection factory to get a connection from. + */ + private final ConnectionFactory factory; + + /** + * Change types. + */ + private final EnumSet changeTypes; + + /** + * Whether to return only changed entries. + */ + private final boolean changesOnly; + + /** + * Whether to return an Entry Change Notification control. + */ + private final boolean returnEcs; + + /** + * Search operation handle. + */ + private SearchOperationHandle handle; + + /** + * Invoked when an entry is received. + */ + private Consumer onEntry; + + /** + * Invoked when a result is received. + */ + private Consumer onResult; + + /** + * Invoked when an exception is received. + */ + private Consumer onException; + + + /** + * Creates a new persistent search client. + * + * @param cf to get a connection from + * @param types persistent search change types + * @param co whether only changed entries are returned + * @param re return an Entry Change Notification control + */ + public PersistentSearchClient( + final ConnectionFactory cf, + final EnumSet types, + final boolean co, + final boolean re) { + factory = cf; + changeTypes = types; + changesOnly = co; + returnEcs = re; + } + + + /** + * Sets the onEntry consumer. + * + * @param consumer to invoke when an entry is received + */ + public void setOnEntry(final Consumer consumer) { + onEntry = consumer; + } + + + /** + * Sets the onResult consumer. + * + * @param consumer to invoke when a result is received + */ + public void setOnResult(final Consumer consumer) { + onResult = consumer; + } + + + /** + * Sets the onException consumer. + * + * @param consumer to invoke when a sync info message is received + */ + public void setOnException(final Consumer consumer) { + onException = consumer; + } + + + /** + * Performs an async search operation with the {@link PersistentSearchRequestControl}. The supplied request is + * modified in the following way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * PersistentSearchRequestControl}
  • + *
+ * + *

The search request object should not be reused for any other search operations.

+ * + * @param request search request to execute + * @return search operation handle + * @throws LdapException if the search fails + */ + public SearchOperationHandle send(final SearchRequest request) + throws LdapException { + request.setControls(new PersistentSearchRequestControl(changeTypes, changesOnly, returnEcs, true)); + final SearchOperation search = new SearchOperation(factory, request); + search.setResultHandlers(result -> { + try { + onResult.accept(result); + } catch (Exception e) { + try { + onException.accept(e); + } catch (Exception ex) { + // + } + } + }); + search.setExceptionHandler(e -> { + try { + onException.accept(e); + } catch (Exception ex) { + // + } + }); + search.setEntryHandlers(entry -> { + try { + onEntry.accept(entry); + } catch (Exception e) { + // + try { + onException.accept(e); + } catch (Exception ex) { + // + } + } + return null; + }); + handle = search.send(); + return handle; + } + + + /** + * Invokes an abandon operation on the search handle. + */ + public void abandon() { + handle.abandon(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplClient.java new file mode 100644 index 0000000..175f80b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplClient.java @@ -0,0 +1,361 @@ + +package org.xbib.net.ldap.control.util; + +import java.util.function.Consumer; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchOperationHandle; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResultReference; +import org.xbib.net.ldap.control.SyncDoneControl; +import org.xbib.net.ldap.control.SyncRequestControl; +import org.xbib.net.ldap.control.SyncStateControl; +import org.xbib.net.ldap.extended.ExtendedOperationHandle; +import org.xbib.net.ldap.extended.SyncInfoMessage; + +/** + * Client that simplifies using the sync repl control. + * + */ +public class SyncReplClient { + + /** + * Connection factory to get a connection from. + */ + private final ConnectionFactory factory; + + /** + * Controls which mode the sync repl control should use. + */ + private final boolean refreshAndPersist; + + /** + * Controls the sync repl request reload hint. + */ + private final boolean reloadHint; + + /** + * Search operation handle. + */ + private SearchOperationHandle handle; + + /** + * Invoked when an entry is received. + */ + private Consumer onEntry; + + /** + * Invoked when a reference is received. + */ + private Consumer onReference; + + /** + * Invoked when a result is received. + */ + private Consumer onResult; + + /** + * Invoked when a sync info message is received. + */ + private Consumer onMessage; + + /** + * Invoked when an exception is received. + */ + private Consumer onException; + + /** + * Whether the sync repl search has received a result response. + */ + private boolean receivedResult; + + + /** + * Creates a new sync repl client. + * + * @param cf to get a connection from + * @param persist whether to refresh and persist or just refresh + */ + public SyncReplClient(final ConnectionFactory cf, final boolean persist) { + this(cf, persist, false); + } + + + /** + * Creates a new sync repl client. + * + * @param cf to get a connection from + * @param persist whether to refresh and persist or just refresh + * @param hint sync repl request reload hint + */ + public SyncReplClient(final ConnectionFactory cf, final boolean persist, final boolean hint) { + factory = cf; + refreshAndPersist = persist; + reloadHint = hint; + } + + + /** + * Returns the connection factory. + * + * @return connection factory + */ + public ConnectionFactory getConnectionFactory() { + return factory; + } + + + /** + * Sets the onEntry consumer. + * + * @param consumer to invoke when an entry is received + */ + public void setOnEntry(final Consumer consumer) { + onEntry = consumer; + } + + + /** + * Sets the onReference consumer. + * + * @param consumer to invoke when a reference is received + */ + public void setOnReference(final Consumer consumer) { + onReference = consumer; + } + + + /** + * Sets the onResult consumer. + * + * @param consumer to invoke when a result is received + */ + public void setOnResult(final Consumer consumer) { + onResult = consumer; + } + + + /** + * Sets the onMessage consumer. + * + * @param consumer to invoke when a sync info message is received + */ + public void setOnMessage(final Consumer consumer) { + onMessage = consumer; + } + + + /** + * Sets the onException consumer. + * + * @param consumer to invoke when a sync info message is received + */ + public void setOnException(final Consumer consumer) { + onException = consumer; + } + + + /** + * Invokes {@link #send(SearchRequest, CookieManager)} with a {@link DefaultCookieManager}. + * + * @param request search request to execute + * @return search operation handle + * @throws LdapException if the search fails + */ + public SearchOperationHandle send(final SearchRequest request) + throws LdapException { + return send(request, new DefaultCookieManager()); + } + + + /** + * Performs an async search operation with the {@link SyncRequestControl}. The supplied request is modified in the + * following way: + * + *
    + *
  • {@link SearchRequest#setControls(org.xbib.net.ldap.control.RequestControl...)} is invoked with {@link + * SyncRequestControl}
  • + *
+ * + *

The search request object should not be reused for any other search operations.

+ * + * @param request search request to execute + * @param manager for reading and writing cookies + * @return search operation handle + * @throws LdapException if the search fails + */ + public SearchOperationHandle send(final SearchRequest request, final CookieManager manager) + throws LdapException { + request.setControls( + new SyncRequestControl( + refreshAndPersist ? SyncRequestControl.Mode.REFRESH_AND_PERSIST : SyncRequestControl.Mode.REFRESH_ONLY, + manager.readCookie(), + reloadHint, + true)); + + final SearchOperation search = new SearchOperation(factory, request); + search.setResultHandlers(result -> { + receivedResult = true; + if (result.getControl(SyncDoneControl.OID) != null) { + final SyncDoneControl syncDoneControl = (SyncDoneControl) result.getControl(SyncDoneControl.OID); + final byte[] cookie = syncDoneControl.getCookie(); + if (cookie != null) { + try { + manager.writeCookie(cookie); + } catch (Exception e) { + // + } + } + } + if (onResult != null) { + try { + onResult.accept(result); + } catch (Exception e) { + if (onException != null) { + try { + onException.accept(e); + } catch (Exception ex) { + // + } + } + } + } + }); + search.setExceptionHandler(e -> { + if (onException != null) { + try { + onException.accept(e); + } catch (Exception ex) { + // + } + } + }); + search.setEntryHandlers(entry -> { + if (entry.getControl(SyncStateControl.OID) != null) { + final SyncStateControl syncStateControl = (SyncStateControl) entry.getControl(SyncStateControl.OID); + final byte[] cookie = syncStateControl.getCookie(); + if (cookie != null) { + try { + manager.writeCookie(cookie); + } catch (Exception e) { + // + } + } + } + if (onEntry != null) { + try { + onEntry.accept(entry); + } catch (Exception e) { + if (onException != null) { + try { + onException.accept(e); + } catch (Exception ex) { + // + } + } + } + } + return null; + }); + search.setReferenceHandlers(reference -> { + if (reference.getControl(SyncStateControl.OID) != null) { + final SyncStateControl syncStateControl = (SyncStateControl) reference.getControl(SyncStateControl.OID); + final byte[] cookie = syncStateControl.getCookie(); + if (cookie != null) { + try { + manager.writeCookie(cookie); + } catch (Exception e) { + // + } + } + } + if (onReference != null) { + try { + onReference.accept(reference); + } catch (Exception e) { + if (onException != null) { + try { + onException.accept(e); + } catch (Exception ex) { + // + } + } + } + } + }); + search.setIntermediateResponseHandlers(response -> { + if (SyncInfoMessage.OID.equals(response.getResponseName())) { + final SyncInfoMessage message = (SyncInfoMessage) response; + if (message.getCookie() != null) { + try { + manager.writeCookie(message.getCookie()); + } catch (Exception e) { + // + } + } + if (onMessage != null) { + try { + onMessage.accept(message); + } catch (Exception e) { + if (onException != null) { + try { + onException.accept(e); + } catch (Exception ex) { + // + } + } + } + } + } + }); + + receivedResult = false; + handle = search.send(); + return handle; + } + + + /** + * Returns whether a search result has been received by this client. + * + * @return whether a search result has been received + */ + public boolean isComplete() { + return receivedResult; + } + + + /** + * Sends a cancel operation on the underlying search operation. See {@link + * org.xbib.net.ldap.transport.DefaultOperationHandle#cancel()}. + * + * @return cancel operation result + */ + public ExtendedOperationHandle cancel() { + return handle.cancel().send(); + } + + + /** + * Closes the connection factory. + */ + public void close() { + factory.close(); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "factory=" + factory + ", " + + "refreshAndPersist=" + refreshAndPersist + ", " + + "onEntry=" + onEntry + ", " + + "onResult=" + onResult + ", " + + "onMessage=" + onMessage + ", " + + "onException=" + onException + ", " + + "handle=" + handle; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplCookie.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplCookie.java new file mode 100644 index 0000000..71f6a9e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplCookie.java @@ -0,0 +1,177 @@ + +package org.xbib.net.ldap.control.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class for parsing a sync repl cookie. + *

+ * See https://www.openldap.org/doc/admin23/syncrepl.html and https://www.openldap.org/faq/data/cache/1145.html + * + */ +public class SyncReplCookie { + + /** + * Cookie RID. + */ + private final String rid; + + /** + * Cookie CSN. + */ + private final CSN csn; + + + /** + * Creates a new sync repl cookie. + * + * @param cookie to parse + */ + public SyncReplCookie(final String cookie) { + final Map nameValues = parseCookie(cookie); + if (!nameValues.containsKey("rid")) { + throw new IllegalArgumentException("Could not parse 'rid' from " + cookie); + } + if (!nameValues.containsKey("csn")) { + throw new IllegalArgumentException("Could not parse 'csn' from " + cookie); + } + rid = nameValues.get("rid"); + csn = new CSN(nameValues.get("csn")); + } + + /** + * Parses the name/value pairs in the supplied cookie. + * + * @param cookie to parse + * @return map of name/value pairs + */ + private static Map parseCookie(final String cookie) { + final Map parsedCookie = new HashMap<>(2); + final String[] nameValuePairs = cookie.split(","); + for (String nameValuePair : nameValuePairs) { + final String[] nameValue = nameValuePair.split("="); + parsedCookie.put(nameValue[0], nameValue[1]); + } + return parsedCookie; + } + + /** + * Returns the RID. + * + * @return cookie RID + */ + public String getRid() { + return rid; + } + + /** + * Returns the CSN. + * + * @return cookie CSN + */ + public CSN getCsn() { + return csn; + } + + /** + * Class representing a Change Sequence Number. + */ + public static class CSN { + + /** + * Entire CSN value. + */ + private final String value; + + /** + * CSN time. + */ + private final String time; + + /** + * CSN count. + */ + private final String count; + + /** + * CSN sid. + */ + private final String sid; + + /** + * CSN mod. + */ + private final String mod; + + + /** + * Creates a new CSN with the supplied string. + * + * @param csn to parse + */ + public CSN(final String csn) { + // CheckStyle:MagicNumber OFF + value = csn; + final String[] csnParts = csn.split("#"); + if (csnParts.length != 4) { + throw new IllegalArgumentException("CSN does not contain 4 parts: " + csn); + } + time = csnParts[0]; + count = csnParts[1]; + sid = csnParts[2]; + mod = csnParts[3]; + // CheckStyle:MagicNumber ON + } + + + /** + * Returns the entire value of the CSN. + * + * @return entire value + */ + public String getValue() { + return value; + } + + + /** + * Returns the time part of the CSN + * + * @return CSN time + */ + public String getTime() { + return time; + } + + + /** + * Returns the count part of the CSN + * + * @return CSN count + */ + public String getCount() { + return count; + } + + + /** + * Returns the sid part of the CSN + * + * @return CSN sid + */ + public String getSid() { + return sid; + } + + + /** + * Returns the mod part of the CSN + * + * @return CSN mod + */ + public String getMod() { + return mod; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplRunner.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplRunner.java new file mode 100644 index 0000000..68ae4dd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/SyncReplRunner.java @@ -0,0 +1,384 @@ + +package org.xbib.net.ldap.control.util; + +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.xbib.net.ldap.AbstractConnectionValidator; +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.ConnectionValidator; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.SearchConnectionValidator; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResultReference; +import org.xbib.net.ldap.SingleConnectionFactory; +import org.xbib.net.ldap.extended.SyncInfoMessage; +import org.xbib.net.ldap.transport.Transport; +import org.xbib.net.ldap.transport.netty.ConnectionFactoryTransport; + +/** + * Class that executes a {@link SyncReplClient} and expects to run continuously, reconnecting if the server is + * unavailable. Consumers must be registered to handle entries, results, and messages as they are returned from the + * server. If the connection validator fails, the runner will be stopped and started, then the sync repl search + * will execute again. Consumers cannot execute blocking LDAP operations on the same connection because the next + * incoming message is not read until the consumer has completed. + * + */ +public class SyncReplRunner { + + /** + * Number of I/O worker threads. + */ + private static final int IO_WORKER_THREADS = 1; + + /** + * Number of message worker threads. + */ + private static final int MESSAGE_WORKER_THREADS = 4; + + /** + * Sync repl search request. + */ + private final SearchRequest searchRequest; + + /** + * Sync repl cookie manager. + */ + private final CookieManager cookieManager; + + /** + * Search operation handle. + */ + private SyncReplClient syncReplClient; + + /** + * Invoked when {@link #start()} begins. + */ + private Supplier onStart; + + /** + * Invoked when an entry is received. + */ + private Consumer onEntry; + + /** + * Invoked when a reference is received. + */ + private Consumer onReference; + + /** + * Invoked when a result is received. + */ + private Consumer onResult; + + /** + * Invoked when a sync info message is received. + */ + private Consumer onMessage; + + /** + * Invoked when an exception occurs. + */ + private Consumer onException; + + /** + * Whether the sync repl search is running. + */ + private boolean started; + + + /** + * Creates a new sync repl runner. The supplied connection factory is modified to invoke {@link + * SyncReplClient#send(SearchRequest, CookieManager)} when the connection opens and {@link SyncReplClient#cancel()} + * when the connection closes. + * + * @param cf to get a connection from + * @param request sync repl search request + * @param manager sync repl cookie manager + */ + public SyncReplRunner(final SingleConnectionFactory cf, final SearchRequest request, final CookieManager manager) { + syncReplClient = new SyncReplClient(cf, true); + searchRequest = request; + cookieManager = manager; + cf.setOnOpen(conn -> { + try { + syncReplClient.send(searchRequest, cookieManager); + } catch (LdapException e) { + return false; + } + return true; + }); + cf.setOnClose(conn -> { + try { + if (!syncReplClient.isComplete()) { + syncReplClient.cancel(); + } + } catch (Exception e) { + return false; + } + return true; + }); + } + + + /** + * Creates a new single connection factory. Uses a {@link SearchConnectionValidator} for connection validation. See + * {@link #createTransport()} + * + * @param config sync repl connection configuration + * @return single connection factory for use with a sync repl runner + */ + public static SingleConnectionFactory createConnectionFactory(final ConnectionConfig config) { + // CheckStyle:MagicNumber OFF + return createConnectionFactory( + config, + SearchConnectionValidator.builder() + .period(Duration.ofMinutes(1)) + .timeout(Duration.ofSeconds(5)) + .timeoutIsFailure(false) + .build()); + // CheckStyle:MagicNumber ON + } + + + /** + * Creates a new single connection factory. See {@link #createTransport()}. + * + * @param config sync repl connection configuration + * @param validator connection validator + * @return single connection factory for use with a sync repl runner + */ + public static SingleConnectionFactory createConnectionFactory( + final ConnectionConfig config, final ConnectionValidator validator) { + return createConnectionFactory(createTransport(), config, validator); + } + + + /** + * Creates a new single connection factory. + * + * @param transport sync repl connection transport + * @param config sync repl connection configuration + * @param validator connection validator + * @return single connection factory for use with a sync repl runner + */ + public static SingleConnectionFactory createConnectionFactory( + final Transport transport, final ConnectionConfig config, final ConnectionValidator validator) { + final SingleConnectionFactory factory = new SingleConnectionFactory(config, transport); + factory.setValidator(validator); + configureConnectionFactory(factory); + return factory; + } + + + /** + * Configures the supplied factory for use with a {@link SyncReplRunner}. The factory's configuration will have the + * following modifications: + *

    + *
  • {@link ConnectionConfig#setTransportOption(String, Object)} of AUTO_READ to false
  • + *
  • {@link ConnectionConfig#setAutoReconnect(boolean)} to false
  • + *
  • {@link ConnectionConfig#setAutoReplay(boolean)} to false
  • + *
  • {@link SingleConnectionFactory#setFailFastInitialize(boolean)} to false
  • + *
  • {@link SingleConnectionFactory#setNonBlockingInitialize(boolean)} to false
  • + *
  • {@link AbstractConnectionValidator#setOnFailure(Consumer)} to + * {@link SingleConnectionFactory.ReinitializeConnectionConsumer}
  • + *
+ * + * @param factory to configure + */ + public static void configureConnectionFactory(final SingleConnectionFactory factory) { + final ConnectionConfig newConfig = ConnectionConfig.copy(factory.getConnectionConfig()); + newConfig.setTransportOption("AUTO_READ", false); + newConfig.setAutoReconnect(false); + newConfig.setAutoReplay(false); + factory.setConnectionConfig(newConfig); + factory.setFailFastInitialize(false); + factory.setNonBlockingInitialize(false); + if (factory.getValidator() instanceof AbstractConnectionValidator) { + final AbstractConnectionValidator validator = (AbstractConnectionValidator) factory.getValidator(); + if (validator.getOnFailure() == null) { + validator.setOnFailure(factory.new ReinitializeConnectionConsumer()); + } + } + } + + + /** + * Returns a transport configured to use for sync repl. Uses its own event loop groups with auto_read set to false. + * Detects whether Epoll or KQueue transports are available, otherwise uses NIO. + * + * @return transport + */ + private static Transport createTransport() { + // message thread pool size must be >2 since exceptions are reported on the messages thread pool and flow control + // requires a thread to signal reads and pass user events + // startTLS and connection initializers will require additional threads + final ConnectionFactoryTransport transport = new ConnectionFactoryTransport( + SyncReplRunner.class.getSimpleName(), + IO_WORKER_THREADS, + MESSAGE_WORKER_THREADS); + transport.setShutdownOnClose(false); + return transport; + } + + + /** + * Sets the onStart supplier. + * + * @param supplier to invoke on start + */ + public void setOnStart(final Supplier supplier) { + onStart = supplier; + } + + + /** + * Sets the onEntry consumer. + * + * @param consumer to invoke when an entry is received + */ + public void setOnEntry(final Consumer consumer) { + onEntry = consumer; + } + + + /** + * Sets the onReference consumer. + * + * @param consumer to invoke when a reference is received + */ + public void setOnReference(final Consumer consumer) { + onReference = consumer; + } + + + /** + * Sets the onResult consumer. + * + * @param consumer to invoke when a result is received + */ + public void setOnResult(final Consumer consumer) { + onResult = consumer; + } + + + /** + * Sets the onMessage consumer. + * + * @param consumer to invoke when a sync info message is received + */ + public void setOnMessage(final Consumer consumer) { + onMessage = consumer; + } + + + /** + * Sets the onException consumer. + * + * @param consumer to invoke when an exception is received + */ + public void setOnException(final Consumer consumer) { + onException = consumer; + } + + + /** + * Prepare this runner for use. + */ + public synchronized void initialize() { + if (started) { + throw new IllegalStateException("Runner has already been started"); + } + syncReplClient.setOnEntry(onEntry); + syncReplClient.setOnReference(onReference); + syncReplClient.setOnResult(onResult); + syncReplClient.setOnMessage(onMessage); + syncReplClient.setOnException(onException); + } + + + /** + * Starts this runner. + */ + public synchronized void start() { + if (started) { + throw new IllegalStateException("Runner has already been started"); + } + try { + if (onStart != null && !onStart.get()) { + throw new RuntimeException("Start aborted from " + onStart); + } + // the connection factory may be shared between multiple runners + if (!((SingleConnectionFactory) syncReplClient.getConnectionFactory()).isInitialized()) { + ((SingleConnectionFactory) syncReplClient.getConnectionFactory()).initialize(); + } + started = true; + } catch (Exception e) { + // + } + } + + + /** + * Stops this runner. + */ + public synchronized void stop() { + if (!started) { + throw new IllegalStateException("Runner has not been started"); + } + if (syncReplClient != null) { + syncReplClient.close(); + } + started = false; + } + + + /** + * Returns whether this runner is started. + * + * @return whether this runner is started + */ + public boolean isStarted() { + return started; + } + + + /** + * Cancels the sync repl search and sends a new search request. + */ + public synchronized void restartSearch() { + if (!started) { + throw new IllegalStateException("Cannot restart the search, runner is stopped"); + } + try { + if (!syncReplClient.isComplete()) { + syncReplClient.cancel(); + } + } catch (Exception e) { + // + } + try { + syncReplClient.send(searchRequest, cookieManager); + } catch (LdapException e) { + throw new IllegalStateException("Could not send sync repl request", e); + } + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "syncReplClient=" + syncReplClient + ", " + + "searchRequest=" + searchRequest + ", " + + "cookieManager=" + cookieManager + ", " + + "onStart=" + onStart + ", " + + "onEntry=" + onEntry + ", " + + "onReference=" + onReference + ", " + + "onResult=" + onResult + ", " + + "onMessage=" + onMessage + ", " + + "onException=" + onException + ", " + + "started=" + started; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/VirtualListViewClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/VirtualListViewClient.java new file mode 100644 index 0000000..c9418ad --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/VirtualListViewClient.java @@ -0,0 +1,229 @@ + +package org.xbib.net.ldap.control.util; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.xbib.net.ldap.AbstractSearchOperationFactory; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.control.SortKey; +import org.xbib.net.ldap.control.SortRequestControl; +import org.xbib.net.ldap.control.VirtualListViewRequestControl; +import org.xbib.net.ldap.control.VirtualListViewResponseControl; +import org.xbib.net.ldap.handler.LdapEntryHandler; + +/** + * Client that simplifies using the virtual list view control. + * + */ +public class VirtualListViewClient extends AbstractSearchOperationFactory { + /** + * Used on the search operation. + */ + private final SortRequestControl sortControl; + + + /** + * Creates a new virtual list view client. + * + * @param cf to get a connection from + * @param keys to supply to a sort request control + */ + public VirtualListViewClient(final ConnectionFactory cf, final SortKey... keys) { + setConnectionFactory(cf); + sortControl = new SortRequestControl(keys); + } + + + /** + * Performs a search operation with the {@link org.xbib.net.ldap.control.VirtualListViewRequestControl}. The supplied + * request is modified in the following way: + * + *
    + *
  • {@link SearchRequest#setControls(RequestControl...)} is invoked with {@link SortRequestControl} and {@link + * VirtualListViewRequestControl} and any other controls previously set on the search request.
  • + *
+ * + * @param request search request to execute + * @param params virtual list view data + * @return search operation response + * @throws LdapException if the search fails + */ + public SearchResponse execute(final SearchRequest request, final VirtualListViewParams params) + throws LdapException { + final SearchOperation search = createSearchOperation(); + request.setControls(appendRequestControls(request, params.createRequestControl(true))); + final SearchResponse response = search.execute(request); + final byte[] cookie = getVirtualListViewCookie(response); + if (cookie != null && params.getCookieManager() != null) { + params.getCookieManager().writeCookie(cookie); + } + return response; + } + + + /** + * Performs a search operation with the {@link VirtualListViewRequestControl}. The supplied + * request is modified in the following way: + * + *
    + *
  • {@link SearchRequest#setControls(RequestControl...)} is invoked with {@link SortRequestControl} and {@link + * VirtualListViewRequestControl} and any other controls previously set on the search request.
  • + *
+ * + *

The content count and context id are extracted from the supplied response and replayed as appropriate in the + * request.

+ * + * @param request search request to execute + * @param params virtual list view data + * @param result of a previous VLV operation + * @return search operation response + * @throws LdapException if the search fails + */ + public SearchResponse execute( + final SearchRequest request, + final VirtualListViewParams params, + final SearchResponse result) + throws LdapException { + final SearchOperation search = createSearchOperation(); + request.setControls(appendRequestControls(request, params.createRequestControl(result, true))); + final SearchResponse response = search.execute(request); + final byte[] cookie = getVirtualListViewCookie(response); + if (cookie != null && params.getCookieManager() != null) { + params.getCookieManager().writeCookie(cookie); + } + return response; + } + + + /** + * Performs a search operation with the {@link VirtualListViewRequestControl}. The supplied request is modified in the + * following way: + * + *
    + *
  • {@link SearchRequest#setControls(RequestControl...)} is invoked with {@link SortRequestControl} and {@link + * VirtualListViewRequestControl} and any other controls previously set on the search request.
  • + *
+ * + *

This method will continue to execute search operations until all search entries have been retrieved from the + * server. The returned response contains the response data of the last search result operation plus the entries and + * references returned by all previous search operations. The criteria used ot determine whether to continue searching + * is that the last response contained a cookie, produced a success result code, has a greater than zero contentCount + * and we have currently processed less entries than the contentCount.

+ * + *

The cookie used for each request is read from the cookie manager and written to the cookie manager after a + * successful search, if the response contains a cookie.

+ * + *

This method builds a synthetic response which contains the results of all search operations. Any ordering + * imposed by result handlers may be lost by this process.

+ * + * @param request search request to execute + * @param params virtual list view data + * @return search operation response of the last paged result operation + * @throws LdapException if the search fails + */ + public SearchResponse executeToCompletion( + final SearchRequest request, + final VirtualListViewParams params) + throws LdapException { + SearchResponse result = null; + final SearchResponse combinedResults = new SearchResponse(); + final SearchOperation search = createSearchOperation(); + final AtomicInteger entryCount = new AtomicInteger(); + final LdapEntryHandler[] handlers = search.getEntryHandlers(); + search.setEntryHandlers(LdapUtils.concatArrays(new LdapEntryHandler[]{e -> { + entryCount.incrementAndGet(); + return e; + }}, handlers)); + + VirtualListViewParams newParams = params; + byte[] cookie; + int contentCount; + ResultCode ctrlResult; + do { + if (result != null) { + combinedResults.addEntries(result.getEntries()); + combinedResults.addReferences(result.getReferences()); + // move the target offset by the size of the after count + newParams = new VirtualListViewParams( + newParams.getTargetOffset() + newParams.getAfterCount() + 1, 0, params.getAfterCount()); + request.setControls(appendRequestControls(request, newParams.createRequestControl(result, true))); + } else { + request.setControls(appendRequestControls(request, newParams.createRequestControl(true))); + } + + result = search.execute(request); + final VirtualListViewResponseControl ctrl = getResponseControl(result); + contentCount = ctrl != null ? ctrl.getContentCount() : 0; + cookie = ctrl != null ? ctrl.getContextID() : null; + ctrlResult = ctrl != null ? ctrl.getViewResult() : null; + if (cookie != null) { + newParams.getCookieManager().writeCookie(cookie); + } + } while ( + cookie != null && ResultCode.SUCCESS.equals(ctrlResult) && contentCount > 0 && entryCount.get() < contentCount); + result.addEntries(combinedResults.getEntries()); + result.addReferences(combinedResults.getReferences()); + return result; + } + + + /** + * Returns the {@link VirtualListViewResponseControl} in the supplied response. + * + * @param result to inspect for a response control + * @return VLV response control or null if it does not exist + */ + public VirtualListViewResponseControl getResponseControl(final SearchResponse result) { + return (VirtualListViewResponseControl) result.getControl(VirtualListViewResponseControl.OID); + } + + + /** + * Returns the VLV results cookie in the supplied response or null if no cookie exists. + * + * @param result of a previous VLV results operation + * @return VLV results cookie or null + */ + protected byte[] getVirtualListViewCookie(final SearchResponse result) { + byte[] cookie = null; + final VirtualListViewResponseControl ctl = (VirtualListViewResponseControl) result.getControl( + VirtualListViewResponseControl.OID); + if (ctl != null) { + if (ctl.getContextID() != null && ctl.getContextID().length > 0) { + cookie = ctl.getContextID(); + } + } + return cookie; + } + + + /** + * Creates a new array of request controls which includes the VLV and sort controls. Any other request controls are + * in included + * + * @param request to read controls from + * @param cntrl VLV control to include + * @return array of request controls ready to be used in a search operation + */ + private RequestControl[] appendRequestControls(final SearchRequest request, final VirtualListViewRequestControl cntrl) { + if (request.getControls() != null && request.getControls().length > 0) { + final List requestControls = Arrays.stream( + request.getControls()).filter(c -> !(c instanceof VirtualListViewRequestControl) && !c.equals(sortControl)) + .collect(Collectors.toList()); + requestControls.add(sortControl); + requestControls.add(cntrl); + return requestControls.toArray(RequestControl[]::new); + } else { + return new RequestControl[]{sortControl, cntrl}; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/control/util/VirtualListViewParams.java b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/VirtualListViewParams.java new file mode 100644 index 0000000..36f5be0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/control/util/VirtualListViewParams.java @@ -0,0 +1,226 @@ + +package org.xbib.net.ldap.control.util; + +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.control.VirtualListViewRequestControl; +import org.xbib.net.ldap.control.VirtualListViewResponseControl; + +/** + * Contains data required by the virtual list view operation. + * + */ +public class VirtualListViewParams { + + /** + * VLV before count. + */ + private final int beforeCount; + + /** + * VLV after count. + */ + private final int afterCount; + + /** + * VLV target offset; mutually exclusive with the assertion value. + */ + private final int targetOffset; + + /** + * VLV assertion value; mutually exclusive with the target offset. + */ + private final String assertionValue; + + /** + * Cookie manager for VLV context ID. + */ + private final CookieManager cookieManager; + + + /** + * Creates a new virtual list view params. + * + * @param offset target offset + * @param before before count + * @param after after count + */ + public VirtualListViewParams(final int offset, final int before, final int after) { + targetOffset = offset; + beforeCount = before; + afterCount = after; + assertionValue = null; + cookieManager = new DefaultCookieManager(); + } + + + /** + * Creates a new virtual list view params. + * + * @param offset target offset + * @param before before count + * @param after after count + * @param manager cookie manager + */ + public VirtualListViewParams(final int offset, final int before, final int after, final CookieManager manager) { + targetOffset = offset; + beforeCount = before; + afterCount = after; + assertionValue = null; + cookieManager = manager; + } + + + /** + * Creates a new virtual list view params. + * + * @param assertion assertion value + * @param before before count + * @param after after count + */ + public VirtualListViewParams(final String assertion, final int before, final int after) { + assertionValue = assertion; + beforeCount = before; + afterCount = after; + targetOffset = 0; + cookieManager = new DefaultCookieManager(); + } + + + /** + * Creates a new virtual list view params. + * + * @param assertion assertion value + * @param before before count + * @param after after count + * @param manager cookie manager + */ + public VirtualListViewParams(final String assertion, final int before, final int after, final CookieManager manager) { + assertionValue = assertion; + beforeCount = before; + afterCount = after; + targetOffset = 0; + cookieManager = manager; + } + + + /** + * Returns the before count. + * + * @return before count + */ + public int getBeforeCount() { + return beforeCount; + } + + + /** + * Returns the after count. + * + * @return after count + */ + public int getAfterCount() { + return afterCount; + } + + + /** + * Returns the target offset. + * + * @return target offset + */ + public int getTargetOffset() { + return targetOffset; + } + + + /** + * Returns the assertion value. + * + * @return assertion value + */ + public String getAssertionValue() { + return assertionValue; + } + + + /** + * Returns the cookie manager. + * + * @return cookie manager + */ + public CookieManager getCookieManager() { + return cookieManager; + } + + + /** + * Creates a new virtual list view request control using the properties in this VLV params. + * + * @param critical whether the returned control is critical + * @return virtual list view request control + */ + public VirtualListViewRequestControl createRequestControl(final boolean critical) { + if (assertionValue != null) { + return new VirtualListViewRequestControl( + assertionValue, beforeCount, afterCount, cookieManager.readCookie(), critical); + } else { + return new VirtualListViewRequestControl( + targetOffset, beforeCount, afterCount, cookieManager.readCookie(), critical); + } + } + + + /** + * Creates a new virtual list view request control using the properties in this VLV params. + * + * @param critical whether the returned control is critical + * @param manager cookie manager + * @return virtual list view request control + */ + public VirtualListViewRequestControl createRequestControl(final boolean critical, final CookieManager manager) { + if (assertionValue != null) { + return new VirtualListViewRequestControl(assertionValue, beforeCount, afterCount, manager.readCookie(), critical); + } else { + return new VirtualListViewRequestControl(targetOffset, beforeCount, afterCount, manager.readCookie(), critical); + } + } + + + /** + * Creates a new virtual list view request control using the properties in this VLV params. The supplied response is + * inspected and if it contains a VLV response control, its contextID and/or content count will be passed into the + * created request control. + * + * @param result of a previous VLV operation + * @param critical whether the returned control is critical + * @return virtual list view request control + */ + public VirtualListViewRequestControl createRequestControl(final SearchResponse result, final boolean critical) { + final VirtualListViewRequestControl control = createRequestControl(critical); + final VirtualListViewResponseControl responseControl = (VirtualListViewResponseControl) result.getControl( + VirtualListViewResponseControl.OID); + if (responseControl != null) { + if (assertionValue == null) { + control.setContentCount(responseControl.getContentCount()); + } + control.setContextID(responseControl.getContextID()); + } + return control; + } + + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("[") + .append(getClass().getName()).append("@").append(hashCode()).append("::"); + if (assertionValue != null) { + sb.append("assertionValue=").append(assertionValue).append(", "); + } else { + sb.append("targetOffset=").append(targetOffset).append(", "); + } + sb.append("beforeCount=").append(beforeCount).append(", "); + sb.append("afterCount=").append(afterCount).append(", "); + sb.append("cookieManager=").append(cookieManager).append("]"); + return sb.toString(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/AbstractAttributeValueEscaper.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/AbstractAttributeValueEscaper.java new file mode 100644 index 0000000..b9bf2ce --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/AbstractAttributeValueEscaper.java @@ -0,0 +1,105 @@ + +package org.xbib.net.ldap.dn; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Escapes an attribute value per RFC 4514 section 2.4. Implementations must decide how to handle unspecified + * characters. + * + */ +public abstract class AbstractAttributeValueEscaper implements AttributeValueEscaper { + + + @Override + public String escape(final String value) { + if (value == null || value.isEmpty()) { + return value; + } + + final int len = value.length(); + final StringBuilder sb = new StringBuilder(len); + char ch; + for (int i = 0; i < len; i++) { + ch = value.charAt(i); + switch (ch) { + + case '"': + case '#': + case '+': + case ',': + case ';': + case '<': + case '=': + case '>': + case '\\': + sb.append('\\').append(ch); + break; + + case ' ': + // escape first space and last space + if (i == 0 || i + 1 == len) { + sb.append('\\').append(ch); + } else { + sb.append(ch); + } + break; + + case 0: + // escape null + sb.append("\\00"); + break; + + default: + // CheckStyle:MagicNumber OFF + if (ch <= 127) { + processAscii(sb, ch); + } else if (i + 1 < len && Character.isHighSurrogate(ch)) { + final char nextChar = value.charAt(++i); + if (Character.isLowSurrogate(nextChar)) { + final int codePoint = Character.toCodePoint(ch, nextChar); + processNonAscii(sb, LdapUtils.utf8Encode(new String(new int[]{codePoint}, 0, 1))); + } else { + processNonAscii(sb, LdapUtils.utf8Encode(String.valueOf(ch))); + } + } else { + processNonAscii(sb, LdapUtils.utf8Encode(String.valueOf(ch))); + } + // CheckStyle:MagicNumber ON + break; + } + } + return sb.toString(); + } + + + /** + * Process ASCII character. + * + * @param sb to append characters to + * @param ch to process + */ + protected abstract void processAscii(StringBuilder sb, char ch); + + + /** + * Process non-ASCII character(s). + * + * @param sb to append characters to + * @param bytes to process + */ + protected abstract void processNonAscii(StringBuilder sb, byte... bytes); + + + /** + * Appends a backslash for every two hex characters. + * + * @param sb to append to + * @param hex to read + */ + protected void escapeHex(final StringBuilder sb, final char... hex) { + for (int i = 0; i < hex.length; i += 2) { + sb.append('\\').append(hex[i]).append(hex[i + 1]); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/AttributeValueEscaper.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/AttributeValueEscaper.java new file mode 100644 index 0000000..af44a20 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/AttributeValueEscaper.java @@ -0,0 +1,18 @@ + +package org.xbib.net.ldap.dn; + +/** + * Interface for escaping attribute values. + * + */ +public interface AttributeValueEscaper { + + + /** + * Escapes the supplied attribute value. + * + * @param value to escape + * @return escaped value + */ + String escape(String value); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultAttributeValueEscaper.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultAttributeValueEscaper.java new file mode 100644 index 0000000..692d95d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultAttributeValueEscaper.java @@ -0,0 +1,29 @@ + +package org.xbib.net.ldap.dn; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Escapes an attribute value per RFC 4514 section 2.4. ASCII control characters and all non-ASCII data is HEX encoded. + * + */ +public class DefaultAttributeValueEscaper extends AbstractAttributeValueEscaper { + + + @Override + protected void processAscii(final StringBuilder sb, final char ch) { + // CheckStyle:MagicNumber OFF + if (ch > 31 && ch < 127) { + sb.append(ch); + } else { + escapeHex(sb, LdapUtils.hexEncode(ch)); + } + // CheckStyle:MagicNumber ON + } + + + @Override + protected void processNonAscii(final StringBuilder sb, final byte... bytes) { + escapeHex(sb, LdapUtils.hexEncode(bytes)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultDnParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultDnParser.java new file mode 100644 index 0000000..6f4c0be --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultDnParser.java @@ -0,0 +1,222 @@ + +package org.xbib.net.ldap.dn; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.DefaultDERBuffer; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.ParseHandler; + +/** + * Parses DNs following the rules in RFC 4514. Attempts to be as + * generous as possible in the format of allowed DNs. + * + */ +public final class DefaultDnParser implements DnParser { + + /** + * Hexadecimal radix. + */ + private static final int HEX_RADIX = 16; + + /** + * DER path for hex values. + */ + private static final DERPath HEX_PATH = new DERPath("/OCTSTR[0]"); + + /** + * Decodes the supplied hexadecimal value. + * + * @param value hex to decode + * @return decoded bytes + */ + private static byte[] decodeHexValue(final char[] value) { + if (value == null || value.length == 0) { + throw new IllegalArgumentException("Invalid HEX value: value cannot be null or empty"); + } + return LdapUtils.hexDecode(value); + } + + /** + * Decodes the supplied string attribute value. Unescapes escaped characters. If escaped character is a hex value, it + * is decoded. + * + * @param value to decode + * @return decoded string + */ + private static String decodeStringValue(final String value) { + final StringBuilder sb = new StringBuilder(); + int pos = 0; + final StringBuilder hexValue = new StringBuilder(); + while (pos < value.length()) { + char c = value.charAt(pos); + boolean appendHex = false; + boolean appendValue = false; + if (c == '\\') { + if (pos + 1 < value.length()) { + c = value.charAt(++pos); + // if hexadecimal character add to buffer to decode later + if (Character.digit(c, HEX_RADIX) != -1) { + if (pos + 1 < value.length()) { + hexValue.append(c).append(value.charAt(++pos)); + if (pos + 1 == value.length()) { + appendHex = true; + } + } else { + throw new IllegalArgumentException("Invalid HEX value: " + c); + } + } else { + appendHex = hexValue.length() > 0; + appendValue = true; + } + } + } else { + appendHex = hexValue.length() > 0; + appendValue = true; + } + if (appendHex) { + sb.append(LdapUtils.utf8Encode(decodeHexValue(hexValue.toString().toCharArray()))); + hexValue.setLength(0); + } + if (appendValue) { + sb.append(c); + } + + pos++; + } + return sb.toString(); + } + + /** + * Reads the supplied string starting at the supplied position until one of the supplied characters is found. + * Characters escaped with '\' are ignored. Characters inside of quotes are ignored. + * + * @param s to read + * @param chars to match + * @param pos to start reading at + * @return string index that matched a character or the last index in the string + */ + private static int[] readToChar(final String s, final char[] chars, final int pos) { + int i = pos; + int matchChar = -1; + // 0 = no quotes, 1 = in quotes, 2 = after quotes + int quotes = 0; + while (i < s.length()) { + boolean match = false; + final char sChar = s.charAt(i); + // ignore escaped characters + if (sChar == '\\') { + i++; + if (i == s.length()) { + // attribute value ends with a backslash, be lenient and ignore it + break; + } + } else if (sChar == '"') { + quotes++; + } else if (quotes != 1) { + // do not check for match characters inside of quotes + for (char c : chars) { + if (c == s.charAt(i)) { + matchChar = c; + match = true; + break; + } + } + if (match) { + break; + } + } + i++; + } + return new int[]{i, matchChar}; + } + + /** + * Parses the supplied DN into a list of RDNs. + * + * @param dn to parse + * @return unmodifiable list of RDNs + */ + public List parse(final String dn) { + if (LdapUtils.trimSpace(dn).isEmpty()) { + return Collections.emptyList(); + } + + final List rdns = new ArrayList<>(); + final List nameValues = new ArrayList<>(); + int pos = 0; + while (pos < dn.length()) { + final int[] endAttrNamePos = readToChar(dn, new char[]{'='}, pos); + final String attrName = LdapUtils.trimSpace(dn.substring(pos, endAttrNamePos[0])); + if (attrName.isEmpty()) { + throw new IllegalArgumentException("Invalid RDN: no attribute name found for " + dn); + } else if (attrName.contains("+") || attrName.contains(",")) { + throw new IllegalArgumentException("Invalid RDN: unexpected '" + attrName.charAt(0) + "' for " + dn); + } + pos = endAttrNamePos[0]; + // error if char isn't an '=' + if (pos >= dn.length() || dn.charAt(pos++) != '=') { + throw new IllegalArgumentException("Invalid RDN: no equals found for " + dn); + } + + final int[] endAttrValuePos = readToChar(dn, new char[]{'+', ','}, pos); + final String attrValue = LdapUtils.trimSpace(dn.substring(pos, endAttrValuePos[0])); + if (attrValue.isEmpty()) { + nameValues.add(new NameValue(attrName, "")); + } else if (attrValue.startsWith("#")) { + final DERParser parser = new DERParser(); + final OctetStringHandler handler = new OctetStringHandler(); + parser.registerHandler(HEX_PATH, handler); + + final String hexData = attrValue.substring(1); + parser.parse(new DefaultDERBuffer(decodeHexValue(hexData.toCharArray()))); + nameValues.add(new NameValue(attrName, handler.getDecodedValue())); + } else { + nameValues.add(new NameValue(attrName, decodeStringValue(attrValue))); + } + if (endAttrValuePos[1] == -1 || endAttrValuePos[1] == ',') { + rdns.add(new RDn(nameValues)); + nameValues.clear(); + } + pos = endAttrValuePos[0] + 1; + if (pos == dn.length() && endAttrValuePos[1] != -1) { + // dangling match character + throw new IllegalArgumentException( + "Invalid RDN: attribute value ends with '" + endAttrValuePos[1] + "' for " + dn); + } + } + return Collections.unmodifiableList(rdns); + } + + /** + * Parse handler for decoding octet strings. + */ + private static final class OctetStringHandler implements ParseHandler { + + /** + * Decoded octet string. + */ + private String decoded; + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + decoded = OctetStringType.decode(encoded); + } + + + /** + * Returns the decoded octet string value. + * + * @return decoded octet string + */ + public String getDecodedValue() { + return decoded; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultRDnNormalizer.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultRDnNormalizer.java new file mode 100644 index 0000000..4a65ebb --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/DefaultRDnNormalizer.java @@ -0,0 +1,173 @@ + +package org.xbib.net.ldap.dn; + +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.xbib.net.ldap.LdapUtils; + +/** + * Normalizes a RDN by performing the following operations: + *
    + *
  • lowercase attribute names
  • + *
  • lowercase attribute values
  • + *
  • compress duplicate spaces in attribute values
  • + *
  • escape attribute value characters
  • + *
  • sort multi value RDNs by name
  • + *
+ *

+ * This API provides properties to control attribute name normalization, attribute value normalization and attribute + * value escaping in order to customize the behavior. Note that attribute value normalization occurs before escaping. + * + */ +public class DefaultRDnNormalizer implements RDnNormalizer { + + /** + * Function that lowercases the value. + */ + public static final Function LOWERCASE = new Function<>() { + @Override + public String apply(final String s) { + return LdapUtils.toLowerCase(s); + } + + @Override + public String toString() { + return "LOWERCASE"; + } + }; + + /** + * Function that removes duplicate spaces from the value. + */ + public static final Function COMPRESS = new Function<>() { + @Override + public String apply(final String s) { + return LdapUtils.compressSpace(s, false); + } + + @Override + public String toString() { + return "COMPRESS"; + } + }; + + /** + * Function that lowercases and removes duplicate spaces from the value. + */ + public static final Function LOWERCASE_COMPRESS = new Function<>() { + @Override + public String apply(final String s) { + return LdapUtils.toLowerCase(LdapUtils.compressSpace(s, false)); + } + + @Override + public String toString() { + return "LOWERCASE_COMPRESS"; + } + }; + + /** + * Attribute name function. + */ + private final Function attributeNameFunction; + + /** + * Attribute value function. + */ + private final Function attributeValueFunction; + + /** + * Attribute value escaper. + */ + private final AttributeValueEscaper attributeValueEscaper; + + + /** + * Creates a new default RDN normalizer. + */ + public DefaultRDnNormalizer() { + this(new DefaultAttributeValueEscaper(), LOWERCASE, LOWERCASE_COMPRESS); + } + + + /** + * Creates a new default RDN normalizer. + * + * @param escaper to escape attribute values + */ + public DefaultRDnNormalizer(final AttributeValueEscaper escaper) { + this(escaper, LOWERCASE, LOWERCASE_COMPRESS); + } + + + /** + * Creates a new default RDN normalizer. + * + * @param escaper to escape attribute values + * @param nameNormalizer to normalize attribute names + * @param valueNormalizer to normalize attribute values + */ + public DefaultRDnNormalizer( + final AttributeValueEscaper escaper, + final Function nameNormalizer, + final Function valueNormalizer) { + attributeValueEscaper = escaper; + attributeNameFunction = nameNormalizer; + attributeValueFunction = valueNormalizer; + } + + + /** + * Returns the value escaper. + * + * @return value escaper + */ + public AttributeValueEscaper getValueEscaper() { + return attributeValueEscaper; + } + + + /** + * Returns the attribute name function. + * + * @return function for attribute names + */ + public Function getNameFunction() { + return attributeNameFunction; + } + + + /** + * Returns the attribute value function. + * + * @return function for attribute values + */ + public Function getValueFunction() { + return attributeValueFunction; + } + + + @Override + public RDn normalize(final RDn rdn) { + final Set nameValues = rdn.getNameValues().stream() + .map( + nv -> new NameValue( + attributeNameFunction.apply(nv.getName()), + attributeValueEscaper.escape(attributeValueFunction.apply(nv.getStringValue())))) + .sorted(Comparator.comparing(NameValue::getName)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + return new RDn(nameValues); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "attributeNameFunction=" + attributeNameFunction + ", " + + "attributeValueFunction=" + attributeValueFunction + ", " + + "attributeValueEscaper=" + attributeValueEscaper; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/Dn.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/Dn.java new file mode 100644 index 0000000..32e1851 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/Dn.java @@ -0,0 +1,414 @@ + +package org.xbib.net.ldap.dn; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.xbib.net.ldap.LdapUtils; + +/** + * Distinguished name containing zero or more relative distinguished names. RDNs are ordered from left to right such + * that the left-most RDN is considered the first. For the DN 'cn=Jane Doe,ou=People,dc=xbib,dc=org', the first RDN + * is 'cn=Jane Doe'. + *

+ * See RFC 4514 for more details on the string representations of + * DNs. + * + */ +public class Dn { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 5003; + + /** + * RDN components. + */ + private final List rdnComponents = new ArrayList<>(); + + + /** + * Default constructor. + */ + public Dn() { + } + + + /** + * Creates a new DN with the supplied string. Uses a {@link DefaultDnParser} by default. + * + * @param dn to parse + */ + public Dn(final String dn) { + this(dn, new DefaultDnParser()); + } + + + /** + * Creates a new DN with the supplied string. + * + * @param dn to parse + * @param parser to parse dn + */ + public Dn(final String dn, final DnParser parser) { + rdnComponents.addAll(parser.parse(dn)); + } + + + /** + * Creates a new DN with the supplied RDNs. + * + * @param rdn to add + */ + public Dn(final RDn... rdn) { + this(Arrays.asList(rdn)); + } + + + /** + * Creates a new DN with the supplied RDNs. + * + * @param rdns to add + */ + public Dn(final List rdns) { + rdnComponents.addAll(rdns); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Dn.Builder builder() { + return new Dn.Builder(); + } + + /** + * Returns the first RDN in this DN. + * + * @return first RDN + */ + public RDn getRDn() { + if (rdnComponents.size() == 0) { + return null; + } + return rdnComponents.get(0); + } + + /** + * Returns the RDNs in this DN. + * + * @return RDNs + */ + public List getRDns() { + return rdnComponents; + } + + /** + * Adds all the RDNs in the supplied DN to the end of this DN. + * + * @param dn to add to this DN + */ + public void add(final Dn dn) { + rdnComponents.addAll(dn.getRDns()); + } + + /** + * Adds the supplied RDN to the end of this DN. + * + * @param rdn to add to this DN + */ + public void add(final RDn rdn) { + rdnComponents.add(rdn); + } + + /** + * Adds the supplied RDN at the supplied index. + * + * @param index to add the RDN at + * @param rdn to add to this DN + */ + public void add(final int index, final RDn rdn) { + rdnComponents.add(index, rdn); + } + + /** + * Returns a new DN containing all the RDN components from the supplied index. + * + * @param index of RDNs to include + * @return DN with sub-components of this DN + */ + public Dn subDn(final int index) { + return subDn(index, rdnComponents.size()); + } + + /** + * Returns a new DN containing all the RDN components between the supplied indexes. + * + * @param beginIndex first RDN to include (inclusive) + * @param endIndex last RDN to include (exclusive) + * @return DN with sub-components of this DN or null if beginIndex > endIndex + */ + public Dn subDn(final int beginIndex, final int endIndex) { + if (beginIndex > endIndex) { + return null; + } + return new Dn(IntStream + .range(0, rdnComponents.size()) + .filter(i -> i >= beginIndex && i < endIndex) + .mapToObj(rdnComponents::get) + .collect(Collectors.toList())); + } + + /** + * Convenience method to retrieve the parent DN. Invokes {@link #subDn(int)} with a parameter of 1. + * + * @return DN containing all sub-components of this DN except the first or null if this DN has no components + */ + public Dn getParent() { + return subDn(1); + } + + /** + * Returns all the RDN names. + * + * @return all RDN names + */ + public Collection getNames() { + return rdnComponents.stream() + .flatMap(rdn -> rdn.getNames().stream()) + .collect(Collectors.toList()); + } + + /** + * Returns the RDN values with the supplied name. If the RDN is multi-value the first value is used. + * + * @param name of the RDN + * @return RDN values for the supplied name + */ + public Collection getValues(final String name) { + return rdnComponents.stream() + .filter(rdn -> rdn.getNameValue().hasName(name)) + .map(rdn -> rdn.getNameValue().getStringValue()) + .collect(Collectors.toList()); + } + + /** + * Returns the first RDN value with the supplied name. If the RDN is multi-value the first value is used. + * + * @param name of the RDN + * @return RDN value + */ + public String getValue(final String name) { + return getValues(name).stream().findFirst().orElse(null); + } + + /** + * Returns the number of RDNs in this DN. + * + * @return number of RDNs + */ + public int size() { + return rdnComponents.size(); + } + + /** + * Returns whether this DN contains any RDN components. + * + * @return whether this DN contains any RDN components + */ + public boolean isEmpty() { + return rdnComponents.isEmpty(); + } + + /** + * Returns whether the normalized format of the supplied DN equals the normalized format of this DN. See {@link + * DefaultRDnNormalizer}. + * + * @param dn to compare + * @return whether the supplied DN is the same as this DN + */ + public boolean isSame(final Dn dn) { + return isSame(dn, new DefaultRDnNormalizer()); + } + + /** + * Returns whether the normalized format of the supplied DN equals the normalized format of this DN. + * + * @param normalizer to use for comparison + * @param dn to compare + * @return whether the supplied DN is the same as this DN + */ + public boolean isSame(final Dn dn, final RDnNormalizer normalizer) { + return format(normalizer).equals(dn.format(normalizer)); + } + + /** + * Returns whether the supplied DN is an ancestor. See {@link #isSame(Dn)}. + * + * @param dn to determine ancestry of + * @return whether the supplied DN is an ancestor + */ + public boolean isAncestor(final Dn dn) { + return isAncestor(dn, new DefaultRDnNormalizer()); + } + + /** + * Returns whether the supplied DN is an ancestor. See {@link #isSame(Dn, RDnNormalizer)}. + * + * @param dn to determine ancestry of + * @param normalizer to format DN for comparison + * @return whether the supplied DN is an ancestor + */ + public boolean isAncestor(final Dn dn, final RDnNormalizer normalizer) { + // all DNs are ancestors from the root DN + if (isEmpty() && !dn.isEmpty()) { + return true; + } + + // greater than or equal number of RDNs, cannot be an ancestor + if (size() >= dn.size()) { + return false; + } + + int index = size() - 1; + int dnIndex = dn.size() - 1; + boolean ancestor = true; + while (index >= 0) { + if (!getRDns().get(index--).isSame(dn.getRDns().get(dnIndex--), normalizer)) { + ancestor = false; + break; + } + } + return ancestor; + } + + /** + * Returns whether the supplied DN is a descendant. See {@link #isSame(Dn)}. + * + * @param dn to determine descendancy of + * @return whether the supplied DN is a descendant + */ + public boolean isDescendant(final Dn dn) { + return isDescendant(dn, new DefaultRDnNormalizer()); + } + + /** + * Returns whether the supplied DN is a descendant. See {@link #isSame(Dn, RDnNormalizer)}. + * + * @param dn to determine descendancy of + * @param normalizer to format DN for comparison + * @return whether the supplied DN is a descendant + */ + public boolean isDescendant(final Dn dn, final RDnNormalizer normalizer) { + return dn.isAncestor(this, normalizer); + } + + /** + * Produces a string representation of this DN. Uses a {@link DefaultRDnNormalizer} by default. + * + * @return DN string + */ + public String format() { + return format(new DefaultRDnNormalizer()); + } + + /** + * Produces a string representation of this DN. + * + * @param normalizer to apply to the RDN components or null for no formatting + * @return DN string + */ + public String format(final RDnNormalizer normalizer) { + return format(normalizer, ',', false); + } + + /** + * Produces a string representation of this DN. + * + * @param normalizer to apply to the RDN components or null for no formatting + * @param delimiter to separate each RDN component + * @param reverse whether to reverse the order of RDN components for formatting. + * i.e. process components from right to left + * @return DN string + */ + public String format(final RDnNormalizer normalizer, final char delimiter, final boolean reverse) { + if (rdnComponents.size() == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + if (reverse) { + for (int i = rdnComponents.size() - 1; i >= 0; i--) { + sb.append(rdnComponents.get(i).format(normalizer)).append(delimiter); + } + } else { + for (RDn rdnComponent : rdnComponents) { + sb.append(rdnComponent.format(normalizer)).append(delimiter); + } + } + if (sb.length() > 0 && sb.charAt(sb.length() - 1) == delimiter) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof Dn v) { + return LdapUtils.areEqual(rdnComponents, v.rdnComponents); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, rdnComponents); + } + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + "rdnComponents=" + rdnComponents; + } + + // CheckStyle:OFF + public static class Builder { + + + private final Dn object = new Dn(); + + + protected Builder() { + } + + + public Dn.Builder add(final String dn) { + object.add(new Dn(dn)); + return this; + } + + + public Dn.Builder add(final Dn dn) { + object.add(dn); + return this; + } + + + public Dn.Builder add(final RDn rdn) { + object.add(rdn); + return this; + } + + + public Dn build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/DnParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/DnParser.java new file mode 100644 index 0000000..98df9ce --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/DnParser.java @@ -0,0 +1,19 @@ + +package org.xbib.net.ldap.dn; + +import java.util.List; + +/** + * Interface for parsing DNs. + * + */ +public interface DnParser { + + /** + * Parses the supplied DN into a list of RDNs. + * + * @param dn to parse + * @return list of RDNs + */ + List parse(String dn); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/MinimalAttributeValueEscaper.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/MinimalAttributeValueEscaper.java new file mode 100644 index 0000000..ce83841 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/MinimalAttributeValueEscaper.java @@ -0,0 +1,23 @@ + +package org.xbib.net.ldap.dn; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Escapes an attribute value per RFC 4514 section 2.4. ASCII control characters and all non-ASCII data is not encoded. + * + */ +public class MinimalAttributeValueEscaper extends AbstractAttributeValueEscaper { + + + @Override + protected void processAscii(final StringBuilder sb, final char ch) { + sb.append(ch); + } + + + @Override + protected void processNonAscii(final StringBuilder sb, final byte... bytes) { + sb.append(LdapUtils.utf8Encode(bytes)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/NameValue.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/NameValue.java new file mode 100644 index 0000000..05e805b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/NameValue.java @@ -0,0 +1,128 @@ + +package org.xbib.net.ldap.dn; + +import java.nio.ByteBuffer; +import java.util.function.Function; +import org.xbib.net.ldap.LdapUtils; + +/** + * Container for a RDN name value pair. + * + */ +public class NameValue { + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 5011; + + /** + * Attribute name. + */ + private final String attributeName; + + /** + * Attribute value. + */ + private final ByteBuffer attributeValue; + + + /** + * Creates a new name value. + * + * @param name of the attribute + * @param value of the attribute + */ + public NameValue(final String name, final String value) { + this(name, LdapUtils.utf8Encode(value)); + } + + + /** + * Creates a new name value. + * + * @param name of the attribute + * @param value of the attribute + */ + public NameValue(final String name, final byte[] value) { + attributeName = name; + attributeValue = value != null ? ByteBuffer.wrap(value) : null; + } + + + /** + * Returns the attribute name. + * + * @return attribute name + */ + public String getName() { + return attributeName; + } + + + public byte[] getBinaryValue() { + return attributeValue != null ? attributeValue.array() : null; + } + + + public String getStringValue() { + return attributeValue != null ? LdapUtils.utf8Encode(attributeValue.array()) : null; + } + + + public T getValue(final Function func) { + return attributeValue != null ? func.apply(attributeValue.array()) : null; + } + + + /** + * Returns whether the attribute name matches the supplied name. + * + * @param name to match + * @return whether name matches the attribute name + */ + public boolean hasName(final String name) { + return attributeName.equalsIgnoreCase(name); + } + + + /** + * Returns a string representation of this name value, of the form 'name=value'. + * + * @return string form of the name value pair + */ + public String format() { + return attributeName + "=" + LdapUtils.utf8Encode(attributeValue != null ? attributeValue.array() : null); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof NameValue v) { + return LdapUtils.areEqual(LdapUtils.toLowerCase(attributeName), LdapUtils.toLowerCase(v.attributeName)) && + LdapUtils.areEqual(attributeValue, v.attributeValue); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + LdapUtils.toLowerCase(attributeName), + attributeValue); + } + + + @Override + public String toString() { + return getClass().getName() + + "@" + hashCode() + "::" + + "name=" + attributeName + ", " + + "value=" + getStringValue(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/RDn.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/RDn.java new file mode 100644 index 0000000..ab279ba --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/RDn.java @@ -0,0 +1,262 @@ + +package org.xbib.net.ldap.dn; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.xbib.net.ldap.LdapUtils; + +/** + * Relative distinguished name containing one or more name value pairs. Name value pairs are ordered from left to right + * such that the left-most pair is considered the first. For the RDN 'cn=Jane Doe+mail=jdoe@example.com', the first name + * value pair is 'cn=Jane Doe'. + *

+ * See RFC 4514 for more details on the string representations of + * RDNs. + * + */ +public class RDn { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 5009; + + /** + * Name value pairs. + */ + private final Set nameValues; + + + /** + * Creates a new RDN with the supplied string. + * + * @param rdn to parse + * @throws IllegalArgumentException if rdn contains multiple RDNs or no RDNs + */ + public RDn(final String rdn) { + this(rdn, new DefaultDnParser()); + } + + + /** + * Creates a new RDN with the supplied string. + * + * @param rdn to parse + * @param parser to parse dn + * @throws IllegalArgumentException if rdn contains multiple RDNs or no RDNS + */ + public RDn(final String rdn, final DnParser parser) { + final List rdns = parser.parse(rdn); + if (rdns.isEmpty()) { + throw new IllegalArgumentException("Invalid RDN: no RDNs found in " + rdn); + } + if (rdns.size() > 1) { + throw new IllegalArgumentException("Invalid RDN: multiple RDNs found in " + rdn); + } + nameValues = Collections.unmodifiableSet(rdns.get(0).getNameValues()); + } + + + /** + * Creates a new RDN with the supplied name value pairs. + * + * @param value to add + */ + public RDn(final NameValue... value) { + nameValues = Stream.of(value).filter(Objects::nonNull).collect( + Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); + } + + + /** + * Creates a new RDN with the supplied name value pairs. + * + * @param values to add + */ + public RDn(final Collection values) { + nameValues = values.stream().filter(Objects::nonNull).collect( + Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); + } + + + /** + * Creates a new RDN with a single name value pair. + * + * @param attributeName to add + * @param attributeValue to add + */ + public RDn(final String attributeName, final String attributeValue) { + nameValues = Set.of(new NameValue(attributeName, attributeValue)); + } + + + /** + * Returns the first name value pair in this RDN. + * + * @return name value pair + */ + public NameValue getNameValue() { + if (nameValues.isEmpty()) { + return null; + } + return nameValues.iterator().next(); + } + + + /** + * Returns all the name value pairs in this RDN. + * + * @return name value paris + */ + public Set getNameValues() { + return nameValues; + } + + + /** + * Returns all the names in this RDN. + * + * @return all names + */ + public List getNames() { + return nameValues.stream().map(NameValue::getName).collect(Collectors.toUnmodifiableList()); + } + + + /** + * Returns the name values that match the supplied name. + * + * @param name to match + * @return name values + */ + public Set getNameValues(final String name) { + return nameValues.stream().filter(nv -> nv.hasName(name)) + .collect(Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); + } + + + /** + * Returns a single name value that matches the supplied name. See {@link #getNameValues(String)}. + * + * @param name to match + * @return name value + */ + public NameValue getNameValue(final String name) { + return getNameValues(name).stream().findFirst().orElse(null); + } + + + /** + * Returns the number of name value pairs in this RDN. + * + * @return RDN size + */ + public int size() { + return nameValues.size(); + } + + + /** + * Returns whether this RDN contains any name values. + * + * @return whether this RDN contains any name values + */ + public boolean isEmpty() { + return nameValues.isEmpty(); + } + + + /** + * Returns whether the normalized format of the supplied RDN equals the normalized format of this RDN. See {@link + * DefaultRDnNormalizer}. + * + * @param rdn to compare + * @return whether the supplied RDN is the same as this RDN + */ + public boolean isSame(final RDn rdn) { + return isSame(rdn, new DefaultRDnNormalizer()); + } + + + /** + * Returns whether the normalized format of the supplied RDN equals the normalized format of this RDN. + * + * @param normalizer to use for comparison + * @param rdn to compare + * @return whether the supplied RDN is the same as this RDN + */ + public boolean isSame(final RDn rdn, final RDnNormalizer normalizer) { + return format(normalizer).equals(rdn.format(normalizer)); + } + + + /** + * Returns a string representation of this RDN. Uses a {@link DefaultRDnNormalizer} by default. + * + * @return string form of the RDN + */ + public String format() { + return format(new DefaultRDnNormalizer()); + } + + + /** + * Returns a string representation of this RDN, joining each name value pair with '+'. + * + * @param normalizer to apply to the RDN components or null for no formatting + * @return string form of the RDN + */ + public String format(final RDnNormalizer normalizer) { + final String formatted; + switch (nameValues.size()) { + case 0: + return ""; + case 1: + if (normalizer != null) { + formatted = normalizer.normalize(this).getNameValues().iterator().next().format(); + } else { + formatted = nameValues.iterator().next().format(); + } + break; + default: + if (normalizer != null) { + formatted = normalizer.normalize(this).getNameValues().stream() + .map(NameValue::format) + .collect(Collectors.joining("+")); + } else { + formatted = nameValues.stream().map(NameValue::format).collect(Collectors.joining("+")); + } + } + return formatted; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof RDn v) { + return LdapUtils.areEqual(nameValues, v.nameValues); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, nameValues); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + "nameValues=" + nameValues; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dn/RDnNormalizer.java b/net-ldap/src/main/java/org/xbib/net/ldap/dn/RDnNormalizer.java new file mode 100644 index 0000000..c059a9b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dn/RDnNormalizer.java @@ -0,0 +1,18 @@ + +package org.xbib.net.ldap.dn; + +/** + * Interface for normalizing RDNs. + * + */ +public interface RDnNormalizer { + + + /** + * Normalize the name value pairs in the supplied RDN. + * + * @param rdn to normalize + * @return new normalized RDN + */ + RDn normalize(RDn rdn); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dns/AbstractDNSResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/dns/AbstractDNSResolver.java new file mode 100644 index 0000000..f2a37cb --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dns/AbstractDNSResolver.java @@ -0,0 +1,115 @@ + +package org.xbib.net.ldap.dns; + +import java.util.HashSet; +import java.util.Set; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; + +/** + * Base class for all DNS resolvers. + * + * @param Type of record to resolve. + */ +public abstract class AbstractDNSResolver implements DNSResolver { + + /** + * Factory to create DNS connections. + */ + private final DNSContextFactory contextFactory; + + + /** + * Creates a new abstract DNS resolver. + * + * @param factory DNS context factory + */ + public AbstractDNSResolver(final DNSContextFactory factory) { + contextFactory = factory; + } + + + @Override + public Set resolve(final String name) { + DirContext ctx = null; + try { + ctx = contextFactory.create(); + final Set records = new HashSet<>(); + for (String key : getAttributes()) { + resolveOne(ctx, name, key, records); + } + final Set results = processRecords(records); + return results; + } catch (NamingException e) { + throw new RuntimeException("DNS lookup failed for " + name, e); + } finally { + if (ctx != null) { + try { + ctx.close(); + } catch (NamingException e) { + // + } + } + } + } + + + /** + * Get the types of records to query for, e.g. {"A", "AAAA"}. + * + * @return Array of JNDI attribute names. + */ + protected abstract String[] getAttributes(); + + + /** + * Process a set of DNS records. + * + * @param records Set of raw DNS records returned from a name query. + * @return Set of converted/processed records. + */ + protected abstract Set processRecords(Set records); + + + /** + * Query for a single kind of DNS record. + * + * @param ctx Directory context. + * @param name Name to query for. + * @param attrId DNS record type, e.g. A. + * @param records Set of records to append results to. + * @throws NamingException on DNS lookup failure. + */ + private void resolveOne(final DirContext ctx, final String name, final String attrId, final Set records) + throws NamingException { + NamingEnumeration en = null; + try { + final Attributes attrs = ctx.getAttributes(name, new String[]{attrId}); + if (attrs != null) { + final Attribute attr = attrs.get(attrId); + if (attr != null) { + en = attr.getAll(); + while (en.hasMore()) { + records.add((String) en.next()); + } + } + } + } catch (final NameNotFoundException e) { + // + } finally { + if (en != null) { + en.close(); + } + } + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + "contextFactory=" + contextFactory; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSContextFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSContextFactory.java new file mode 100644 index 0000000..28b86a2 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSContextFactory.java @@ -0,0 +1,21 @@ + +package org.xbib.net.ldap.dns; + +import javax.naming.NamingException; +import javax.naming.directory.DirContext; + +/** + * Interface to provide {@link DirContext} implementations to be used for DNS queries. + * + */ +public interface DNSContextFactory { + + + /** + * Creates a new JNDI context. + * + * @return JNDI context + * @throws NamingException if an error occurs creating the context + */ + DirContext create() throws NamingException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSDomainFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSDomainFunction.java new file mode 100644 index 0000000..2a6c480 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSDomainFunction.java @@ -0,0 +1,37 @@ + +package org.xbib.net.ldap.dns; + +import java.util.function.Function; +import org.xbib.net.ldap.dn.Dn; +import org.xbib.net.ldap.dn.RDn; + +/** + * Maps a DN to a domain name using the process described in + * draft-ietf-ldapext-locate + * + */ +public class DNSDomainFunction implements Function { + + + @Override + public String apply(final Dn dn) { + final StringBuilder domain = new StringBuilder(); + for (RDn rdn : dn.getRDns()) { + if (rdn.size() == 1 && rdn.getNameValue().hasName("DC")) { + final String attrValue = rdn.getNameValue().getStringValue(); + // ignore empty DC components or any component containing a single dot + if (attrValue != null && !attrValue.isEmpty() && !".".equals(attrValue)) { + if (domain.length() > 0) { + domain.append('.'); + } + domain.append(attrValue); + } + } else if (domain.length() > 0) { + // clear the domain if anything other than a single value, DC component is encountered + // this enforces that the DC components used must be at the end of the RDN sequence + domain.setLength(0); + } + } + return domain.toString(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSResolver.java new file mode 100644 index 0000000..930c32e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dns/DNSResolver.java @@ -0,0 +1,21 @@ + +package org.xbib.net.ldap.dns; + +import java.util.Set; + +/** + * Strategy pattern interface for resolving DNS records. + * + * @param Type of record to resolve. + */ +public interface DNSResolver { + + + /** + * Resolve a set of DNS records of some type for the given name. + * + * @param name Name for which to resolve DNS records. + * @return Set of records of type T bound to the given name. + */ + Set resolve(String name); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dns/DefaultDNSContextFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/dns/DefaultDNSContextFactory.java new file mode 100644 index 0000000..25872fd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dns/DefaultDNSContextFactory.java @@ -0,0 +1,72 @@ + +package org.xbib.net.ldap.dns; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; + +/** + * Provides the default implementation of the JNDI context factory for DNS queries. + * + */ +public class DefaultDNSContextFactory implements DNSContextFactory { + + /** + * JNDI context factory for DNS. + */ + public static final String DNS_CONTEXT_FACTORY = "com.sun.jndi.dns.DnsContextFactory"; + + /** + * Default provider URL for DNS, determines DNS from the underlying OS. Value is {@value}. + */ + public static final String DEFAULT_DNS_PROVIDER_URL = "dns:"; + + /** + * DNS name servers in order of preference. + */ + private final List nameservers; + + + /** + * Creates a new instance that resolves DNS names using the given name servers. + * + * @param servers name servers in order of preference. + */ + public DefaultDNSContextFactory(final String... servers) { + if (servers != null && servers.length > 0) { + nameservers = Arrays.asList(servers); + } else { + nameservers = Collections.emptyList(); + } + } + + + @Override + public InitialDirContext create() + throws NamingException { + final Map env = new HashMap<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, DNS_CONTEXT_FACTORY); + if (nameservers.isEmpty()) { + env.put(Context.PROVIDER_URL, DEFAULT_DNS_PROVIDER_URL); + } else { + env.put( + Context.PROVIDER_URL, + String.join(" ", nameservers)); + } + // CheckStyle:IllegalType OFF + return new InitialDirContext(new Hashtable<>(env)); + // CheckStyle:IllegalType ON + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + "nameservers=" + nameservers; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dns/SRVDNSResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/dns/SRVDNSResolver.java new file mode 100644 index 0000000..4279362 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dns/SRVDNSResolver.java @@ -0,0 +1,155 @@ + +package org.xbib.net.ldap.dns; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Queries for DNS A records for a given host name. + * + */ +public class SRVDNSResolver extends AbstractDNSResolver { + + /** + * Attributes (DNS record types) to query for. + */ + private static final String[] ATTRIBUTES = {"SRV",}; + + /** + * Default DNS record name. + */ + private static final String DEFAULT_RECORD_NAME = "_ldap._tcp"; + + /** + * Connect to LDAP using LDAPS. + */ + private final boolean useSSL; + + + /** + * Default constructor. + */ + public SRVDNSResolver() { + this(new DefaultDNSContextFactory()); + } + + + /** + * Creates a new DNS address resolver. + * + * @param factory JNDI dir context factory + */ + public SRVDNSResolver(final DNSContextFactory factory) { + this(factory, false); + } + + + /** + * Creates a new DNS address resolver. + * + * @param factory JNDI dir context factory + * @param ssl whether SRV records should produce LDAPS URLs + */ + public SRVDNSResolver(final DNSContextFactory factory, final boolean ssl) { + super(factory); + useSSL = ssl; + } + + + @Override + public Set resolve(final String name) { + if (name == null) { + return super.resolve(DEFAULT_RECORD_NAME); + } + return super.resolve(name); + } + + + @Override + protected String[] getAttributes() { + return ATTRIBUTES; + } + + + @Override + protected Set processRecords(final Set records) { + final Set srvRecords = new HashSet<>(records.size()); + for (String record : records) { + srvRecords.add(new SRVRecord(record, useSSL)); + } + return sortSrvRecords(srvRecords); + } + + + /** + * Sorts the supplied SRV records according to RFC 2782. Records with the lowest priority are first. Records with the + * same priority are arranged by weight with higher weights having a greater chance to be ordered first. + * + * @param records to sort + * @return sorted records + */ + protected Set sortSrvRecords(final Set records) { + // group records and order them by priority + final Map> priorityRecords = new TreeMap<>(); + for (SRVRecord record : records) { + final Set priority; + if (!priorityRecords.containsKey(record.getPriority())) { + priority = new LinkedHashSet<>(); + priorityRecords.put(record.getPriority(), priority); + } else { + priority = priorityRecords.get(record.getPriority()); + } + priority.add(record); + } + + // order records by priority then by weight + // unweighted records are ordered last + final Set sortedRecords = new LinkedHashSet<>(); + for (Map.Entry> entry : priorityRecords.entrySet()) { + final Map weighted = new HashMap<>(); + final Set unweighted = new LinkedHashSet<>(); + long totalWeight = 0; + for (SRVRecord record : entry.getValue()) { + if (record.getWeight() == 0) { + unweighted.add(record); + } else { + totalWeight += record.getWeight(); + weighted.put(totalWeight, record); + } + } + + while (!weighted.isEmpty()) { + SRVRecord record = null; + final Iterator i = weighted.keySet().iterator(); + final long random = ThreadLocalRandom.current().nextLong(totalWeight + 1); + while (i.hasNext()) { + final Long weight = i.next(); + if (weight >= random) { + record = weighted.get(weight); + totalWeight -= record.getWeight(); + i.remove(); + break; + } + } + sortedRecords.add(record); + } + if (!unweighted.isEmpty()) { + sortedRecords.addAll(unweighted); + } + } + + return sortedRecords; + } + + + @Override + public String toString() { + return "[" + super.toString() + ", " + "useSSL=" + useSSL + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/dns/SRVRecord.java b/net-ldap/src/main/java/org/xbib/net/ldap/dns/SRVRecord.java new file mode 100644 index 0000000..5344940 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/dns/SRVRecord.java @@ -0,0 +1,146 @@ + +package org.xbib.net.ldap.dns; + +import org.xbib.net.ldap.LdapURL; +import org.xbib.net.ldap.LdapUtils; + +/** + * Class to contain the properties of a DNS SRV record. + * + */ +public class SRVRecord { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1201; + + /** + * SRV priority. + */ + private final long priority; + + /** + * SRV weight. + */ + private final long weight; + + /** + * SRV port. + */ + private final int port; + + /** + * SRV target. + */ + private final String target; + + /** + * whether to use LDAPS. + */ + private final boolean useSSL; + + + /** + * Creates a new SRV record. + * + * @param record from DNS + * @param ssl whether to use LDAPS + */ + public SRVRecord(final String record, final boolean ssl) { + final String[] parts = record.split(" "); + int i = 0; + priority = Long.parseLong(parts[i++]); + weight = Long.parseLong(parts[i++]); + port = Integer.parseInt(parts[i++]); + target = parts[i].endsWith(".") ? parts[i].substring(0, parts[i].length() - 1) : parts[i]; + useSSL = ssl; + } + + + /** + * Returns the priority. + * + * @return priority + */ + public long getPriority() { + return priority; + } + + + /** + * Returns the weight. + * + * @return weight + */ + public long getWeight() { + return weight; + } + + + /** + * Returns the port. + * + * @return port + */ + public int getPort() { + return port; + } + + + /** + * Returns the target. + * + * @return target + */ + public String getTarget() { + return target; + } + + + /** + * Returns the target properly formatted as an LDAP URL. + * + * @return LDAP URL + */ + public LdapURL getLdapURL() { + if (useSSL) { + return new LdapURL("ldaps://" + target + ":" + port); + } + return new LdapURL("ldap://" + target + ":" + port); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SRVRecord v) { + return LdapUtils.areEqual(priority, v.priority) && + LdapUtils.areEqual(weight, v.weight) && + LdapUtils.areEqual(port, v.port) && + LdapUtils.areEqual(target, v.target) && + LdapUtils.areEqual(useSSL, v.useSSL); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, priority, weight, port, target, useSSL); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "priority=" + priority + ", " + + "weight=" + weight + ", " + + "port=" + port + ", " + + "target=" + target + ", " + + "useSSL=" + useSSL + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ext/MergeOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/ext/MergeOperation.java new file mode 100644 index 0000000..cb292f5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ext/MergeOperation.java @@ -0,0 +1,306 @@ + +package org.xbib.net.ldap.ext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.xbib.net.ldap.AddOperation; +import org.xbib.net.ldap.AddRequest; +import org.xbib.net.ldap.AttributeModification; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.DeleteOperation; +import org.xbib.net.ldap.DeleteRequest; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ModifyOperation; +import org.xbib.net.ldap.ModifyRequest; +import org.xbib.net.ldap.OperationHandle; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.handler.ResultPredicate; + +/** + * The merge operation performs the LDAP operations necessary to synchronize the data in an {@link LdapEntry} with its + * corresponding entry in the LDAP. The following logic is executed: + * + *

    + *
  • if the entry does not exist in the LDAP, execute an add
  • + *
  • if the request is for a delete, execute a delete
  • + *
  • if the entry exists in the LDAP, execute a modify
  • + *
+ * + *

{@link LdapEntry#computeModifications(LdapEntry, LdapEntry)} is used to determine the list of attribute + * modifications that are necessary to perform the merge. Either {@link MergeRequest#getIncludeAttributes()} or {@link + * MergeRequest#getExcludeAttributes()} will be used, but not both.

+ * + */ +public class MergeOperation { + + /** + * Connection factory. + */ + private ConnectionFactory connectionFactory; + + /** + * Search operation used to find the entry. + */ + private SearchOperation searchOperation; + + /** + * Add operation used to add a new entry. + */ + private AddOperation addOperation; + + /** + * Modify operation used to update an entry. + */ + private ModifyOperation modifyOperation; + + /** + * Delete operation used to remove an entry. + */ + private DeleteOperation deleteOperation; + + /** + * Function to test results. + */ + private ResultPredicate throwCondition; + + + /** + * Default constructor. + */ + public MergeOperation() { + } + + + /** + * Creates a new merge operation. + * + * @param factory connection factory + */ + public MergeOperation(final ConnectionFactory factory) { + connectionFactory = factory; + } + + + public ConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + + public void setConnectionFactory(final ConnectionFactory factory) { + connectionFactory = factory; + } + + + public SearchOperation getSearchOperation() { + return searchOperation; + } + + + public void setSearchOperation(final SearchOperation operation) { + searchOperation = operation; + } + + + public AddOperation getAddOperation() { + return addOperation; + } + + + public void setAddOperation(final AddOperation operation) { + addOperation = operation; + } + + + public ModifyOperation getModifyOperation() { + return modifyOperation; + } + + + public void setModifyOperation(final ModifyOperation operation) { + modifyOperation = operation; + } + + + public DeleteOperation getDeleteOperation() { + return deleteOperation; + } + + + public void setDeleteOperation(final DeleteOperation operation) { + deleteOperation = operation; + } + + + public ResultPredicate getThrowCondition() { + return throwCondition; + } + + + public void setThrowCondition(final ResultPredicate function) { + throwCondition = function; + } + + + /** + * Executes a merge request. See {@link OperationHandle#execute()}. + * + * @param request merge request + * @return merge result + * @throws LdapException if the connection cannot be opened + */ + public Result execute(final MergeRequest request) + throws LdapException { + final LdapEntry sourceEntry = request.getEntry(); + + // search for existing entry + final SearchOperation operation = searchOperation != null ? + SearchOperation.copy(searchOperation) : new SearchOperation(); + operation.setConnectionFactory(connectionFactory); + operation.setThrowCondition( + r -> r.getResultCode() != ResultCode.SUCCESS && r.getResultCode() != ResultCode.NO_SUCH_OBJECT); + final SearchResponse searchResult = operation.execute( + SearchRequest.objectScopeSearchRequest(sourceEntry.getDn(), request.getSearchAttributes())); + + final Result result; + if (searchResult.entrySize() == 0) { + if (request.getDeleteEntry()) { + result = null; + } else { + // entry does not exist, add it + result = add(request, sourceEntry); + if (throwCondition != null) { + throwCondition.testAndThrow(result); + } + } + } else if (request.getDeleteEntry()) { + // delete entry + result = delete(request, sourceEntry); + if (throwCondition != null) { + throwCondition.testAndThrow(result); + } + } else { + // entry exists, merge attributes + result = modify(request, sourceEntry, searchResult.getEntry()); + if (throwCondition != null) { + throwCondition.testAndThrow(result); + } + } + return result; + } + + + /** + * Retrieves the attribute modifications from {@link LdapEntry#computeModifications(LdapEntry, LdapEntry)} and + * executes a {@link ModifyOperation} with those results. If no modifications are necessary, no operation is + * performed. + * + * @param request merge request + * @param source ldap entry to merge into the LDAP + * @param target ldap entry that exists in the LDAP + * @return response of the modify operation or a null response if no operation is performed. If batching is + * enabled in the request, returns the response of the last operation performed + * @throws LdapException if an error occurs executing the modify operation + */ + protected Result modify( + final MergeRequest request, + final LdapEntry source, + final LdapEntry target) + throws LdapException { + final AttributeModification[] modifications = + LdapEntry.computeModifications(source, target, request.isUseReplace()); + if (modifications != null && modifications.length > 0) { + // create a list of lists to support attribute modification processors that perform batching + // by default the structure is 1xn, which may be modified by configured processors + final List> resultModifications = new ArrayList<>(1); + resultModifications.add(new ArrayList<>(modifications.length)); + final String[] includeAttrs = request.getIncludeAttributes(); + final String[] excludeAttrs = request.getExcludeAttributes(); + if (includeAttrs != null && includeAttrs.length > 0) { + final List l = Arrays.asList(includeAttrs); + for (AttributeModification am : modifications) { + if (l.contains(am.getAttribute().getName())) { + resultModifications.get(0).add(am); + } + } + } else if (excludeAttrs != null && excludeAttrs.length > 0) { + final List l = Arrays.asList(excludeAttrs); + for (AttributeModification am : modifications) { + if (!l.contains(am.getAttribute().getName())) { + resultModifications.get(0).add(am); + } + } + } else { + Collections.addAll(resultModifications.get(0), modifications); + } + if (!resultModifications.get(0).isEmpty()) { + // post process attribute modifications + List> processedModifications = resultModifications; + if (request.getAttributeModificationsHandlers() != null) { + for (MergeRequest.AttributeModificationsHandler processor : request.getAttributeModificationsHandlers()) { + processedModifications = processor.apply(processedModifications); + } + } + Result result = null; + for (List batch : processedModifications) { + final ModifyOperation operation = modifyOperation != null ? + ModifyOperation.copy(modifyOperation) : new ModifyOperation(); + operation.setConnectionFactory(connectionFactory); + result = operation.execute( + ModifyRequest.builder() + .dn(target.getDn()) + .modifications(batch.toArray(AttributeModification[]::new)) + .build()); + } + return result; + } + } + return null; + } + + + /** + * Executes an {@link AddOperation} for the supplied entry. + * + * @param request merge request + * @param entry to add to the LDAP + * @return response of the add operation + * @throws LdapException if an error occurs executing the add operation + */ + protected Result add(final MergeRequest request, final LdapEntry entry) + throws LdapException { + final AddOperation operation = addOperation != null ? + AddOperation.copy(addOperation) : new AddOperation(); + operation.setConnectionFactory(connectionFactory); + final Result result = operation.execute( + AddRequest.builder() + .dn(entry.getDn()) + .attributes(entry.getAttributes()) + .build()); + return result; + } + + + /** + * Executes a {@link DeleteOperation} for the supplied entry. + * + * @param request merge request + * @param entry to delete from the LDAP + * @return response of the delete operation + * @throws LdapException if an error occurs executing the deleting operation + */ + protected Result delete(final MergeRequest request, final LdapEntry entry) + throws LdapException { + final DeleteOperation operation = deleteOperation != null ? + DeleteOperation.copy(deleteOperation) : new DeleteOperation(); + operation.setConnectionFactory(connectionFactory); + final Result result = operation.execute(new DeleteRequest(entry.getDn())); + return result; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ext/MergeRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/ext/MergeRequest.java new file mode 100644 index 0000000..2b59a4e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ext/MergeRequest.java @@ -0,0 +1,332 @@ + +package org.xbib.net.ldap.ext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import org.xbib.net.ldap.AttributeModification; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; + +/** + * Contains the data required to perform a merge operation. + * + */ +public class MergeRequest { + + /** + * Ldap entry to merge. + */ + private LdapEntry ldapEntry; + + /** + * Whether to delete the entry. + */ + private boolean deleteEntry; + + /** + * Attribute names to include in the search. + */ + private String[] searchAttrs; + + /** + * Attribute names to include when performing a merge. + */ + private String[] includeAttrs; + + /** + * Attribute names to exclude when performing a merge. + */ + private String[] excludeAttrs; + + /** + * Whether to use replace or add/delete for attribute modifications. + */ + private boolean useReplace = true; + + /** + * Handler for attribute modifications. + */ + private AttributeModificationsHandler[] attributeModificationsHandlers; + + + /** + * Default constructor. + */ + public MergeRequest() { + } + + + /** + * Creates a new merge request. + * + * @param entry to merge into the LDAP + */ + public MergeRequest(final LdapEntry entry) { + setEntry(entry); + } + + + /** + * Creates a new merge request. + * + * @param entry to merge into the LDAP + * @param delete whether the supplied entry should be deleted + */ + public MergeRequest(final LdapEntry entry, final boolean delete) { + setEntry(entry); + setDeleteEntry(delete); + } + + /** + * Divides the supplied list into sub lists by the supplied divisor and passes each sub list to the consumer. + * + * @param type of list element + * @param list to divide + * @param divisor to divide list by + * @param consumer to process each sub list + */ + private static void divideList(final List list, final int divisor, final Consumer> consumer) { + for (int i = 0; i < list.size() / divisor; i++) { + final int start = i * divisor; + final int end = (i + 1) * divisor; + consumer.accept(list.subList(start, end > list.size() ? list.size() : end)); + } + } + + /** + * Returns the ldap entry to merge. + * + * @return ldap entry to merge + */ + public LdapEntry getEntry() { + return ldapEntry; + } + + /** + * Sets the ldap entry to merge into the LDAP. + * + * @param entry to merge + */ + public void setEntry(final LdapEntry entry) { + ldapEntry = entry; + } + + /** + * Returns whether to delete the entry. + * + * @return whether to delete the entry + */ + public boolean getDeleteEntry() { + return deleteEntry; + } + + /** + * Sets whether to delete the entry. + * + * @param b whether to delete the entry + */ + public void setDeleteEntry(final boolean b) { + deleteEntry = b; + } + + /** + * Returns the names of attributes that are used when searching for the entry. + * + * @return attribute names to return when searching + */ + public String[] getSearchAttributes() { + return searchAttrs; + } + + /** + * Sets the list of attribute names that are used when searching for the entry. + * + * @param attrs names to return when searching + */ + public void setSearchAttributes(final String... attrs) { + searchAttrs = attrs; + } + + /** + * Returns the names of attributes that are included when performing a modify. + * + * @return attribute names to include + */ + public String[] getIncludeAttributes() { + return includeAttrs; + } + + /** + * Sets the list of attribute names to include when performing modify. + * + * @param attrs names to include + */ + public void setIncludeAttributes(final String... attrs) { + includeAttrs = attrs; + } + + /** + * Returns the names of attributes that are excluded when performing a modify. + * + * @return attribute names to exclude + */ + public String[] getExcludeAttributes() { + return excludeAttrs; + } + + /** + * Sets the list of attribute names to exclude when performing a modify. + * + * @param attrs names to exclude + */ + public void setExcludeAttributes(final String... attrs) { + excludeAttrs = attrs; + } + + /** + * Returns whether replace should be used for attribute modifications. + * + * @return whether replace should be used for attribute modifications + */ + public boolean isUseReplace() { + return useReplace; + } + + /** + * Sets whether replace should be used for attribute modifications. + * + * @param replace whether replace should be used for attribute modifications + */ + public void setUseReplace(final boolean replace) { + useReplace = replace; + } + + /** + * Returns the attribute modifications handlers. + * + * @return attribute modifications handlers + */ + public AttributeModificationsHandler[] getAttributeModificationsHandlers() { + return attributeModificationsHandlers; + } + + /** + * Sets the attribute value processors. + * + * @param handlers attribute modifications handlers + */ + public void setAttributeModificationsHandlers(final AttributeModificationsHandler... handlers) { + attributeModificationsHandlers = handlers; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "ldapEntry=" + ldapEntry + ", " + + "deleteEntry=" + deleteEntry + ", " + + "searchAttributes=" + Arrays.toString(searchAttrs) + ", " + + "includeAttributes=" + Arrays.toString(includeAttrs) + ", " + + "excludeAttributes=" + Arrays.toString(excludeAttrs) + ", " + + "useReplace=" + useReplace + ", " + + "attributeModificationProcessor=" + + Arrays.toString(attributeModificationsHandlers) + "]"; + } + + /** + * Marker interface for an attribute modifications handler. The complexity of this interface stems from the + * requirement to support batching. A modify operation is executed for each element in the outer list. Attribute + * modifications in the inner list may be mutated as needed to produce modification lists of the desired size and + * complexity. + * + */ + public interface AttributeModificationsHandler + extends Function>, List>> { + } + + /** + * Processes attribute modifications to enforce the maximum number of attribute values in any single attribute. For + * attribute values that exceed this limit, a new attribute is created to contain the excess values. + */ + public static class MaxSizeAttributeValueHandler implements AttributeModificationsHandler { + + /** + * Maximum number of attribute values allowed in a single attribute. + */ + private final int maxSize; + + + /** + * Creates a new max attribute value size processor. + * + * @param size maximum number of attribute values to allow + */ + public MaxSizeAttributeValueHandler(final int size) { + maxSize = size; + } + + + @Override + public List> apply(final List> modifications) { + final List> attrValuesModifications = new ArrayList<>(new ArrayList<>()); + for (List mods : modifications) { + final List attrMods = new ArrayList<>(mods.size()); + for (AttributeModification am : mods) { + if (am.getAttribute().size() > maxSize) { + divideList( + new ArrayList<>(am.getAttribute().getBinaryValues()), + maxSize, + values -> attrMods.add( + new AttributeModification( + am.getOperation(), + LdapAttribute.builder().name(am.getAttribute().getName()).binaryValues(values).build()))); + } else { + attrMods.add(am); + } + } + attrValuesModifications.add(attrMods); + } + return attrValuesModifications; + } + } + + /** + * Processes attribute modifications so that any list of attribute modifications does not exceed the configured batch + * size. For a provided matrix of 1x10 with batch size of 5, would result in a matrix of 2x5. This would result in the + * merge operation performing two modifies, each with five attribute modifications. + */ + public static class BatchHandler implements AttributeModificationsHandler { + + /** + * Batch size to enforce. + */ + private final int batchSize; + + + /** + * Creates a new batch processor. + * + * @param size batch size + */ + public BatchHandler(final int size) { + batchSize = size; + } + + + @Override + public List> apply(final List> modifications) { + final List> batchModifications = new ArrayList<>(modifications.size()); + for (List mods : modifications) { + if (mods.size() > batchSize) { + divideList(mods, batchSize, batchModifications::add); + } else { + batchModifications.add(mods); + } + } + return batchModifications; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/CancelRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/CancelRequest.java new file mode 100644 index 0000000..7e8fe7b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/CancelRequest.java @@ -0,0 +1,40 @@ + +package org.xbib.net.ldap.extended; + +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.UniversalDERTag; + +/** + * LDAP cancel request defined as: + * + *
+ * ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
+ * requestName      [0] LDAPOID,
+ * requestValue     [1] OCTET STRING OPTIONAL }
+ *
+ * cancelRequestValue ::= SEQUENCE {
+ * cancelID        MessageID -- MessageID is as defined in [RFC2251]
+ * }
+ * 
+ * + */ +public class CancelRequest extends ExtendedRequest { + + /** + * OID of this request. + */ + public static final String OID = "1.3.6.1.1.8"; + + + /** + * Creates a new cancel request. + * + * @param id of the message to cancel + */ + public CancelRequest(final int id) { + super(OID); + final ConstructedDEREncoder se = new ConstructedDEREncoder(UniversalDERTag.SEQ, new IntegerType(id)); + setRequestValue(se.encode()); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedOperation.java new file mode 100644 index 0000000..4734095 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedOperation.java @@ -0,0 +1,204 @@ + +package org.xbib.net.ldap.extended; + +import java.util.Arrays; +import org.xbib.net.ldap.AbstractOperation; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.OperationHandle; +import org.xbib.net.ldap.handler.ExtendedValueHandler; + +/** + * Executes an ldap extended operation. + * + */ +public class ExtendedOperation extends AbstractOperation { + + /** + * Function to handle extended response data. + */ + private ExtendedValueHandler[] extendedValueHandlers; + + + /** + * Default constructor. + */ + public ExtendedOperation() { + } + + + /** + * Creates a new extended operation. + * + * @param factory connection factory + */ + public ExtendedOperation(final ConnectionFactory factory) { + super(factory); + } + + /** + * Sends an extended request. See {@link OperationHandle#send()}. + * + * @param factory connection factory + * @param request extended request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + public static ExtendedOperationHandle send(final ConnectionFactory factory, final ExtendedRequest request) + throws LdapException { + final Connection conn = factory.getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return conn.operation(request).onComplete(conn::close).send(); + } + + /** + * Executes an extended request. See {@link OperationHandle#execute()}. + * + * @param factory connection factory + * @param request extended request + * @return extended result + * @throws LdapException if the connection cannot be opened + */ + public static ExtendedResponse execute(final ConnectionFactory factory, final ExtendedRequest request) + throws LdapException { + try (Connection conn = factory.getConnection()) { + conn.open(); + return conn.operation(request).execute(); + } + } + + /** + * Returns a new extended operation with the same properties as the supplied operation. + * + * @param operation to copy + * @return copy of the supplied extended operation + */ + public static ExtendedOperation copy(final ExtendedOperation operation) { + final ExtendedOperation op = new ExtendedOperation(); + op.setRequestHandlers(operation.getRequestHandlers()); + op.setResultHandlers(operation.getResultHandlers()); + op.setControlHandlers(operation.getControlHandlers()); + op.setReferralHandlers(operation.getReferralHandlers()); + op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers()); + op.setExceptionHandler(operation.getExceptionHandler()); + op.setThrowCondition(operation.getThrowCondition()); + op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers()); + op.setConnectionFactory(operation.getConnectionFactory()); + op.setExtendedValueHandlers(operation.getExtendedValueHandlers()); + return op; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + public ExtendedValueHandler[] getExtendedValueHandlers() { + return extendedValueHandlers; + } + + public void setExtendedValueHandlers(final ExtendedValueHandler... handlers) { + extendedValueHandlers = handlers; + } + + /** + * Sends an extended request. See {@link OperationHandle#send()}. + * + * @param request extended request + * @return operation handle + * @throws LdapException if the connection cannot be opened + */ + @Override + public ExtendedOperationHandle send(final ExtendedRequest request) + throws LdapException { + final Connection conn = getConnectionFactory().getConnection(); + try { + conn.open(); + } catch (Exception e) { + conn.close(); + throw e; + } + return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send(); + } + + /** + * Executes an extended request. See {@link OperationHandle#execute()}. + * + * @param request extended request + * @return extended result + * @throws LdapException if the connection cannot be opened + */ + @Override + public ExtendedResponse execute(final ExtendedRequest request) + throws LdapException { + try (Connection conn = getConnectionFactory().getConnection()) { + conn.open(); + return configureHandle(conn.operation(configureRequest(request))).execute(); + } + } + + /** + * Adds configured functions to the supplied handle. + * + * @param handle to configure + * @return configured handle + */ + protected ExtendedOperationHandle configureHandle(final ExtendedOperationHandle handle) { + return handle + .onExtended(getExtendedValueHandlers()) + .onControl(getControlHandlers()) + .onReferral(getReferralHandlers()) + .onIntermediate(getIntermediateResponseHandlers()) + .onException(getExceptionHandler()) + .throwIf(getThrowCondition()) + .onUnsolicitedNotification(getUnsolicitedNotificationHandlers()) + .onResult(getResultHandlers()); + } + + @Override + public String toString() { + return super.toString() + ", " + "extendedValueHandlers=" + Arrays.toString(extendedValueHandlers); + } + + /** + * Extended operation builder. + */ + public static class Builder extends AbstractOperation.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new ExtendedOperation()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the functions to execute when an extended result is complete. + * + * @param handlers to execute on an extended result + * @return this builder + */ + public Builder onExtended(final ExtendedValueHandler... handlers) { + object.setExtendedValueHandlers(handlers); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedOperationHandle.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedOperationHandle.java new file mode 100644 index 0000000..e7feec2 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedOperationHandle.java @@ -0,0 +1,77 @@ + +package org.xbib.net.ldap.extended; + +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.OperationHandle; +import org.xbib.net.ldap.handler.CompleteHandler; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.ExtendedValueHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Handle that notifies on the components of an extended request. + * + */ +public interface ExtendedOperationHandle extends OperationHandle { + + + @Override + ExtendedOperationHandle send(); + + + @Override + ExtendedResponse await() throws LdapException; + + + @Override + default ExtendedResponse execute() + throws LdapException { + return send().await(); + } + + + @Override + ExtendedOperationHandle onResult(ResultHandler... function); + + + @Override + ExtendedOperationHandle onControl(ResponseControlHandler... function); + + + @Override + ExtendedOperationHandle onReferral(ReferralHandler... function); + + + @Override + ExtendedOperationHandle onIntermediate(IntermediateResponseHandler... function); + + + @Override + ExtendedOperationHandle onUnsolicitedNotification(UnsolicitedNotificationHandler... function); + + + @Override + ExtendedOperationHandle onException(ExceptionHandler function); + + + @Override + ExtendedOperationHandle throwIf(ResultPredicate function); + + + @Override + ExtendedOperationHandle onComplete(CompleteHandler function); + + + /** + * Sets the function to execute when an extended result is received. + * + * @param function to execute on an extended result + * @return this handle + */ + ExtendedOperationHandle onExtended(ExtendedValueHandler... function); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedRequest.java new file mode 100644 index 0000000..a9404af --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedRequest.java @@ -0,0 +1,167 @@ + +package org.xbib.net.ldap.extended; + +import java.util.Arrays; +import org.xbib.net.ldap.AbstractRequestMessage; +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP extended request defined as: + * + *
+ * ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
+ * requestName      [0] LDAPOID,
+ * requestValue     [1] OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class ExtendedRequest extends AbstractRequestMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 23; + + /** + * Extended request name. + */ + private String requestName; + + /** + * Extended request value. + */ + private byte[] requestValue; + + + /** + * Default constructor. + */ + private ExtendedRequest() { + } + + + /** + * Creates a new extended request. + * + * @param name of this request + */ + public ExtendedRequest(final String name) { + requestName = name; + } + + + /** + * Creates a new extended request. + * + * @param name of this request + * @param value of this request + */ + public ExtendedRequest(final String name, final byte[] value) { + requestName = name; + requestValue = value; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Sets the request value. Protected method available for extension. + * + * @param value request value + */ + protected void setRequestValue(final byte[] value) { + requestValue = value; + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + if (requestValue == null) { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new OctetStringType(new ContextDERTag(0, false), requestName)), + }; + } else { + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new OctetStringType(new ContextDERTag(0, false), requestName), + new OctetStringType(new ContextDERTag(1, false), requestValue)), + }; + } + } + + @Override + public String toString() { + return super.toString() + ", " + + "requestName=" + requestName + ", " + + "requestValue=" + Arrays.toString(requestValue); + } + + /** + * Extended request builder. + */ + public static class Builder extends AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new ExtendedRequest()); + } + + + /** + * Creates a new builder. + * + * @param r extended request to build + */ + protected Builder(final ExtendedRequest r) { + super(r); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the request name. + * + * @param name request name + * @return this builder + */ + public Builder name(final String name) { + object.requestName = name; + return self(); + } + + + /** + * Sets the request value. + * + * @param value request value + * @return this builder + */ + public Builder value(final byte[] value) { + object.requestValue = value; + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedResponse.java new file mode 100644 index 0000000..17578a8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/ExtendedResponse.java @@ -0,0 +1,237 @@ + +package org.xbib.net.ldap.extended; + +import java.util.Arrays; +import org.xbib.net.ldap.AbstractResult; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP extended response defined as: + * + *
+ * ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
+ * COMPONENTS OF LDAPResult,
+ * responseName     [10] LDAPOID OPTIONAL,
+ * responseValue    [11] OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class ExtendedResponse extends AbstractResult { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 24; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10259; + + /** + * DER path to result code. + */ + private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(24)/ENUM[0]"); + + /** + * DER path to matched DN. + */ + private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(24)/OCTSTR[1]"); + + /** + * DER path to diagnostic message. + */ + private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(24)/OCTSTR[2]"); + + /** + * DER path to referral. + */ + private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(24)/CTX(3)/OCTSTR[0]"); + + /** + * DER path to name. + */ + private static final DERPath NAME_PATH = new DERPath("/SEQ/APP(24)/CTX(10)"); + + /** + * DER path to value. + */ + private static final DERPath VALUE_PATH = new DERPath("/SEQ/APP(24)/CTX(11)"); + + /** + * Response name. + */ + private String responseName; + + /** + * Response value. + */ + private byte[] responseValue; + + + /** + * Default constructor. + */ + protected ExtendedResponse() { + } + + + /** + * Creates a new extended response. + * + * @param buffer to decode + */ + public ExtendedResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this)); + parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this)); + parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this)); + parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this)); + parser.registerHandler(NAME_PATH, new ResponseNameHandler(this)); + parser.registerHandler(VALUE_PATH, new ResponseValueHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + public String getResponseName() { + return responseName; + } + + public void setResponseName(final String name) { + responseName = name; + } + + public byte[] getResponseValue() { + return responseValue; + } + + public void setResponseValue(final byte[] value) { + responseValue = value; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof ExtendedResponse v && super.equals(o)) { + return LdapUtils.areEqual(responseName, v.responseName) && + LdapUtils.areEqual(responseValue, v.responseValue); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs(), + responseName, + responseValue); + } + + @Override + public String toString() { + return super.toString() + ", " + + "responseName=" + responseName + ", " + + "responseValue=" + Arrays.toString(responseValue); + } + + /** + * Parse handler implementation for the response name. + */ + protected static class ResponseNameHandler extends AbstractParseHandler { + + + /** + * Creates a new response name handler. + * + * @param response to configure + */ + ResponseNameHandler(final ExtendedResponse response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setResponseName(OctetStringType.decode(encoded)); + } + } + + /** + * Parse handler implementation for the response value. + */ + protected static class ResponseValueHandler extends AbstractParseHandler { + + + /** + * Creates a new response value handler. + * + * @param response to configure + */ + ResponseValueHandler(final ExtendedResponse response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setResponseValue(encoded.getRemainingBytes()); + } + } + + // CheckStyle:OFF + public static class Builder extends AbstractResult.AbstractBuilder { + + + protected Builder() { + super(new ExtendedResponse()); + } + + + protected Builder(final ExtendedResponse r) { + super(r); + } + + + @Override + protected Builder self() { + return this; + } + + + public Builder responseName(final String name) { + object.responseName = name; + return this; + } + + + public Builder responseValue(final byte[] value) { + object.responseValue = value; + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/IntermediateResponse.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/IntermediateResponse.java new file mode 100644 index 0000000..138c0ab --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/IntermediateResponse.java @@ -0,0 +1,215 @@ + +package org.xbib.net.ldap.extended; + +import java.util.Arrays; +import org.xbib.net.ldap.AbstractMessage; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP extended response defined as: + * + *
+ * IntermediateResponse ::= [APPLICATION 25] SEQUENCE {
+ * responseName     [0] LDAPOID OPTIONAL,
+ * responseValue    [1] OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class IntermediateResponse extends AbstractMessage { + + /** + * BER protocol number. + */ + public static final int PROTOCOL_OP = 25; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10267; + + /** + * Response name. + */ + private String responseName; + + /** + * Response value. + */ + private byte[] responseValue; + + + /** + * Default constructor. + */ + protected IntermediateResponse() { + } + + + /** + * Creates a new intermediate response. + * + * @param buffer to decode + */ + public IntermediateResponse(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(ResponseNameHandler.PATH, new ResponseNameHandler(this)); + parser.registerHandler(ResponseValueHandler.PATH, new ResponseValueHandler(this)); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + public String getResponseName() { + return responseName; + } + + protected void setResponseName(final String name) { + responseName = name; + } + + public byte[] getResponseValue() { + return responseValue; + } + + protected void setResponseValue(final byte[] value) { + responseValue = value; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof IntermediateResponse v && super.equals(o)) { + return LdapUtils.areEqual(responseName, v.responseName) && + LdapUtils.areEqual(responseValue, v.responseValue); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + responseName, + responseValue); + } + + @Override + public String toString() { + return super.toString() + ", " + + "responseName=" + responseName + ", " + + "responseValue=" + Arrays.toString(responseValue); + } + + /** + * Parse handler implementation for the response name. + */ + protected static class ResponseNameHandler extends AbstractParseHandler { + + /** + * DER path to response name. + */ + public static final DERPath PATH = new DERPath("/SEQ/APP(25)/CTX(0)"); + + + /** + * Creates a new response name handler. + * + * @param response to configure + */ + ResponseNameHandler(final IntermediateResponse response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + if (encoded.remaining() > 0) { + getObject().setResponseName(OctetStringType.decode(encoded)); + } + } + } + + /** + * Parse handler implementation for the response value. + */ + protected static class ResponseValueHandler extends AbstractParseHandler { + + /** + * DER path to response value. + */ + public static final DERPath PATH = new DERPath("/SEQ/APP(25)/CTX(1)"); + + + /** + * Creates a new response value handler. + * + * @param response to configure + */ + ResponseValueHandler(final IntermediateResponse response) { + super(response); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + if (encoded.remaining() > 0) { + final DERParser p = new DERParser(); + p.readTag(encoded).getTagNo(); + p.readLength(encoded); + getObject().setResponseValue(encoded.getRemainingBytes()); + } + } + } + + // CheckStyle:OFF + public static class Builder extends AbstractMessage.AbstractBuilder { + + + protected Builder() { + super(new IntermediateResponse()); + } + + + protected Builder(final IntermediateResponse r) { + super(r); + } + + + @Override + protected Builder self() { + return this; + } + + + public Builder responseName(final String name) { + object.setResponseName(name); + return this; + } + + + public Builder responseValue(final byte[] value) { + object.setResponseValue(value); + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/NoticeOfDisconnection.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/NoticeOfDisconnection.java new file mode 100644 index 0000000..99feacc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/NoticeOfDisconnection.java @@ -0,0 +1,97 @@ + +package org.xbib.net.ldap.extended; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.DERBuffer; + +/** + * LDAP notice of disconnection defined as: + * + *
+ * ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
+ * COMPONENTS OF LDAPResult,
+ * responseName     [10] LDAPOID OPTIONAL,
+ * responseValue    [11] OCTET STRING OPTIONAL }
+ * 
+ *

+ * where the result code indicates the reason for disconnection and the response value is absent. + * + */ +public class NoticeOfDisconnection extends UnsolicitedNotification { + + /** + * OID of this response. + */ + public static final String OID = "1.3.6.1.4.1.1466.20036"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10289; + + + /** + * Default constructor. + */ + public NoticeOfDisconnection() { + setResponseName(OID); + } + + + /** + * Creates a new notice of disconnection. + * + * @param buffer to decode + */ + public NoticeOfDisconnection(final DERBuffer buffer) { + super(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof NoticeOfDisconnection && super.equals(o); + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs(), + getResponseName(), + getResponseValue()); + } + + // CheckStyle:OFF + public static class Builder extends UnsolicitedNotification.Builder { + + + protected Builder() { + super(new NoticeOfDisconnection()); + } + + + @Override + protected Builder self() { + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/PasswordModifyRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/PasswordModifyRequest.java new file mode 100644 index 0000000..5a9d81f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/PasswordModifyRequest.java @@ -0,0 +1,83 @@ + +package org.xbib.net.ldap.extended; + +import java.util.ArrayList; +import java.util.List; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextType; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.UniversalDERTag; + +/** + * LDAP password modify request defined as: + * + *

+ * PasswdModifyRequestValue ::= SEQUENCE {
+ * userIdentity    [0]  OCTET STRING OPTIONAL
+ * oldPasswd       [1]  OCTET STRING OPTIONAL
+ * newPasswd       [2]  OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class PasswordModifyRequest extends ExtendedRequest { + + /** + * OID of this request. + */ + public static final String OID = "1.3.6.1.4.1.4203.1.11.1"; + + + /** + * Creates a new password modify request. + */ + public PasswordModifyRequest() { + super(OID); + } + + + /** + * Creates a new password modify request. + * + * @param identity to modify or null + */ + public PasswordModifyRequest(final String identity) { + this(identity, null, null); + } + + + /** + * Creates a new password modify request. + * + * @param identity to modify or null + * @param oldPass current password for the dn or null + */ + public PasswordModifyRequest(final String identity, final String oldPass) { + this(identity, oldPass, null); + } + + + /** + * Creates a new password modify request. + * + * @param identity to modify or null + * @param oldPass current password for the dn or null + * @param newPass desired password for the dn or null + */ + public PasswordModifyRequest(final String identity, final String oldPass, final String newPass) { + super(OID); + final List l = new ArrayList<>(); + if (identity != null) { + l.add(new ContextType(0, identity)); + } + if (oldPass != null) { + l.add(new ContextType(1, oldPass)); + } + if (newPass != null) { + l.add(new ContextType(2, newPass)); + } + final ConstructedDEREncoder se = new ConstructedDEREncoder( + UniversalDERTag.SEQ, + l.toArray(DEREncoder[]::new)); + setRequestValue(se.encode()); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/PasswordModifyResponseParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/PasswordModifyResponseParser.java new file mode 100644 index 0000000..c07bb3f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/PasswordModifyResponseParser.java @@ -0,0 +1,66 @@ + +package org.xbib.net.ldap.extended; + +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.DefaultDERBuffer; +import org.xbib.asn1.OctetStringType; + +/** + * Utility class for parsing the responseValue from a password generation. + * + */ +public final class PasswordModifyResponseParser { + + + /** + * Default constructor. + */ + private PasswordModifyResponseParser() { + } + + + /** + * Parse the supplied extended operation response. + * + * @param response from a password modify extended operation + * @return generated password + */ + public static String parse(final ExtendedResponse response) { + final StringBuilder sb = new StringBuilder(); + final DERParser p = new DERParser(); + p.registerHandler(GenPasswdHandler.PATH, new GenPasswdHandler(sb)); + p.parse(new DefaultDERBuffer(response.getResponseValue())); + return sb.toString(); + } + + + /** + * Parse handler implementation for the genPasswd. + */ + private static class GenPasswdHandler extends AbstractParseHandler { + + /** + * DER path to generated password. + */ + public static final DERPath PATH = new DERPath("/SEQ/CTX(0)"); + + + /** + * Creates a new gen passwd handler. + * + * @param sb to append to + */ + GenPasswdHandler(final StringBuilder sb) { + super(sb); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().append(OctetStringType.decode(encoded)); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/StartTLSRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/StartTLSRequest.java new file mode 100644 index 0000000..50dcfa0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/StartTLSRequest.java @@ -0,0 +1,30 @@ + +package org.xbib.net.ldap.extended; + +/** + * LDAP startTLS request defined as: + * + *
+ * ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
+ * requestName      [0] LDAPOID,
+ * requestValue     [1] OCTET STRING OPTIONAL }
+ * 
+ *

+ * where the request value is absent. + * + */ +public class StartTLSRequest extends ExtendedRequest { + + /** + * OID of this request. + */ + public static final String OID = "1.3.6.1.4.1.1466.20037"; + + + /** + * Default constructor. + */ + public StartTLSRequest() { + super(OID); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/SyncInfoMessage.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/SyncInfoMessage.java new file mode 100644 index 0000000..4bd8dda --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/SyncInfoMessage.java @@ -0,0 +1,674 @@ + +package org.xbib.net.ldap.extended; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.AbstractParseHandler; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.ParseHandler; +import org.xbib.asn1.UuidType; +import org.xbib.net.ldap.control.ResponseControl; + +/** + * Intermediate response message for LDAP content synchronization. See RFC 4533. Message is defined as: + * + *

+ * IntermediateResponse ::= [APPLICATION 25] SEQUENCE {
+ * responseName     [0] LDAPOID OPTIONAL,
+ * responseValue    [1] OCTET STRING OPTIONAL }
+ *
+ *
+ * syncInfoValue ::= CHOICE {
+ * newcookie      [0] syncCookie,
+ * refreshDelete  [1] SEQUENCE {
+ * cookie         syncCookie OPTIONAL,
+ * refreshDone    BOOLEAN DEFAULT TRUE
+ * },
+ * refreshPresent [2] SEQUENCE {
+ * cookie         syncCookie OPTIONAL,
+ * refreshDone    BOOLEAN DEFAULT TRUE
+ * },
+ * syncIdSet      [3] SEQUENCE {
+ * cookie         syncCookie OPTIONAL,
+ * refreshDeletes BOOLEAN DEFAULT FALSE,
+ * syncUUIDs      SET OF syncUUID
+ * }
+ * }
+ * 
+ * + */ +public class SyncInfoMessage extends IntermediateResponse { + + /** + * OID of this response. + */ + public static final String OID = "1.3.6.1.4.1.4203.1.9.1.4"; + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10321; + + /** + * DER path to new cookie. + */ + private static final DERPath NEW_COOKIE_PATH = new DERPath("/CTX(0)"); + + /** + * DER path to refresh delete. + */ + private static final DERPath REFRESH_DELETE_PATH = new DERPath("/CTX(1)"); + + /** + * DER path to refresh delete cookie. + */ + private static final DERPath REFRESH_DELETE_COOKIE_PATH = new DERPath("/CTX(1)/OCTSTR[0]"); + + /** + * DER path to refresh delete done. + */ + private static final DERPath REFRESH_DELETE_DONE_PATH = new DERPath("/CTX(1)/BOOL[1]"); + + /** + * DER path to refresh present. + */ + private static final DERPath REFRESH_PRESENT_PATH = new DERPath("/CTX(2)"); + + /** + * DER path to refresh present cookie. + */ + private static final DERPath REFRESH_PRESENT_COOKIE_PATH = new DERPath("/CTX(2)/OCTSTR[0]"); + + /** + * DER path to refresh present done. + */ + private static final DERPath REFRESH_PRESENT_DONE_PATH = new DERPath("/CTX(2)/BOOL[1]"); + + /** + * DER path to sync ID set. + */ + private static final DERPath SYNC_ID_SET_PATH = new DERPath("/CTX(3)"); + + /** + * DER path to sync ID set cookie. + */ + private static final DERPath SYNC_ID_SET_COOKIE_PATH = new DERPath("/CTX(3)/OCTSTR[0]"); + + /** + * DER path to sync ID set deletes. + */ + private static final DERPath SYNC_ID_SET_DELETES_PATH = new DERPath("/CTX(3)/BOOL[1]"); + + /** + * DER path to sync ID set UUIDS. + */ + private static final DERPath SYNC_ID_SET_UUIDS_PATH = new DERPath("/CTX(3)/SET/OCTSTR"); + /** + * message type. + */ + private Type messageType; + /** + * server generated cookie. + */ + private byte[] cookie; + /** + * refresh done. + */ + private boolean refreshDone = true; + /** + * refresh deletes. + */ + private boolean refreshDeletes; + /** + * entry uuids. + */ + private Set entryUuids = new LinkedHashSet<>(); + + /** + * Default constructor. + */ + protected SyncInfoMessage() { + setResponseName(OID); + } + + + /** + * Creates a new sync info message. + * + * @param buffer to decode + */ + public SyncInfoMessage(final DERBuffer buffer) { + final DERParser parser = new DERParser(); + parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this)); + parser.registerHandler(ResponseNameHandler.PATH, new ResponseNameHandler(this)); + parser.registerHandler(ResponseValueHandler.PATH, getResponseValueParseHandler()); + parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this)); + parser.parse(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + protected ParseHandler getResponseValueParseHandler() { + return (parser, encoded) -> { + final DERParser p = new DERParser(); + p.registerHandler(NEW_COOKIE_PATH, new NewCookieHandler(SyncInfoMessage.this)); + p.registerHandler(REFRESH_DELETE_PATH, new RefreshDeleteHandler(SyncInfoMessage.this)); + p.registerHandler(REFRESH_DELETE_COOKIE_PATH, new RefreshDeleteCookieHandler(SyncInfoMessage.this)); + p.registerHandler(REFRESH_DELETE_DONE_PATH, new RefreshDeleteDoneHandler(SyncInfoMessage.this)); + p.registerHandler(REFRESH_PRESENT_PATH, new RefreshPresentHandler(SyncInfoMessage.this)); + p.registerHandler(REFRESH_PRESENT_COOKIE_PATH, new RefreshPresentCookieHandler(SyncInfoMessage.this)); + p.registerHandler(REFRESH_PRESENT_DONE_PATH, new RefreshPresentDoneHandler(SyncInfoMessage.this)); + p.registerHandler(SYNC_ID_SET_PATH, new SyncIdSetHandler(SyncInfoMessage.this)); + p.registerHandler(SYNC_ID_SET_COOKIE_PATH, new SyncIdSetCookieHandler(SyncInfoMessage.this)); + p.registerHandler(SYNC_ID_SET_DELETES_PATH, new SyncIdSetDeletesHandler(SyncInfoMessage.this)); + p.registerHandler(SYNC_ID_SET_UUIDS_PATH, new SyncIdSetUuidsHandler(SyncInfoMessage.this)); + p.parse(encoded); + }; + } + + + /** + * Returns the message type. + * + * @return message type + */ + public Type getMessageType() { + return messageType; + } + + + /** + * Sets the message type. + * + * @param type message type + */ + public void setMessageType(final Type type) { + messageType = type; + } + + + /** + * Returns the sync request cookie. + * + * @return sync request cookie + */ + public byte[] getCookie() { + return cookie; + } + + + /** + * Sets the sync request cookie. + * + * @param value sync request cookie + */ + public void setCookie(final byte[] value) { + cookie = value; + } + + + /** + * Returns whether refreshes are done. + * + * @return refresh done + */ + public boolean getRefreshDone() { + return refreshDone; + } + + + /** + * Sets whether refreshes are done. + * + * @param b refresh done + */ + public void setRefreshDone(final boolean b) { + refreshDone = b; + } + + + /** + * Returns whether to refresh deletes. + * + * @return whether to refresh deletes + */ + public boolean getRefreshDeletes() { + return refreshDeletes; + } + + + /** + * Sets whether to refresh deletes. + * + * @param b whether to refresh deletes + */ + public void setRefreshDeletes(final boolean b) { + refreshDeletes = b; + } + + + /** + * Returns the entry uuids. + * + * @return entry uuids + */ + public Set getEntryUuids() { + return entryUuids; + } + + + /** + * Adds the supplied UUIDs to this message. + * + * @param uuids to add + */ + public void addEntryUuids(final UUID... uuids) { + Collections.addAll(entryUuids, uuids); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SyncInfoMessage && super.equals(o)) { + final SyncInfoMessage v = (SyncInfoMessage) o; + return LdapUtils.areEqual(messageType, v.messageType) && + LdapUtils.areEqual(cookie, v.cookie) && + LdapUtils.areEqual(refreshDone, v.refreshDone) && + LdapUtils.areEqual(refreshDeletes, v.refreshDeletes) && + LdapUtils.areEqual(entryUuids, v.entryUuids); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResponseName(), + getResponseValue(), + messageType, + cookie, + refreshDone, + refreshDeletes, + entryUuids); + } + + + @Override + public String toString() { + return super.toString() + ", " + + "messageType=" + messageType + ", " + + "cookie=" + LdapUtils.base64Encode(cookie) + ", " + + "refreshDone=" + refreshDone + ", " + + "refreshDeletes=" + refreshDeletes + ", " + + "entryUuids=" + entryUuids; + } + + + /** + * Types of request modes. + */ + public enum Type { + + /** + * new cookie. + */ + NEW_COOKIE, + + /** + * refresh delete. + */ + REFRESH_DELETE, + + /** + * refresh present. + */ + REFRESH_PRESENT, + + /** + * sync id set. + */ + SYNC_ID_SET + } + + /** + * Parse handler implementation for new cookie. + */ + private static class NewCookieHandler extends AbstractParseHandler { + + + /** + * Creates a new cookie handler. + * + * @param message to configure + */ + NewCookieHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setMessageType(Type.NEW_COOKIE); + + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setCookie(cookie); + } + } + } + + /** + * Parse handler implementation for refresh delete. + */ + private static class RefreshDeleteHandler extends AbstractParseHandler { + + + /** + * Creates a refresh delete handler. + * + * @param message to configure + */ + RefreshDeleteHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setMessageType(Type.REFRESH_DELETE); + } + } + + /** + * Parse handler implementation for refresh delete cookie. + */ + private static class RefreshDeleteCookieHandler extends AbstractParseHandler { + + + /** + * Creates a refresh delete cookie handler. + * + * @param message to configure + */ + RefreshDeleteCookieHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setCookie(cookie); + } + } + } + + /** + * Parse handler implementation for refresh delete done. + */ + private static class RefreshDeleteDoneHandler extends AbstractParseHandler { + + + /** + * Creates a refresh delete done handler. + * + * @param message to configure + */ + RefreshDeleteDoneHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setRefreshDone(BooleanType.decode(encoded)); + } + } + + /** + * Parse handler implementation for refresh present. + */ + private static class RefreshPresentHandler extends AbstractParseHandler { + + + /** + * Creates a refresh present handler. + * + * @param message to configure + */ + RefreshPresentHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setMessageType(Type.REFRESH_PRESENT); + } + } + + /** + * Parse handler implementation for refresh present cookie. + */ + private static class RefreshPresentCookieHandler extends AbstractParseHandler { + + + /** + * Creates a refresh present cookie handler. + * + * @param message to configure + */ + RefreshPresentCookieHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setCookie(cookie); + } + } + } + + /** + * Parse handler implementation for refresh present done. + */ + private static class RefreshPresentDoneHandler extends AbstractParseHandler { + + + /** + * Creates a refresh present done handler. + * + * @param message to configure + */ + RefreshPresentDoneHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setRefreshDone(BooleanType.decode(encoded)); + } + } + + /** + * Parse handler implementation for sync id set. + */ + private static class SyncIdSetHandler extends AbstractParseHandler { + + + /** + * Creates a sync id set handler. + * + * @param message to configure + */ + SyncIdSetHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setMessageType(Type.SYNC_ID_SET); + } + } + + /** + * Parse handler implementation for sync id set cookie. + */ + private static class SyncIdSetCookieHandler extends AbstractParseHandler { + + + /** + * Creates a sync id set cookie handler. + * + * @param message to configure + */ + SyncIdSetCookieHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + final byte[] cookie = encoded.getRemainingBytes(); + if (cookie != null && cookie.length > 0) { + getObject().setCookie(cookie); + } + } + } + + /** + * Parse handler implementation for sync id set deletes. + */ + private static class SyncIdSetDeletesHandler extends AbstractParseHandler { + + + /** + * Creates a sync id set deletes handler. + * + * @param message to configure + */ + SyncIdSetDeletesHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().setRefreshDeletes(BooleanType.decode(encoded)); + } + } + + /** + * Parse handler implementation for sync id set uuids. + */ + private static class SyncIdSetUuidsHandler extends AbstractParseHandler { + + + /** + * Creates a sync id set uuids handler. + * + * @param message to configure + */ + SyncIdSetUuidsHandler(final SyncInfoMessage message) { + super(message); + } + + + @Override + public void handle(final DERParser parser, final DERBuffer encoded) { + getObject().getEntryUuids().add(UuidType.decode(encoded)); + } + } + + // CheckStyle:OFF + public static class Builder extends IntermediateResponse.Builder { + + + protected Builder() { + super(new SyncInfoMessage()); + } + + + protected Builder(final SyncInfoMessage m) { + super(m); + } + + + @Override + protected Builder self() { + return this; + } + + + @Override + public Builder messageID(final int id) { + object.setMessageID(id); + return self(); + } + + + @Override + public Builder controls(final ResponseControl... controls) { + object.addControls(controls); + return self(); + } + + + public Builder type(final Type t) { + ((SyncInfoMessage) object).messageType = t; + return this; + } + + + public Builder cookie(final byte[] b) { + ((SyncInfoMessage) object).cookie = b; + return this; + } + + + public Builder refreshDone(final boolean b) { + ((SyncInfoMessage) object).refreshDone = b; + return this; + } + + + public Builder refreshDeletes(final boolean b) { + ((SyncInfoMessage) object).refreshDeletes = b; + return this; + } + + + public Builder uuids(final UUID... uuids) { + ((SyncInfoMessage) object).addEntryUuids(uuids); + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/UnsolicitedNotification.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/UnsolicitedNotification.java new file mode 100644 index 0000000..1d7a963 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/UnsolicitedNotification.java @@ -0,0 +1,105 @@ + +package org.xbib.net.ldap.extended; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.DERBuffer; + +/** + * LDAP unsolicited notification defined as: + * + *
+ * ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
+ * COMPONENTS OF LDAPResult,
+ * responseName     [10] LDAPOID OPTIONAL,
+ * responseValue    [11] OCTET STRING OPTIONAL }
+ * 
+ *

+ * where the messageID is always zero. + * + */ +public class UnsolicitedNotification extends ExtendedResponse { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10331; + + + /** + * Default constructor. + */ + public UnsolicitedNotification() { + setMessageID(0); + } + + + /** + * Creates a new unsolicited notification. + * + * @param buffer to decode + */ + public UnsolicitedNotification(final DERBuffer buffer) { + super(buffer); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public void setMessageID(final int id) { + if (id != 0) { + throw new IllegalArgumentException("Message ID must be zero"); + } + super.setMessageID(id); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof UnsolicitedNotification && super.equals(o); + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + getMessageID(), + getControls(), + getResultCode(), + getMatchedDN(), + getDiagnosticMessage(), + getReferralURLs(), + getResponseName(), + getResponseValue()); + } + + // CheckStyle:OFF + public static class Builder extends ExtendedResponse.Builder { + + + protected Builder() { + super(new UnsolicitedNotification()); + } + + + protected Builder(final UnsolicitedNotification n) { + super(n); + } + + + @Override + protected Builder self() { + return this; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/WhoAmIRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/WhoAmIRequest.java new file mode 100644 index 0000000..d8aa6d0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/WhoAmIRequest.java @@ -0,0 +1,30 @@ + +package org.xbib.net.ldap.extended; + +/** + * LDAP who am i request defined as: + * + *

+ * ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
+ * requestName      [0] LDAPOID,
+ * requestValue     [1] OCTET STRING OPTIONAL }
+ * 
+ *

+ * where the request value is absent. + * + */ +public class WhoAmIRequest extends ExtendedRequest { + + /** + * OID of this request. + */ + public static final String OID = "1.3.6.1.4.1.4203.1.11.3"; + + + /** + * Default constructor. + */ + public WhoAmIRequest() { + super(OID); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/extended/WhoAmIResponseParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/extended/WhoAmIResponseParser.java new file mode 100644 index 0000000..a393180 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/extended/WhoAmIResponseParser.java @@ -0,0 +1,29 @@ + +package org.xbib.net.ldap.extended; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Utility class for parsing the responseValue from a whoami extended operation. + * + */ +public final class WhoAmIResponseParser { + + + /** + * Default constructor. + */ + private WhoAmIResponseParser() { + } + + + /** + * Parse the supplied extended operation response. + * + * @param response from a password modify extended operation + * @return generated password + */ + public static String parse(final ExtendedResponse response) { + return LdapUtils.utf8Encode(response.getResponseValue(), false); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/AbstractAttributeValueAssertionFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/AbstractAttributeValueAssertionFilter.java new file mode 100644 index 0000000..6cbc0c4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/AbstractAttributeValueAssertionFilter.java @@ -0,0 +1,102 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.OctetStringType; + +/** + * Base class for attribute value assertion filters. + * + */ +public abstract class AbstractAttributeValueAssertionFilter implements Filter { + + /** + * Type of filter. + */ + protected final Filter.Type filterType; + + /** + * Attribute description. + */ + protected final String attributeDesc; + + /** + * Attribute value. + */ + protected final byte[] assertionValue; + + + /** + * Creates a new abstract attribute value assertion filter. + * + * @param type of filter + * @param name attribute description + * @param value attribute value + */ + public AbstractAttributeValueAssertionFilter(final Filter.Type type, final String name, final byte[] value) { + filterType = type; + attributeDesc = name; + assertionValue = value; + } + + + /** + * Returns the attribute description. + * + * @return attribute description + */ + public String getAttributeDesc() { + return attributeDesc; + } + + + /** + * Returns the assertion value. + * + * @return assertion value + */ + public byte[] getAssertionValue() { + return assertionValue; + } + + + @Override + public DEREncoder getEncoder() { + return new ConstructedDEREncoder( + new ContextDERTag(filterType.ordinal(), true), + new OctetStringType(attributeDesc), + new OctetStringType(assertionValue)); + } + + + // CheckStyle:EqualsHashCode OFF + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AbstractAttributeValueAssertionFilter v) { + return LdapUtils.areEqual(filterType, v.filterType) && + LdapUtils.areEqual(attributeDesc, v.attributeDesc) && + LdapUtils.areEqual(assertionValue, v.assertionValue); + } + return false; + } + // CheckStyle:EqualsHashCode ON + + + @Override + public abstract int hashCode(); + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "filterType=" + filterType + ", " + + "attributeDesc=" + attributeDesc + ", " + + "assertionValue=" + LdapUtils.utf8Encode(assertionValue); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/AbstractFilterFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/AbstractFilterFunction.java new file mode 100644 index 0000000..9084ee7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/AbstractFilterFunction.java @@ -0,0 +1,159 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.ResultCode; + +/** + * Base implementation to parse an LDAP search filter string. + * + */ +public abstract class AbstractFilterFunction implements FilterFunction { + + + @Override + public Filter parse(final String filter) + throws FilterParseException { + final String balancedFilter; + // Check for balanced parentheses + if (filter.startsWith("(")) { + if (!filter.endsWith(")")) { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Unbalanced parentheses. Opening paren without closing paren."); + } + balancedFilter = filter; + } else if (filter.endsWith(")")) { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Unbalanced parentheses. Closing paren without opening paren."); + } else { + // Allow entire filter strings without enclosing parentheses + balancedFilter = "(".concat(filter).concat(")"); + } + + return readNextComponent(balancedFilter); + } + + + /** + * Reads the next component contained in the supplied filter. + * + * @param filter to parse + * @return search filter + * @throws FilterParseException if filter does not start with '(' and end with ')' + */ + private Filter readNextComponent(final String filter) + throws FilterParseException { + final int end = filter.length() - 1; + if (filter.charAt(0) != '(' || filter.charAt(end) != ')') { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Filter must be surround by parentheses: '" + filter + "'"); + } + int pos = 1; + final Filter searchFilter; + switch (filter.charAt(pos)) { + + case '&': + searchFilter = readFilterSet(new AndFilter(), filter, ++pos, end); + break; + + case '|': + searchFilter = readFilterSet(new OrFilter(), filter, ++pos, end); + break; + + case '!': + searchFilter = readFilterSet(new NotFilter(), filter, ++pos, end); + break; + + default: + // attempt to match a non-set filter type + searchFilter = parseFilterComp(filter); + if (searchFilter == null) { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Could not determine filter type for '" + filter + "'"); + } + break; + } + return searchFilter; + } + + + /** + * Reads the supplied filter using the supplied indices and adds them to the supplied filter set. + * + * @param set to update + * @param filter to parse + * @param start position in filter + * @param end position in filter + * @return the supplied filter set with components added from filter + * @throws FilterParseException if filter doesn't start with '(' and containing a matching ')' + */ + private FilterSet readFilterSet(final FilterSet set, final String filter, final int start, final int end) + throws FilterParseException { + int pos = start; + int closeIndex = findMatchingParenPosition(filter, pos); + if (filter.charAt(pos) != '(' || closeIndex == -1 || closeIndex == end) { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Invalid filter syntax, missing parenthesis after " + set.getType()); + } + while (pos < end) { + try { + set.add(readNextComponent(filter.substring(pos, closeIndex + 1))); + } catch (Exception e) { + throw new FilterParseException(ResultCode.FILTER_ERROR, e); + } + pos = closeIndex + 1; + if (pos < end) { + closeIndex = findMatchingParenPosition(filter, pos); + } + } + return set; + } + + + /** + * Returns the index in the supplied filter of the closing paren that matches the opening paren at the start of the + * filter. + * + * @param filter to search + * @param start position of the opening paren + * @return index of the matching paren + * @throws FilterParseException if filter is null, empty or does not begin with '(' + */ + private int findMatchingParenPosition(final String filter, final int start) + throws FilterParseException { + if (filter == null || filter.length() == 0) { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Filter cannot be null or empty"); + } + if (filter.charAt(start) != '(') { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Filter must begin with '('"); + } + int pos = start + 1; + int parenCount = 1; + while (pos < filter.length()) { + final char c = filter.charAt(pos); + if (c == '(') { + parenCount++; + } else if (c == ')') { + parenCount--; + } + if (parenCount == 0) { + return pos; + } + pos++; + } + return -1; + } + + + /** + * Inspects the supplied filter string and creates the type of filter it represents. + * + * @param filter to inspect + * @return search filter + * @throws FilterParseException if filter is invalid + */ + protected abstract Filter parseFilterComp(String filter) + throws FilterParseException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/AndFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/AndFilter.java new file mode 100644 index 0000000..d3875ea --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/AndFilter.java @@ -0,0 +1,108 @@ + +package org.xbib.net.ldap.filter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.NullType; + +/** + * And search filter set defined as: + * + *

+ * (&(filter)(filter)...)
+ * 
+ * + */ +public class AndFilter implements FilterSet { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10009; + + /** + * Components of this filter. + */ + private final List filterComponents = new ArrayList<>(); + + + /** + * Default constructor. + */ + public AndFilter() { + } + + + /** + * Creates a new and filter. + * + * @param components of this filter + */ + public AndFilter(final Filter... components) { + filterComponents.addAll(Arrays.asList(components)); + } + + + @Override + public Type getType() { + return Type.AND; + } + + + @Override + public void add(final Filter component) { + filterComponents.add(component); + } + + + /** + * Returns the components of this filter. + * + * @return filter components + */ + public List getComponents() { + return Collections.unmodifiableList(filterComponents); + } + + + @Override + public DEREncoder getEncoder() { + if (filterComponents.size() == 0) { + return new NullType(new ContextDERTag(Type.AND.ordinal(), true)); + } else { + return new ConstructedDEREncoder( + new ContextDERTag(Type.AND.ordinal(), true), + filterComponents.stream().map(Filter::getEncoder).toArray(DEREncoder[]::new)); + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AndFilter v) { + return LdapUtils.areEqual(filterComponents, v.filterComponents); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, filterComponents); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::filterComponents=" + filterComponents; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/ApproximateFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/ApproximateFilter.java new file mode 100644 index 0000000..ba831dc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/ApproximateFilter.java @@ -0,0 +1,57 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Approximate search filter component defined as: + * + *
+ * (attributeDescription~=attributeValue)
+ * 
+ * + */ +public class ApproximateFilter extends AbstractAttributeValueAssertionFilter { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10037; + + + /** + * Creates a new approximate filter. + * + * @param name attribute description + * @param value attribute value + */ + public ApproximateFilter(final String name, final String value) { + super(Type.APPROXIMATE_MATCH, name, LdapUtils.utf8Encode(value, false)); + } + + + /** + * Creates a new approximate filter. + * + * @param name attribute description + * @param value attribute value + */ + public ApproximateFilter(final String name, final byte[] value) { + super(Type.APPROXIMATE_MATCH, name, value); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof ApproximateFilter && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, filterType, attributeDesc, assertionValue); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/DefaultFilterFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/DefaultFilterFunction.java new file mode 100644 index 0000000..b54f9ca --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/DefaultFilterFunction.java @@ -0,0 +1,351 @@ + +package org.xbib.net.ldap.filter; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.xbib.net.ldap.ResultCode; + +/** + * Parses an LDAP search filter string. + * + */ +public class DefaultFilterFunction extends AbstractFilterFunction { + + /** + * Lower and upper case ASCII alphabetical, digits, semi-colon, dot, dash. + */ + protected static final String DEFAULT_ATTRIBUTE_DESCRIPTION_CHARS = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + ";.-"; + + /** + * Allowed attribute description characters. + */ + private final String attributeDescriptionChars; + + + /** + * Default constructor. + */ + public DefaultFilterFunction() { + this(DEFAULT_ATTRIBUTE_DESCRIPTION_CHARS); + } + + + /** + * Creates a new default filter function. + * + * @param validChars characters that are valid for an attribute description + */ + public DefaultFilterFunction(final String validChars) { + attributeDescriptionChars = validChars; + } + + + @Override + protected Filter parseFilterComp(final String filter) + throws FilterParseException { + if (filter == null || filter.isEmpty()) { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Filter cannot be null or empty"); + } + CharBuffer filterBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(filter.getBytes())); + if (filterBuffer.get() != '(') { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Filter '" + filter + "' must start with '('"); + } + if (filterBuffer.get(filterBuffer.limit() - 1) != ')') { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Filter '" + filter + "' must end with ')'"); + } + if (!filterBuffer.hasRemaining()) { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Filter '" + filter + "' does not contain an expression"); + } + final Filter searchFilter; + filterBuffer = filterBuffer.limit(filterBuffer.limit() - 1).slice(); + if (!filterBuffer.hasRemaining()) { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Filter '" + filter + "' does not contain an expression"); + } + if (filterBuffer.get() == ':') { + // extensible filter with no attribute description + searchFilter = parseExtensible(null, filterBuffer); + } else { + // read an attribute + filterBuffer.position(filterBuffer.position() - 1); + final CharBuffer attribute = readAttribute(filterBuffer); + if (attribute.length() == 0) { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Invalid attribute description for filter '" + filter + "'"); + } + switch (filterBuffer.get()) { + case '=': + if (!filterBuffer.hasRemaining()) { + // empty equality + searchFilter = new EqualityFilter(attribute.toString(), new byte[0]); + } else { + if (filterBuffer.get() == '*' && !filterBuffer.hasRemaining()) { + // presence + searchFilter = new PresenceFilter(attribute.toString()); + } else { + // substring or equality + searchFilter = parseSubstringOrEquality( + attribute.toString(), + filterBuffer.position(filterBuffer.position() - 1).slice()); + } + } + break; + case ':': + if (filterBuffer.get() != '=') { + searchFilter = parseExtensible( + attribute.toString(), + filterBuffer.position(filterBuffer.position() - 1).slice()); + } else { + try { + searchFilter = new ExtensibleFilter( + null, + attribute.toString(), + FilterUtils.parseAssertionValue(filterBuffer.slice().toString())); + } catch (IllegalArgumentException e) { + throw new FilterParseException(ResultCode.FILTER_ERROR, e); + } + } + break; + case '>': + if (filterBuffer.get() != '=') { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Invalid greaterOrEqual expression for filter '" + filter + "'"); + } + searchFilter = new GreaterOrEqualFilter( + attribute.toString(), + FilterUtils.parseAssertionValue(filterBuffer.slice().toString())); + break; + case '<': + if (filterBuffer.get() != '=') { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Invalid lessOrEqual expression for filter '" + filter + "'"); + } + searchFilter = new LessOrEqualFilter( + attribute.toString(), + FilterUtils.parseAssertionValue(filterBuffer.slice().toString())); + break; + case '~': + if (filterBuffer.get() != '=') { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Invalid approximate expression for filter '" + filter + "'"); + } + searchFilter = new ApproximateFilter( + attribute.toString(), + FilterUtils.parseAssertionValue(filterBuffer.slice().toString())); + break; + default: + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Invalid filter expression for filter '" + filter + "'"); + } + } + return searchFilter; + } + + + /** + * Returns a new buffer containing an attribute description. The supplied buffer will have its position set to the + * next position after the attribute. + * + * @param cb to read from + * @return new char buffer + * @throws FilterParseException if the char buffer is empty + */ + private CharBuffer readAttribute(final CharBuffer cb) + throws FilterParseException { + if (cb.length() == 0) { + throw new FilterParseException(ResultCode.LOCAL_ERROR, "Attribute buffer size must be greater than zero"); + } + final int limit = cb.limit(); + while (cb.hasRemaining()) { + final char c = cb.get(); + if (attributeDescriptionChars.indexOf(c) == -1) { + break; + } + } + final int pos = cb.position() - 1; + cb.position(pos); + final CharBuffer slice = cb.flip().slice(); + cb.limit(limit).position(pos); + return slice; + } + + + /** + * Parses the supplied buffer and returns either a substring or equality filter. + * + * @param attribute attribute description + * @param cb containing the assertion + * @return either EqualityFilter or SubstringFilter + * @throws FilterParseException if neither substring or equality syntax can be parsed + */ + private Filter parseSubstringOrEquality(final String attribute, final CharBuffer cb) + throws FilterParseException { + final Filter filter; + final Map> substrings = readSubstrings(cb); + if (substrings.size() == 0) { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Could not parse equality or substring assertion"); + } + if (substrings.containsKey("EQUALITY")) { + filter = new EqualityFilter( + attribute, + FilterUtils.parseAssertionValue(substrings.get("EQUALITY").get(0).toString())); + } else { + try { + filter = new SubstringFilter( + attribute, + substrings.get("INITIAL") == null ? null : + FilterUtils.parseAssertionValue(substrings.get("INITIAL").get(0).toString()), + substrings.get("FINAL") == null ? null : + FilterUtils.parseAssertionValue(substrings.get("FINAL").get(0).toString()), + substrings.get("ANY") == null ? null : + FilterUtils.parseAssertionValue( + substrings.get("ANY").stream().map(CharBuffer::toString).toArray(String[]::new))); + } catch (IllegalArgumentException e) { + throw new FilterParseException(ResultCode.FILTER_ERROR, e); + } + } + return filter; + } + + + /** + * Reads the supplied buffer and builds a map of the substring data it contains. The following keys are made available + * in the map: + *
    + *
  • INITIAL: singleton list containing the initial substring or null
  • + *
  • ANY: list of any substring components or null
  • + *
  • FINAL: singleton list containing the final substring or null
  • + *
  • EQUALITY: singleton list containing the equality expression or null
  • + *
+ * If the return map contains 'EQUALITY', all other entries will be null and the buffer should be considered an + * equality assertion. + * + * @param cb to read + * @return map of character buffers + */ + private Map> readSubstrings(final CharBuffer cb) { + final Map> buffers = new HashMap<>(); + final int limit = cb.limit(); + cb.mark(); + while (cb.hasRemaining()) { + final char c = cb.get(); + if (c == '*') { + if (cb.position() == 1) { + buffers.put("INITIAL", null); + cb.mark(); + } else { + if (cb.position() == cb.limit()) { + buffers.put("FINAL", null); + } + final int pos = cb.position(); + if (buffers.containsKey("INITIAL")) { + if (!buffers.containsKey("ANY")) { + buffers.put("ANY", new ArrayList<>()); + } + buffers.get("ANY").add(cb.limit(pos - 1).reset().slice()); + } else { + buffers.put("INITIAL", Collections.singletonList(cb.limit(pos - 1).reset().slice())); + } + cb.limit(limit).position(pos); + cb.mark(); + } + } + } + cb.reset(); + if (cb.hasRemaining()) { + if (buffers.size() > 0) { + buffers.put("FINAL", Collections.singletonList(cb.slice())); + } else { + buffers.put("EQUALITY", Collections.singletonList(cb.slice())); + } + } + if (!buffers.containsKey("INITIAL")) { + buffers.put("INITIAL", null); + } + if (!buffers.containsKey("ANY")) { + buffers.put("ANY", null); + } + if (!buffers.containsKey("FINAL")) { + buffers.put("FINAL", null); + } + return buffers; + } + + + /** + * Parses the supplied buffer and creates an extensible filter. + * + * @param attribute attribute description or null + * @param cb to parse + * @return extensible filter + * @throws FilterParseException if the buffer does not contain an extensible expression + */ + private ExtensibleFilter parseExtensible(final String attribute, final CharBuffer cb) + throws FilterParseException { + boolean dnAttrs = false; + CharBuffer remainingFilter = cb.slice(); + CharBuffer matchingRule = sliceAtMatch(remainingFilter, ':'); + if (matchingRule == null) { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Invalid extensible expression, no data after ':'"); + } + if ("dn".equalsIgnoreCase(matchingRule.toString())) { + dnAttrs = true; + matchingRule = null; + if (remainingFilter.hasRemaining()) { + if (remainingFilter.get() != '=') { + remainingFilter = remainingFilter.position(remainingFilter.position() - 1).slice(); + matchingRule = sliceAtMatch(remainingFilter, ':'); + } else { + remainingFilter.position(remainingFilter.position() - 1); + } + } + } + if (remainingFilter.hasRemaining() && remainingFilter.get() != '=') { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Invalid extensible expression"); + } + try { + return new ExtensibleFilter( + matchingRule == null ? null : matchingRule.toString(), + attribute, + FilterUtils.parseAssertionValue(remainingFilter.slice().toString()), + dnAttrs); + } catch (IllegalArgumentException e) { + throw new FilterParseException(ResultCode.FILTER_ERROR, e); + } + } + + + /** + * Returns a new char buffer whose position is 0 and whose limit is before the match character. The supplied buffer + * has its position incremented one position past the match character. + * + * @param cb to search + * @param match to search for + * @return new char buffer or null if there is no match + */ + private CharBuffer sliceAtMatch(final CharBuffer cb, final char match) { + final int limit = cb.limit(); + while (cb.hasRemaining()) { + final char c = cb.get(); + if (c == match) { + final int pos = cb.position(); + cb.position(pos - 1); + final CharBuffer slice = cb.flip().slice(); + cb.limit(limit).position(pos); + return slice; + } + } + return null; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/EqualityFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/EqualityFilter.java new file mode 100644 index 0000000..a4452c4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/EqualityFilter.java @@ -0,0 +1,53 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Equality search filter component. + * + */ +public class EqualityFilter extends AbstractAttributeValueAssertionFilter { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10039; + + + /** + * Creates a new equality filter. + * + * @param name attribute description + * @param value attribute value + */ + public EqualityFilter(final String name, final String value) { + super(Type.EQUALITY, name, LdapUtils.utf8Encode(value, false)); + } + + + /** + * Creates a new equality filter. + * + * @param name attribute description + * @param value attribute value + */ + public EqualityFilter(final String name, final byte[] value) { + super(Type.EQUALITY, name, value); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof EqualityFilter && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, filterType, attributeDesc, assertionValue); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/ExtensibleFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/ExtensibleFilter.java new file mode 100644 index 0000000..cfee743 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/ExtensibleFilter.java @@ -0,0 +1,193 @@ + +package org.xbib.net.ldap.filter; + +import java.util.Objects; +import java.util.stream.Stream; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.BooleanType; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.OctetStringType; + +/** + * Extensible search filter component. + * + */ +public class ExtensibleFilter implements Filter { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10061; + + /** + * Matching rule id. + */ + private final String matchingRuleID; + + /** + * Attribute description. + */ + private final String attributeDesc; + + /** + * Attribute value. + */ + private final byte[] assertionValue; + + /** + * DN attributes. + */ + private final boolean dnAttributes; + + + /** + * Creates a new extensible filter. + * + * @param matchingRule matching rule + * @param type attribute description + * @param value attribute value + */ + public ExtensibleFilter(final String matchingRule, final String type, final String value) { + this(matchingRule, type, LdapUtils.utf8Encode(value, false), false); + } + + + /** + * Creates a new extensible filter. + * + * @param matchingRule matching rule + * @param type attribute description + * @param value attribute value + * @param dnAttrs DN attributes + */ + public ExtensibleFilter(final String matchingRule, final String type, final String value, final boolean dnAttrs) { + this(matchingRule, type, LdapUtils.utf8Encode(value, false), dnAttrs); + } + + + /** + * Creates a new extensible filter. + * + * @param matchingRule matching rule + * @param type attribute description + * @param value attribute value + */ + public ExtensibleFilter(final String matchingRule, final String type, final byte[] value) { + this(matchingRule, type, value, false); + } + + + /** + * Creates a new extensible filter. + * + * @param matchingRule matching rule + * @param type attribute description + * @param value attribute value + * @param dnAttrs DN attributes + */ + public ExtensibleFilter(final String matchingRule, final String type, final byte[] value, final boolean dnAttrs) { + if (matchingRule == null && type == null) { + throw new IllegalArgumentException("Either the matching rule or the type must be specified"); + } + if (value == null) { + throw new IllegalArgumentException("A match value must be specified"); + } + matchingRuleID = matchingRule; + attributeDesc = type; + assertionValue = value; + dnAttributes = dnAttrs; + } + + + /** + * Returns the matching rule id. + * + * @return matching rule id + */ + public String getMatchingRuleID() { + return matchingRuleID; + } + + + /** + * Returns the attribute description. + * + * @return attribute description + */ + public String getAttributeDesc() { + return attributeDesc; + } + + + /** + * Returns the assertion value. + * + * @return assertion value + */ + public byte[] getAssertionValue() { + return assertionValue; + } + + + /** + * Returns whether matching should occur against attributes of the DN. + * + * @return whether to match against DN attributes + */ + public boolean getDnAttributes() { + return dnAttributes; + } + + + @Override + public DEREncoder getEncoder() { + // CheckStyle:MagicNumber OFF + final DEREncoder[] encoders = new DEREncoder[4]; + encoders[0] = matchingRuleID != null ? new OctetStringType(new ContextDERTag(1, false), matchingRuleID) : null; + encoders[1] = attributeDesc != null ? new OctetStringType(new ContextDERTag(2, false), attributeDesc) : null; + encoders[2] = assertionValue != null ? new OctetStringType(new ContextDERTag(3, false), assertionValue) : null; + encoders[3] = dnAttributes ? new BooleanType(new ContextDERTag(4, false), true) : null; + // CheckStyle:MagicNumber ON + return new ConstructedDEREncoder( + new ContextDERTag(Filter.Type.EXTENSIBLE_MATCH.ordinal(), true), + Stream.of(encoders).filter(Objects::nonNull).toArray(DEREncoder[]::new)); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof ExtensibleFilter v) { + return LdapUtils.areEqual(matchingRuleID, v.matchingRuleID) && + LdapUtils.areEqual(attributeDesc, v.attributeDesc) && + LdapUtils.areEqual(assertionValue, v.assertionValue) && + LdapUtils.areEqual(dnAttributes, v.dnAttributes); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode( + HASH_CODE_SEED, + matchingRuleID, + attributeDesc, + assertionValue, + dnAttributes); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "matchingRuleID=" + matchingRuleID + ", " + + "attributeDesc=" + attributeDesc + ", " + + "assertionValue=" + LdapUtils.utf8Encode(assertionValue) + ", " + + "dnAttributes=" + dnAttributes; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/Filter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/Filter.java new file mode 100644 index 0000000..89bb069 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/Filter.java @@ -0,0 +1,105 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.asn1.DEREncoder; + +/** + * LDAP search filter defined as: + * + *
+ * Filter ::= CHOICE {
+ * and             [0] SET SIZE (1..MAX) OF filter Filter,
+ * or              [1] SET SIZE (1..MAX) OF filter Filter,
+ * not             [2] Filter,
+ * equalityMatch   [3] AttributeValueAssertion,
+ * substrings      [4] SubstringFilter,
+ * greaterOrEqual  [5] AttributeValueAssertion,
+ * lessOrEqual     [6] AttributeValueAssertion,
+ * present         [7] AttributeDescription,
+ * approxMatch     [8] AttributeValueAssertion,
+ * extensibleMatch [9] MatchingRuleAssertion,
+ * ...  }
+ *
+ * SubstringFilter ::= SEQUENCE {
+ * type           AttributeDescription,
+ * substrings     SEQUENCE SIZE (1..MAX) OF substring CHOICE {
+ * initial [0] AssertionValue,  -- can occur at most once
+ * any     [1] AssertionValue,
+ * final   [2] AssertionValue } -- can occur at most once
+ * }
+ *
+ * MatchingRuleAssertion ::= SEQUENCE {
+ * matchingRule    [1] MatchingRuleId OPTIONAL,
+ * type            [2] AttributeDescription OPTIONAL,
+ * matchValue      [3] AssertionValue,
+ * dnAttributes    [4] BOOLEAN DEFAULT FALSE }
+ * 
+ * + */ +public interface Filter { + + + /** + * Returns the encoder for this filter. + * + * @return DER encoder + */ + DEREncoder getEncoder(); + + + /** + * Filter type. + */ + enum Type { + + /** + * And filter. + */ + AND, + + /** + * Or filter. + */ + OR, + + /** + * Not filter. + */ + NOT, + + /** + * Equality filter + */ + EQUALITY, + + /** + * Substring filter. + */ + SUBSTRING, + + /** + * Greater or equal filter. + */ + GREATER_OR_EQUAL, + + /** + * Less or equal filter. + */ + LESS_OR_EQUAL, + + /** + * Presence filter. + */ + PRESENCE, + + /** + * Approximate match filter. + */ + APPROXIMATE_MATCH, + + /** + * Extensible match filter. + */ + EXTENSIBLE_MATCH, + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterFunction.java new file mode 100644 index 0000000..ee787e2 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterFunction.java @@ -0,0 +1,20 @@ + +package org.xbib.net.ldap.filter; + +/** + * Marker interface for a filter function. + * + */ +@FunctionalInterface +public interface FilterFunction { + + + /** + * Parses the supplied string representation of a filter. + * + * @param filter to parse + * @return parsed filter + * @throws FilterParseException if the supplied filter is invalid + */ + Filter parse(String filter) throws FilterParseException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterParseException.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterParseException.java new file mode 100644 index 0000000..47511b6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterParseException.java @@ -0,0 +1,51 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ResultCode; + +/** + * Exception that indicates an invalid filter string. + * + */ +public class FilterParseException extends LdapException { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = 2314015446271971772L; + + + /** + * Creates a new filter parse exception. + * + * @param code result code describing this exception + * @param msg describing this exception + */ + public FilterParseException(final ResultCode code, final String msg) { + super(code, msg); + } + + + /** + * Creates a new filter parse exception. + * + * @param code result code describing this exception + * @param e underlying exception + */ + public FilterParseException(final ResultCode code, final Throwable e) { + super(code, e); + } + + + /** + * Creates a new filter parse exception. + * + * @param code result code describing this exception + * @param msg describing this exception + * @param e underlying exception + */ + public FilterParseException(final ResultCode code, final String msg, final Throwable e) { + super(code, msg, e); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterParser.java new file mode 100644 index 0000000..c60760d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterParser.java @@ -0,0 +1,69 @@ + +package org.xbib.net.ldap.filter; + +import java.lang.reflect.Constructor; +import org.xbib.net.ldap.LdapUtils; + +/** + * Encapsulates a {@link FilterFunction} and exposes a convenience static method for parsing filters. The filter + * function used by this class can be set using the system property {@link #FILTER_FUNCTION_PROPERTY}. + * + */ +public final class FilterParser { + + /** + * Ldap filter function system property. + */ + private static final String FILTER_FUNCTION_PROPERTY = "org.xbib.net.ldap.filter.function"; + /** + * Custom filter parser constructor. + */ + private static final Constructor FILTER_FUNCTION_CONSTRUCTOR; + /** + * Default filter function. + */ + private static final FilterFunction FILTER_FUNCTION = getFilterFunction(); + + static { + // Initialize a custom filter function if a system property is found + FILTER_FUNCTION_CONSTRUCTOR = LdapUtils.createConstructorFromProperty(FILTER_FUNCTION_PROPERTY); + } + + + /** + * Default constructor. + */ + private FilterParser() { + } + + + /** + * The {@link #FILTER_FUNCTION_PROPERTY} property is checked and that class is loaded if provided. Otherwise the + * {@link DefaultFilterFunction} is returned. + * + * @return default filter function + */ + public static FilterFunction getFilterFunction() { + if (FILTER_FUNCTION_CONSTRUCTOR != null) { + try { + return (FilterFunction) FILTER_FUNCTION_CONSTRUCTOR.newInstance(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + return new DefaultFilterFunction(); + } + + + /** + * Parse the supplied filter string. + * + * @param filter to parse + * @return search filter + * @throws FilterParseException if filter is invalid + */ + public static Filter parse(final String filter) + throws FilterParseException { + return FILTER_FUNCTION.parse(filter); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterSet.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterSet.java new file mode 100644 index 0000000..74d3550 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterSet.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.filter; + +/** + * Container of search filters. + * + */ +public interface FilterSet extends Filter { + + + /** + * Returns the type of filter set. + * + * @return type of filter set + */ + Type getType(); + + + /** + * Adds a search filter to this set. + * + * @param filter to add + */ + void add(Filter filter); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterUtils.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterUtils.java new file mode 100644 index 0000000..230d6de --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/FilterUtils.java @@ -0,0 +1,123 @@ + +package org.xbib.net.ldap.filter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ResultCode; + +/** + * Provides utility methods for this package. + * + */ +public final class FilterUtils { + + + /** + * Default constructor. + */ + private FilterUtils() { + } + + + /** + * Escapes the supplied string per RFC 4515. + * + * @param s to escape + * @return escaped string + */ + public static String escape(final String s) { + final StringBuilder sb = new StringBuilder(s.length()); + final byte[] utf8 = LdapUtils.utf8Encode(s, false); + // CheckStyle:MagicNumber OFF + // optimize if ASCII + if (s.length() == utf8.length) { + for (byte b : utf8) { + if (b <= 0x1F || b == 0x28 || b == 0x29 || b == 0x2A || b == 0x5C || b == 0x7F) { + sb.append('\\').append(LdapUtils.hexEncode(b)); + } else { + sb.append((char) b); + } + } + } else { + int multiByte = 0; + for (byte b : utf8) { + if (multiByte > 0) { + sb.append('\\').append(LdapUtils.hexEncode(b)); + multiByte--; + } else if ((b & 0x7F) == b) { + if (b <= 0x1F || b == 0x28 || b == 0x29 || b == 0x2A || b == 0x5C || b == 0x7F) { + sb.append('\\').append(LdapUtils.hexEncode(b)); + } else { + sb.append((char) b); + } + } else { + // 2 byte character + if ((b & 0xE0) == 0xC0) { + multiByte = 1; + // 3 byte character + } else if ((b & 0xF0) == 0xE0) { + multiByte = 2; + // 4 byte character + } else if ((b & 0xF8) == 0xF0) { + multiByte = 3; + } else { + throw new IllegalStateException("Could not read UTF-8 string encoding"); + } + sb.append('\\').append(LdapUtils.hexEncode(b)); + } + } + } + // CheckStyle:MagicNumber ON + return sb.toString(); + } + + + /** + * Convenience method for parsing an array of assertion values. See {@link #parseAssertionValue(String)}. + * + * @param value array of assertion values + * @return assertion value bytes + * @throws FilterParseException if the value contains \0, ( or ) + */ + public static byte[][] parseAssertionValue(final String... value) + throws FilterParseException { + final byte[][] bytes = new byte[value.length][]; + for (int i = 0; i < value.length; i++) { + bytes[i] = parseAssertionValue(value[i]); + } + return bytes; + } + + + /** + * Decodes hex characters in the attribute assertion. + * + * @param value to parse + * @return assertion value bytes + * @throws FilterParseException if the value contains \0, ( or ) + */ + public static byte[] parseAssertionValue(final String value) + throws FilterParseException { + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(value.length()); + for (int i = 0; i < value.length(); i++) { + final char c = value.charAt(i); + if (c == '\0' || c == '(' || c == ')') { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Assertion value contains unescaped characters"); + } else if (c == '\\') { + final char[] hexValue = new char[]{value.charAt(++i), value.charAt(++i)}; + try { + bytes.write(LdapUtils.hexDecode(hexValue)); + } catch (IOException e) { + throw new FilterParseException( + ResultCode.FILTER_ERROR, + "Could not hex decode " + Arrays.toString(hexValue) + " in " + value); + } + } else { + bytes.write(c); + } + } + return bytes.toByteArray(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/GreaterOrEqualFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/GreaterOrEqualFilter.java new file mode 100644 index 0000000..868453e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/GreaterOrEqualFilter.java @@ -0,0 +1,57 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Greater or equal search filter component defined as: + * + *
+ * (attributeDescription>=attributeValue)
+ * 
+ * + */ +public class GreaterOrEqualFilter extends AbstractAttributeValueAssertionFilter { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10067; + + + /** + * Creates a new greater or equal filter. + * + * @param name attribute description + * @param value attribute value + */ + public GreaterOrEqualFilter(final String name, final String value) { + super(Type.GREATER_OR_EQUAL, name, LdapUtils.utf8Encode(value, false)); + } + + + /** + * Creates a new greater or equal filter. + * + * @param name attribute description + * @param value attribute value + */ + public GreaterOrEqualFilter(final String name, final byte[] value) { + super(Type.GREATER_OR_EQUAL, name, value); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof GreaterOrEqualFilter && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, filterType, attributeDesc, assertionValue); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/LessOrEqualFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/LessOrEqualFilter.java new file mode 100644 index 0000000..0a977af --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/LessOrEqualFilter.java @@ -0,0 +1,57 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Less or equal search filter component defined as: + * + *
+ * (attributeDescription<=attributeValue)
+ * 
+ * + */ +public class LessOrEqualFilter extends AbstractAttributeValueAssertionFilter { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10069; + + + /** + * Creates a new less or equal filter. + * + * @param name attribute description + * @param value attribute value + */ + public LessOrEqualFilter(final String name, final String value) { + super(Type.LESS_OR_EQUAL, name, LdapUtils.utf8Encode(value, false)); + } + + + /** + * Creates a new less or equal filter. + * + * @param name attribute description + * @param value attribute value + */ + public LessOrEqualFilter(final String name, final byte[] value) { + super(Type.LESS_OR_EQUAL, name, value); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof LessOrEqualFilter && super.equals(o); + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, filterType, attributeDesc, assertionValue); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/NotFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/NotFilter.java new file mode 100644 index 0000000..eb94475 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/NotFilter.java @@ -0,0 +1,102 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; + +/** + * Not search filter set defined as: + * + *
+ * (!(filter))
+ * 
+ * + */ +public class NotFilter implements FilterSet { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10079; + + /** + * Component of this filter. + */ + private Filter filterComponent; + + + /** + * Default constructor. + */ + public NotFilter() { + } + + + /** + * Creates a new not filter. + * + * @param component of this filter + */ + public NotFilter(final Filter component) { + filterComponent = component; + } + + + @Override + public Type getType() { + return Type.NOT; + } + + + @Override + public void add(final Filter component) { + if (filterComponent != null) { + throw new IllegalStateException("Filter component has already been set"); + } + filterComponent = component; + } + + + /** + * Returns the component of this filter. + * + * @return filter component + */ + public Filter getComponent() { + return filterComponent; + } + + + @Override + public DEREncoder getEncoder() { + return new ConstructedDEREncoder( + new ContextDERTag(Type.NOT.ordinal(), true), + filterComponent.getEncoder()); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof NotFilter v) { + return LdapUtils.areEqual(filterComponent, v.filterComponent); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, filterComponent); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::filterComponent=" + filterComponent; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/OrFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/OrFilter.java new file mode 100644 index 0000000..5669a41 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/OrFilter.java @@ -0,0 +1,108 @@ + +package org.xbib.net.ldap.filter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.NullType; + +/** + * Or search filter set defined as: + * + *
+ * (|(filter)(filter)...)
+ * 
+ * + */ +public class OrFilter implements FilterSet { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10091; + + /** + * Components of this filter. + */ + private final List filterComponents = new ArrayList<>(); + + + /** + * Default constructor. + */ + public OrFilter() { + } + + + /** + * Creates a new or filter. + * + * @param components of this filter + */ + public OrFilter(final Filter... components) { + filterComponents.addAll(Arrays.asList(components)); + } + + + @Override + public Type getType() { + return Type.OR; + } + + + @Override + public void add(final Filter component) { + filterComponents.add(component); + } + + + /** + * Returns the components of this filter. + * + * @return filter components + */ + public List getComponents() { + return Collections.unmodifiableList(filterComponents); + } + + + @Override + public DEREncoder getEncoder() { + if (filterComponents.size() == 0) { + return new NullType(new ContextDERTag(Type.OR.ordinal(), true)); + } else { + return new ConstructedDEREncoder( + new ContextDERTag(Type.OR.ordinal(), true), + filterComponents.stream().map(Filter::getEncoder).toArray(DEREncoder[]::new)); + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof OrFilter v) { + return LdapUtils.areEqual(filterComponents, v.filterComponents); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, filterComponents); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::filterComponents=" + filterComponents; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/PresenceFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/PresenceFilter.java new file mode 100644 index 0000000..c7adadd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/PresenceFilter.java @@ -0,0 +1,77 @@ + +package org.xbib.net.ldap.filter; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.OctetStringType; + +/** + * Presence search filter component defined as: + * + *
+ * (attributeDescription=*)
+ * 
+ * + */ +public class PresenceFilter implements Filter { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10093; + + /** + * Attribute description. + */ + private final String attributeDesc; + + + /** + * Creates a new presence filter. + * + * @param name attribute description + */ + public PresenceFilter(final String name) { + attributeDesc = name; + } + + + /** + * Returns the attribute description. + * + * @return attribute description + */ + public String getAttributeDesc() { + return attributeDesc; + } + + + @Override + public DEREncoder getEncoder() { + return new OctetStringType(new ContextDERTag(Type.PRESENCE.ordinal(), false), attributeDesc); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof PresenceFilter v) { + return LdapUtils.areEqual(attributeDesc, v.attributeDesc); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, attributeDesc); + } + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::attributeDesc=" + attributeDesc; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/RegexFilterFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/RegexFilterFunction.java new file mode 100644 index 0000000..2f81384 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/RegexFilterFunction.java @@ -0,0 +1,270 @@ + +package org.xbib.net.ldap.filter; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.ResultCode; + +/** + * Parses an LDAP search filter string using regular expressions. + * + */ +public class RegexFilterFunction extends AbstractFilterFunction { + + /** + * Regular expression that matches an attribute description. + */ + private static final String ATTRIBUTE_DESC = "[\\p{Alnum};\\-\\.]+"; + + /** + * Regular expression that matches an assertion value. + */ + private static final String ASSERTION_VALUE = "([^\\)]*+)"; + + /** + * Regular expression that matches characters that should have been escaped. + */ + private static final Pattern ESCAPE_CHARS_PATTERN = Pattern.compile("[\0\\(\\)]+"); + + /** + * Regex pattern to match a presence filter. + */ + private static final Pattern PRESENCE_FILTER_PATTERN = Pattern.compile("\\((" + ATTRIBUTE_DESC + ")=\\*\\)"); + + /** + * Regex pattern to match an equality filter. + */ + private static final Pattern EQUALITY_FILTER_PATTERN = Pattern.compile("\\((" + ATTRIBUTE_DESC + ")=([^\\*]*)\\)"); + + /** + * Regex pattern to match a substring filter. + */ + private static final Pattern SUBSTRING_FILTER_PATTERN = Pattern.compile( + "\\((" + ATTRIBUTE_DESC + ")=((?:[^\\*]*\\*[^\\*]*)+)\\)"); + + /** + * Regex pattern to match an extensible filter. + */ + private static final Pattern EXTENSIBLE_FILTER_PATTERN = Pattern.compile( + "\\((" + ATTRIBUTE_DESC + ")?(:[Dd][Nn])?(?::(.+))?:=(" + ASSERTION_VALUE + ")\\)"); + + /** + * Regex pattern to match a greater or equal filter. + */ + private static final Pattern GREATER_OR_EQUAL_FILTER_PATTERN = Pattern.compile( + "\\((" + ATTRIBUTE_DESC + ")>=(" + ASSERTION_VALUE + ")\\)"); + + /** + * Regex pattern to match a less or equal filter. + */ + private static final Pattern LESS_OR_EQUAL_FILTER_PATTERN = Pattern.compile( + "\\((" + ATTRIBUTE_DESC + ")<=(" + ASSERTION_VALUE + ")\\)"); + + /** + * Regex pattern to match an approximate filter. + */ + private static final Pattern APPROXIMATE_FILTER_PATTERN = Pattern.compile( + "\\((" + ATTRIBUTE_DESC + ")~=(" + ASSERTION_VALUE + ")\\)"); + + /** + * Creates a new presence filter by parsing the supplied filter string. + * + * @param component to parse + * @return presence filter or null if component doesn't match this filter type + */ + static PresenceFilter parsePresenceFilter(final String component) { + final Matcher m = PRESENCE_FILTER_PATTERN.matcher(component); + if (m.matches()) { + return new PresenceFilter(m.group(1)); + } + return null; + } + + /** + * Creates a new equality filter by parsing the supplied filter string. + * + * @param component to parse + * @return equality filter or null if component doesn't match this filter type + * @throws FilterParseException if the filter is invalid + */ + static EqualityFilter parseEqualityFilter(final String component) + throws FilterParseException { + final Matcher m = EQUALITY_FILTER_PATTERN.matcher(component); + if (m.matches()) { + final String attr = m.group(1); + final String value = m.group(2); + throwOnEscapeChars(value); + return new EqualityFilter(attr, FilterUtils.parseAssertionValue(value)); + } + return null; + } + + /** + * Creates a new substring filter by parsing the supplied filter string. + * + * @param component to parse + * @return substring filter or null if component doesn't match this filter type + * @throws FilterParseException if the filter contains values that should have been escaped + */ + static SubstringFilter parseSubstringFilter(final String component) + throws FilterParseException { + final Matcher m = SUBSTRING_FILTER_PATTERN.matcher(component); + if (m.matches()) { + // don't allow presence match or multiple asterisks + if (!m.group(2).equals("*") && !m.group(2).contains("**")) { + final String attr = m.group(1); + final String assertions = m.group(2); + + String startsWith = null; + final int firstAsterisk = assertions.indexOf('*'); + if (firstAsterisk > 0) { + startsWith = assertions.substring(0, firstAsterisk); + throwOnEscapeChars(startsWith); + } + String endsWith = null; + final int lastAsterisk = assertions.lastIndexOf('*'); + if (lastAsterisk < assertions.length() - 1) { + endsWith = assertions.substring(lastAsterisk + 1); + throwOnEscapeChars(endsWith); + } + String[] contains = null; + if (lastAsterisk > firstAsterisk) { + contains = assertions.substring(firstAsterisk + 1, lastAsterisk).split("\\*"); + throwOnEscapeChars(contains); + } + try { + return new SubstringFilter( + attr, + startsWith != null ? FilterUtils.parseAssertionValue(startsWith) : null, + endsWith != null ? FilterUtils.parseAssertionValue(endsWith) : null, + contains != null ? FilterUtils.parseAssertionValue(contains) : null); + } catch (IllegalArgumentException e) { + throw new FilterParseException(ResultCode.FILTER_ERROR, e); + } + } + } + return null; + } + + /** + * Creates a new extensible filter by parsing the supplied filter string. + * + * @param component to parse + * @return extensible filter or null if component doesn't match this filter type + * @throws FilterParseException if the component cannot be parsed + */ + static ExtensibleFilter parseExtensibleFilter(final String component) + throws FilterParseException { + final Matcher m = EXTENSIBLE_FILTER_PATTERN.matcher(component); + if (m.matches()) { + // CheckStyle:MagicNumber OFF + final String rule = m.group(3); + final String attr = m.group(1); + final String value = m.group(4); + final boolean dn = m.group(2) != null; + try { + return new ExtensibleFilter(rule, attr, FilterUtils.parseAssertionValue(value), dn); + } catch (IllegalArgumentException e) { + throw new FilterParseException(ResultCode.FILTER_ERROR, e); + } + // CheckStyle:MagicNumber ON + } + return null; + } + + /** + * Creates a new greater or equal filter by parsing the supplied filter string. + * + * @param component to parse + * @return greater or equal filter or null if component doesn't match this filter type + * @throws FilterParseException if the component cannot be parsed + */ + static GreaterOrEqualFilter parseGreaterOrEqualFilter(final String component) + throws FilterParseException { + final Matcher m = GREATER_OR_EQUAL_FILTER_PATTERN.matcher(component); + if (m.matches()) { + final String attr = m.group(1); + final String value = m.group(2); + return new GreaterOrEqualFilter(attr, FilterUtils.parseAssertionValue(value)); + } + return null; + } + + /** + * Creates a new less or equal filter by parsing the supplied filter string. + * + * @param component to parse + * @return less or equal filter or null if component doesn't match this filter type + * @throws FilterParseException if the component cannot be parsed + */ + static LessOrEqualFilter parseLessOrEqualFilter(final String component) + throws FilterParseException { + final Matcher m = LESS_OR_EQUAL_FILTER_PATTERN.matcher(component); + if (m.matches()) { + final String attr = m.group(1); + final String value = m.group(2); + return new LessOrEqualFilter(attr, FilterUtils.parseAssertionValue(value)); + } + return null; + } + + /** + * Creates a new approximate filter by parsing the supplied filter string. + * + * @param component to parse + * @return approximate filter or null if component doesn't match this filter type + * @throws FilterParseException if the component cannot be parsed + */ + static ApproximateFilter parseApproximateFilter(final String component) + throws FilterParseException { + final Matcher m = APPROXIMATE_FILTER_PATTERN.matcher(component); + if (m.matches()) { + final String attr = m.group(1); + final String value = m.group(2); + return new ApproximateFilter(attr, FilterUtils.parseAssertionValue(value)); + } + return null; + } + + /** + * Throws an exception if the supplied value matches {@link #ESCAPE_CHARS_PATTERN}. + * + * @param values to check + * @throws FilterParseException if a value contains characters that should be escaped + */ + private static void throwOnEscapeChars(final String... values) + throws FilterParseException { + for (String s : values) { + final Matcher m = ESCAPE_CHARS_PATTERN.matcher(s); + if (m.find()) { + throw new FilterParseException(ResultCode.FILTER_ERROR, "Invalid filter syntax, contains unescaped characters"); + } + } + } + + @Override + protected Filter parseFilterComp(final String filter) + throws FilterParseException { + // note that presence *must* be checked before substring + Filter searchFilter = parsePresenceFilter(filter); + if (searchFilter == null) { + searchFilter = parseEqualityFilter(filter); + } + if (searchFilter == null) { + searchFilter = parseSubstringFilter(filter); + } + if (searchFilter == null) { + searchFilter = parseExtensibleFilter(filter); + } + if (searchFilter == null) { + searchFilter = parseGreaterOrEqualFilter(filter); + } + if (searchFilter == null) { + searchFilter = parseLessOrEqualFilter(filter); + } + if (searchFilter == null) { + searchFilter = parseApproximateFilter(filter); + } + return searchFilter; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/filter/SubstringFilter.java b/net-ldap/src/main/java/org/xbib/net/ldap/filter/SubstringFilter.java new file mode 100644 index 0000000..4e1e6c9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/filter/SubstringFilter.java @@ -0,0 +1,220 @@ + +package org.xbib.net.ldap.filter; + +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.OctetStringType; +import org.xbib.asn1.UniversalDERTag; + +/** + * Substring search filter component defined as: + * + *
+ * (attributeDescription=attributeValueWithWildCard)
+ * 
+ * + */ +public class SubstringFilter implements Filter { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 10099; + /** + * Attribute description. + */ + private final String attributeDesc; + /** + * Substring initial. + */ + private final byte[] subInitial; + /** + * Substring any . + */ + private final byte[][] subAny; + /** + * Substring final. + */ + private final byte[] subFinal; + + /** + * Creates a new substring filter. + * + * @param name attribute description + * @param startsWith substring initial + * @param endsWith substring final + * @param contains substring any + */ + public SubstringFilter(final String name, final String startsWith, final String endsWith, final String... contains) { + if (startsWith == null && endsWith == null && contains == null) { + throw new IllegalArgumentException("Assertion must have one of subInitial, subAny, or subFinal"); + } + attributeDesc = name; + byte[][] containsBytes = null; + if (contains != null) { + containsBytes = new byte[contains.length][]; + for (int i = 0; i < contains.length; i++) { + containsBytes[i] = LdapUtils.utf8Encode(contains[i], false); + } + } + subInitial = LdapUtils.utf8Encode(startsWith); + subAny = containsBytes; + subFinal = LdapUtils.utf8Encode(endsWith); + } + + + /** + * Creates a new substring filter. + * + * @param name attribute description + * @param startsWith substring initial + * @param endsWith substring final + * @param contains substring any + */ + public SubstringFilter(final String name, final byte[] startsWith, final byte[] endsWith, final byte[]... contains) { + if (startsWith == null && endsWith == null && contains == null) { + throw new IllegalArgumentException("Assertion must have one of subInitial, subAny, or subFinal"); + } + attributeDesc = name; + subInitial = startsWith; + subAny = contains; + subFinal = endsWith; + } + + /** + * Returns the attribute description. + * + * @return attribute description + */ + public String getAttributeDesc() { + return attributeDesc; + } + + /** + * Returns the initial substring assertion. + * + * @return initial substring assertion + */ + public byte[] getSubInitial() { + return subInitial; + } + + /** + * Returns the any substring assertion. + * + * @return any substring assertion + */ + public byte[][] getSubAny() { + return subAny; + } + + /** + * Returns the final substring assertion. + * + * @return final substring assertion + */ + public byte[] getSubFinal() { + return subFinal; + } + + /** + * Returns the number of assertions in this substring filter. + * + * @return assertion count + */ + private int getAssertionCount() { + int count = subAny != null ? subAny.length : 0; + if (subInitial != null) { + count++; + } + if (subFinal != null) { + count++; + } + return count; + } + + @Override + public DEREncoder getEncoder() { + final DEREncoder[] encoders = new DEREncoder[getAssertionCount()]; + int i = 0; + if (subInitial != null) { + encoders[i++] = new OctetStringType( + new ContextDERTag(Substrings.INITIAL.ordinal(), false), subInitial); + } + if (subAny != null) { + for (byte[] assertion : subAny) { + encoders[i++] = new OctetStringType(new ContextDERTag(Substrings.ANY.ordinal(), false), assertion); + } + } + if (subFinal != null) { + encoders[i] = new OctetStringType( + new ContextDERTag(Substrings.FINAL.ordinal(), false), subFinal); + } + return new ConstructedDEREncoder( + new ContextDERTag(Filter.Type.SUBSTRING.ordinal(), true), + new OctetStringType(attributeDesc), + new ConstructedDEREncoder( + UniversalDERTag.SEQ, + encoders)); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof SubstringFilter v) { + return LdapUtils.areEqual(attributeDesc, v.attributeDesc) && + LdapUtils.areEqual(subInitial, v.subInitial) && + LdapUtils.areEqual(subAny, v.subAny) && + LdapUtils.areEqual(subFinal, v.subFinal); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode( + HASH_CODE_SEED, + attributeDesc, + subInitial, + subAny, + subFinal); + } + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "attributeDesc=" + attributeDesc + ", " + + "subInitial=" + LdapUtils.utf8Encode(subInitial) + ", " + + "subAny=" + + (subAny == null ? null : Stream.of(subAny).map(LdapUtils::utf8Encode).collect(Collectors.toList())) + ", " + + "subFinal=" + LdapUtils.utf8Encode(subFinal); + } + + + /** + * Type of substring match. + */ + public enum Substrings { + + /** + * Initial substring. + */ + INITIAL, + + /** + * Any substring. + */ + ANY, + + /** + * Final substring. + */ + FINAL, + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/AbstractEntryHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/AbstractEntryHandler.java new file mode 100644 index 0000000..52d9e8a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/AbstractEntryHandler.java @@ -0,0 +1,120 @@ + +package org.xbib.net.ldap.handler; + +import java.util.Set; +import java.util.stream.Collectors; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.transport.MessageFunctional; + +/** + * Base class for entry handlers which simply returns values unaltered. + * + * @param type of object to handle + */ +public abstract class AbstractEntryHandler + extends MessageFunctional.Function { + + /** + * Handle the entry. + * + * @param entry to handle + */ + public void handleEntry(final LdapEntry entry) { + entry.setDn(handleDn(entry)); + handleAttributes(entry); + } + + + /** + * Handle the dn of a search entry. + * + * @param entry search entry to extract the dn from + * @return handled dn + */ + protected String handleDn(final LdapEntry entry) { + return entry.getDn(); + } + + + /** + * Handle the attributes of a search entry. + * + * @param entry search entry to extract the attributes from + */ + protected void handleAttributes(final LdapEntry entry) { + for (LdapAttribute la : entry.getAttributes()) { + handleAttribute(la); + } + } + + + /** + * Handle a single attribute. + * + * @param attr to handle + */ + protected void handleAttribute(final LdapAttribute attr) { + if (attr != null) { + attr.setName(handleAttributeName(attr.getName())); + if (attr.isBinary()) { + final Set newValues = attr.getBinaryValues().stream().map( + this::handleAttributeValue).collect(Collectors.toSet()); + attr.clear(); + attr.addBinaryValues(newValues); + } else { + final Set newValues = attr.getStringValues().stream().map( + this::handleAttributeValue).collect(Collectors.toSet()); + attr.clear(); + attr.addStringValues(newValues); + } + } + } + + + /** + * Returns the supplied attribute name unaltered. + * + * @param name to handle + * @return handled name + */ + protected String handleAttributeName(final String name) { + return name; + } + + + /** + * Returns the supplied attribute value unaltered. + * + * @param value to handle + * @return handled value + */ + protected String handleAttributeValue(final String value) { + return value; + } + + + /** + * Returns the supplied attribute value unaltered. + * + * @param value to handle + * @return handled value + */ + protected byte[] handleAttributeValue(final byte[] value) { + return value; + } + + + // CheckStyle:EqualsHashCode OFF + @Override + public boolean equals(final Object o) { + return super.equals(o); + } + // CheckStyle:EqualsHashCode ON + + + @Override + public abstract int hashCode(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/CaseChangeEntryHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/CaseChangeEntryHandler.java new file mode 100644 index 0000000..a10024b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/CaseChangeEntryHandler.java @@ -0,0 +1,224 @@ + +package org.xbib.net.ldap.handler; + +import java.util.Arrays; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides the ability to modify the case of search entry DNs, attribute names, and attribute values. + * + */ +public class CaseChangeEntryHandler extends AbstractEntryHandler implements LdapEntryHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 821; + /** + * Type of case modification to make to the entry DN. + */ + private CaseChange dnCaseChange = CaseChange.NONE; + /** + * Type of case modification to make to the attribute names. + */ + private CaseChange attributeNameCaseChange = CaseChange.NONE; + /** + * Type of case modification to make to the attributes values. + */ + private CaseChange attributeValueCaseChange = CaseChange.NONE; + /** + * Attribute names to modify. + */ + private String[] attributeNames; + + /** + * Returns the DN case change. + * + * @return case change + */ + public CaseChange getDnCaseChange() { + return dnCaseChange; + } + + /** + * Sets the DN case change. + * + * @param cc case change + */ + public void setDnCaseChange(final CaseChange cc) { + dnCaseChange = cc; + } + + /** + * Returns the attribute name case change. + * + * @return case change + */ + public CaseChange getAttributeNameCaseChange() { + return attributeNameCaseChange; + } + + /** + * Sets the attribute name case change. + * + * @param cc case change + */ + public void setAttributeNameCaseChange(final CaseChange cc) { + attributeNameCaseChange = cc; + } + + /** + * Returns the attribute value case change. + * + * @return case change + */ + public CaseChange getAttributeValueCaseChange() { + return attributeValueCaseChange; + } + + /** + * Sets the attribute value case change. + * + * @param cc case change + */ + public void setAttributeValueCaseChange(final CaseChange cc) { + attributeValueCaseChange = cc; + } + + /** + * Returns the attribute names. + * + * @return attribute names + */ + public String[] getAttributeNames() { + return attributeNames; + } + + /** + * Sets the attribute names. + * + * @param names of the attributes + */ + public void setAttributeNames(final String... names) { + attributeNames = names; + } + + @Override + public LdapEntry apply(final LdapEntry entry) { + handleEntry(entry); + return entry; + } + + @Override + protected String handleDn(final LdapEntry entry) { + return CaseChange.perform(dnCaseChange, entry.getDn()); + } + + @Override + protected void handleAttributes(final LdapEntry entry) { + if (attributeNames == null) { + super.handleAttributes(entry); + } else { + for (String s : attributeNames) { + final LdapAttribute la = entry.getAttribute(s); + if (la != null) { + handleAttribute(la); + } + } + } + } + + @Override + protected String handleAttributeName(final String name) { + return CaseChange.perform(attributeNameCaseChange, name); + } + + @Override + protected String handleAttributeValue(final String value) { + return CaseChange.perform(attributeValueCaseChange, value); + } + + @Override + protected byte[] handleAttributeValue(final byte[] value) { + return value; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof CaseChangeEntryHandler v) { + return LdapUtils.areEqual(dnCaseChange, v.dnCaseChange) && + LdapUtils.areEqual(attributeNameCaseChange, v.attributeNameCaseChange) && + LdapUtils.areEqual(attributeValueCaseChange, v.attributeValueCaseChange) && + LdapUtils.areEqual(attributeNames, v.attributeNames); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + dnCaseChange, + attributeNameCaseChange, + attributeValueCaseChange, + attributeNames); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "dnCaseChange=" + dnCaseChange + ", " + + "attributeNameCaseChange=" + attributeNameCaseChange + ", " + + "attributeValueCaseChange=" + attributeValueCaseChange + ", " + + "attributeNames=" + Arrays.toString(attributeNames) + "]"; + } + + + /** + * Enum to define the type of case change. + */ + public enum CaseChange { + + /** + * no case change. + */ + NONE, + + /** + * lower case. + */ + LOWER, + + /** + * upper case. + */ + UPPER; + + + /** + * This changes the supplied string based on the supplied case change. + * + * @param cc case change to perform + * @param string to modify + * @return string that has been changed + */ + public static String perform(final CaseChange cc, final String string) { + String s = null; + if (CaseChange.LOWER == cc) { + s = string.toLowerCase(); + } else if (CaseChange.UPPER == cc) { + s = string.toUpperCase(); + } else if (CaseChange.NONE == cc) { + s = string; + } + return s; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/CompareValueHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/CompareValueHandler.java new file mode 100644 index 0000000..74755d9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/CompareValueHandler.java @@ -0,0 +1,11 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Consumer; + +/** + * Marker interface for a compare result handler. + * + */ +public interface CompareValueHandler extends Consumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/CompleteHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/CompleteHandler.java new file mode 100644 index 0000000..cf17ff9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/CompleteHandler.java @@ -0,0 +1,16 @@ + +package org.xbib.net.ldap.handler; + +/** + * Marker interface for a complete handler. + * + */ +@FunctionalInterface +public interface CompleteHandler { + + + /** + * Method to execute on completion. + */ + void execute(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/DnAttributeEntryHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/DnAttributeEntryHandler.java new file mode 100644 index 0000000..46e91bd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/DnAttributeEntryHandler.java @@ -0,0 +1,113 @@ + +package org.xbib.net.ldap.handler; + +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; + +/** + * Adds the entry DN as an attribute to the result set. Provides a client side implementation of RFC 5020. + * + */ +public class DnAttributeEntryHandler extends AbstractEntryHandler implements LdapEntryHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 823; + + /** + * Attribute name for the entry dn. + */ + private String dnAttributeName = "entryDN"; + + /** + * Whether to add the entry dn if an attribute of the same name exists. + */ + private boolean addIfExists; + + + /** + * Returns the DN attribute name. + * + * @return DN attribute name + */ + public String getDnAttributeName() { + return dnAttributeName; + } + + + /** + * Sets the DN attribute name. + * + * @param name of the DN attribute + */ + public void setDnAttributeName(final String name) { + dnAttributeName = name; + } + + + /** + * Returns whether to add the entryDN if an attribute of the same name exists. + * + * @return whether to add the entryDN if an attribute of the same name exists + */ + public boolean isAddIfExists() { + return addIfExists; + } + + + /** + * Sets whether to add the entryDN if an attribute of the same name exists. + * + * @param b whether to add the entryDN if an attribute of the same name exists + */ + public void setAddIfExists(final boolean b) { + addIfExists = b; + } + + + @Override + public LdapEntry apply(final LdapEntry entry) { + handleEntry(entry); + return entry; + } + + + @Override + protected void handleAttributes(final LdapEntry entry) { + if (entry.getAttribute(dnAttributeName) == null) { + entry.addAttributes(new LdapAttribute(dnAttributeName, entry.getDn())); + } else if (addIfExists) { + entry.getAttribute(dnAttributeName).addStringValues(entry.getDn()); + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof DnAttributeEntryHandler v) { + return LdapUtils.areEqual(addIfExists, v.addIfExists) && + LdapUtils.areEqual(dnAttributeName, v.dnAttributeName); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, addIfExists, dnAttributeName); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "dnAttributeName=" + dnAttributeName + ", " + + "addIfExists=" + addIfExists + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/ExceptionHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ExceptionHandler.java new file mode 100644 index 0000000..7783559 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ExceptionHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Consumer; +import org.xbib.net.ldap.LdapException; + +/** + * Marker interface for an LDAP exception handler. + * + */ +public interface ExceptionHandler extends Consumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/ExtendedValueHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ExtendedValueHandler.java new file mode 100644 index 0000000..fe5731d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ExtendedValueHandler.java @@ -0,0 +1,11 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.BiConsumer; + +/** + * Marker interface for an extended result handler. + * + */ +public interface ExtendedValueHandler extends BiConsumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/IntermediateResponseHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/IntermediateResponseHandler.java new file mode 100644 index 0000000..8601547 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/IntermediateResponseHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Consumer; +import org.xbib.net.ldap.extended.IntermediateResponse; + +/** + * Marker interface for an intermediate response handler. + * + */ +public interface IntermediateResponseHandler extends Consumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/LdapEntryHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/LdapEntryHandler.java new file mode 100644 index 0000000..7c0f34d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/LdapEntryHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Function; +import org.xbib.net.ldap.LdapEntry; + +/** + * Marker interface for an ldap entry handler. + * + */ +public interface LdapEntryHandler extends Function { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/MergeAttributeEntryHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/MergeAttributeEntryHandler.java new file mode 100644 index 0000000..1ffb45e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/MergeAttributeEntryHandler.java @@ -0,0 +1,131 @@ + +package org.xbib.net.ldap.handler; + +import java.util.Arrays; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; + +/** + * Merges the values of one or more attributes into a single attribute. The merged attribute may or may not already + * exist on the entry. If it does exist its existing values will remain intact. + * + */ +public class MergeAttributeEntryHandler extends AbstractEntryHandler implements LdapEntryHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 827; + + /** + * Attribute name to add merge values into. + */ + private String mergeAttributeName; + + /** + * Attribute names to read values from. + */ + private String[] attributeNames; + + + /** + * Returns the merge attribute name. + * + * @return merge attribute name + */ + public String getMergeAttributeName() { + return mergeAttributeName; + } + + + /** + * Sets the merge attribute name. + * + * @param name of the merge attribute + */ + public void setMergeAttributeName(final String name) { + mergeAttributeName = name; + } + + + /** + * Returns the attribute names. + * + * @return attribute names + */ + public String[] getAttributeNames() { + return attributeNames; + } + + + /** + * Sets the attribute names. + * + * @param names of the attributes + */ + public void setAttributeNames(final String... names) { + attributeNames = names; + } + + + @Override + public LdapEntry apply(final LdapEntry entry) { + handleEntry(entry); + return entry; + } + + + @Override + protected void handleAttributes(final LdapEntry entry) { + boolean newAttribute = false; + LdapAttribute mergedAttribute = entry.getAttribute(mergeAttributeName); + if (mergedAttribute == null) { + mergedAttribute = new LdapAttribute(mergeAttributeName); + newAttribute = true; + } + for (String s : attributeNames) { + final LdapAttribute la = entry.getAttribute(s); + if (la != null) { + if (la.isBinary()) { + mergedAttribute.addBinaryValues(la.getBinaryValues()); + } else { + mergedAttribute.addStringValues(la.getStringValues()); + } + } + } + + if (mergedAttribute.size() > 0 && newAttribute) { + entry.addAttributes(mergedAttribute); + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof MergeAttributeEntryHandler) { + final MergeAttributeEntryHandler v = (MergeAttributeEntryHandler) o; + return LdapUtils.areEqual(attributeNames, v.attributeNames) && + LdapUtils.areEqual(mergeAttributeName, v.mergeAttributeName); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, attributeNames, mergeAttributeName); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "mergeAttributeName=" + mergeAttributeName + ", " + + "attributeNames=" + Arrays.toString(attributeNames) + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/MergeResultHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/MergeResultHandler.java new file mode 100644 index 0000000..01766f8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/MergeResultHandler.java @@ -0,0 +1,52 @@ + +package org.xbib.net.ldap.handler; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchResponse; + +/** + * Merges the values of the attributes in all entries into a single entry. + * + * @author Miguel Martinez de Espronceda + */ +public class MergeResultHandler extends AbstractEntryHandler implements SearchResultHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 857; + + + /** + * Default constructor. + */ + public MergeResultHandler() { + } + + + @Override + public SearchResponse apply(final SearchResponse searchResponse) { + return SearchResponse.merge(searchResponse); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof MergeResultHandler; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED); + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/RecursiveResultHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/RecursiveResultHandler.java new file mode 100644 index 0000000..412c9aa --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/RecursiveResultHandler.java @@ -0,0 +1,265 @@ + +package org.xbib.net.ldap.handler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; + +/** + * This recursively searches based on a supplied attribute and merges those results into the original entry. For the + * following LDIF: + * + *
+ * dn: uugid=group1,ou=groups,dc=xbib,dc=org
+ * uugid: group1
+ * member: uugid=group2,ou=groups,dc=xbib,dc=org
+ *
+ * dn: uugid=group2,ou=groups,dc=xbib,dc=org
+ * uugid: group2
+ * 
+ * + *

With the following code:

+ * + *
+ * RecursiveResultHandler reh = new RecursiveResultHandler("member", "uugid");
+ * 
+ * + *

Will produce this result for the query (uugid=group1):

+ * + *
+ * dn: uugid=group1,ou=groups,dc=xbib,dc=org
+ * uugid: group1
+ * uugid: group2
+ * member: uugid=group2,ou=groups,dc=xbib,dc=org
+ * 
+ *

+ * This handler should only be used with the {@link org.xbib.net.ldap.SearchOperation#execute()} method since it leverages + * the connection to make further searches. + * + */ +public class RecursiveResultHandler extends AbstractEntryHandler implements SearchResultHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 829; + + /** + * Attribute to recursively search on. + */ + private String searchAttribute; + + /** + * Attribute(s) to merge. + */ + private String[] mergeAttributes; + + /** + * Attributes to return when searching, mergeAttributes + searchAttribute. + */ + private String[] retAttrs; + + + /** + * Default constructor. + */ + public RecursiveResultHandler() { + } + + + /** + * Creates a new recursive entry handler. + * + * @param searchAttr attribute to search on + * @param mergeAttrs attribute names to merge + */ + public RecursiveResultHandler(final String searchAttr, final String... mergeAttrs) { + searchAttribute = searchAttr; + mergeAttributes = mergeAttrs; + initializeReturnAttributes(); + } + + + /** + * Returns the attribute name that will be recursively searched on. + * + * @return attribute name + */ + public String getSearchAttribute() { + return searchAttribute; + } + + + /** + * Sets the attribute name that will be recursively searched on. + * + * @param name of the search attribute + */ + public void setSearchAttribute(final String name) { + searchAttribute = name; + initializeReturnAttributes(); + } + + + /** + * Returns the attribute names that will be merged by the recursive search. + * + * @return attribute names + */ + public String[] getMergeAttributes() { + return mergeAttributes; + } + + + /** + * Sets the attribute name that will be merged by the recursive search. + * + * @param mergeAttrs attribute names to merge + */ + public void setMergeAttributes(final String... mergeAttrs) { + mergeAttributes = mergeAttrs; + initializeReturnAttributes(); + } + + + /** + * Initializes the return attributes array. Must be called after both searchAttribute and mergeAttributes have been + * set. + */ + protected void initializeReturnAttributes() { + if (mergeAttributes != null && searchAttribute != null) { + // return attributes must include the search attribute + retAttrs = new String[mergeAttributes.length + 1]; + System.arraycopy(mergeAttributes, 0, retAttrs, 0, mergeAttributes.length); + retAttrs[retAttrs.length - 1] = searchAttribute; + } + } + + + @Override + public SearchResponse apply(final SearchResponse response) { + response.getEntries().forEach(this::handleEntry); + return response; + } + + + @Override + public void handleEntry(final LdapEntry entry) { + // Recursively searches a list of attributes and merges those results with + // the existing entry. + final List searchedDns = new ArrayList<>(); + if (entry.getAttribute(searchAttribute) != null) { + searchedDns.add(entry.getDn()); + readSearchAttribute(entry, searchedDns); + } else { + recursiveSearch(entry.getDn(), entry, searchedDns); + } + } + + + /** + * Reads the values of {@link #searchAttribute} from the supplied attributes and calls {@link #recursiveSearch} for + * each. + * + * @param entry to read + * @param searchedDns list of DNs whose attributes have been read + */ + private void readSearchAttribute(final LdapEntry entry, final List searchedDns) { + if (entry != null) { + final LdapAttribute attr = entry.getAttribute(searchAttribute); + if (attr != null && !attr.isBinary()) { + final Set values = new HashSet<>(attr.getStringValues()); + for (String s : values) { + recursiveSearch(s, entry, searchedDns); + } + } + } + } + + + /** + * Recursively gets the attribute(s) {@link #mergeAttributes} for the supplied dn and adds the values to the supplied + * attributes. + * + * @param dn to get attribute(s) for + * @param entry to merge with + * @param searchedDns list of DNs that have been searched for + */ + private void recursiveSearch(final String dn, final LdapEntry entry, final List searchedDns) { + if (!searchedDns.contains(dn)) { + + LdapEntry newEntry = null; + try { + final SearchResponse result = getConnection().operation( + SearchRequest.objectScopeSearchRequest(dn, retAttrs)).execute(); + if (result.isSuccess()) { + newEntry = result.getEntry(); + } + } catch (LdapException e) { + // + } + searchedDns.add(dn); + + if (newEntry != null) { + // recursively search new attributes + readSearchAttribute(newEntry, searchedDns); + + // merge new attribute values + for (String s : mergeAttributes) { + final LdapAttribute newAttr = newEntry.getAttribute(s); + if (newAttr != null) { + final LdapAttribute oldAttr = entry.getAttribute(s); + if (oldAttr == null) { + entry.addAttributes(newAttr); + } else { + if (newAttr.isBinary()) { + newAttr.getBinaryValues().forEach(oldAttr::addBinaryValues); + } else { + newAttr.getStringValues().forEach(oldAttr::addStringValues); + } + } + } + } + } + } + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof RecursiveResultHandler) { + final RecursiveResultHandler v = (RecursiveResultHandler) o; + return LdapUtils.areEqual(mergeAttributes, v.mergeAttributes) && + LdapUtils.areEqual(retAttrs, v.retAttrs) && + LdapUtils.areEqual(searchAttribute, v.searchAttribute); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, mergeAttributes, retAttrs, searchAttribute); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "searchAttribute=" + searchAttribute + ", " + + "mergeAttributes=" + Arrays.toString(mergeAttributes) + ", " + + "retAttrs=" + Arrays.toString(retAttrs) + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/ReferralHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ReferralHandler.java new file mode 100644 index 0000000..bb20147 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ReferralHandler.java @@ -0,0 +1,11 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Consumer; + +/** + * Marker interface for a referral handler. + * + */ +public interface ReferralHandler extends Consumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/RequestHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/RequestHandler.java new file mode 100644 index 0000000..12a2f96 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/RequestHandler.java @@ -0,0 +1,13 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Function; +import org.xbib.net.ldap.Request; + +/** + * Marker interface for a request handler. + * + * @param type of request + */ +public interface RequestHandler extends Function { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResponseControlHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResponseControlHandler.java new file mode 100644 index 0000000..b20b507 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResponseControlHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Consumer; +import org.xbib.net.ldap.control.ResponseControl; + +/** + * Marker interface for a response control handler. + * + */ +public interface ResponseControlHandler extends Consumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResultHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResultHandler.java new file mode 100644 index 0000000..79fba5d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResultHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Consumer; +import org.xbib.net.ldap.Result; + +/** + * Marker interface for a result handler. + * + */ +public interface ResultHandler extends Consumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResultPredicate.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResultPredicate.java new file mode 100644 index 0000000..70ab104 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/ResultPredicate.java @@ -0,0 +1,37 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Predicate; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.ResultCode; + +/** + * Marker interface for a throw predicate. + * + */ +@FunctionalInterface +public interface ResultPredicate extends Predicate { + + /** + * Predicate that throws if the result code is not {@link ResultCode#SUCCESS}. + */ + ResultPredicate NOT_SUCCESS = result -> !result.isSuccess(); + + + /** + * Test a result and throw if the test succeeds. + * + * @param result input argument + * @throws LdapException if {@link #test(Object)} returns true + */ + default void testAndThrow(final Result result) + throws LdapException { + if (test(result)) { + if (result == null) { + throw new LdapException("Predicate failed for null result"); + } + throw new LdapException(result); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/SearchReferenceHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/SearchReferenceHandler.java new file mode 100644 index 0000000..e3a27e8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/SearchReferenceHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Consumer; +import org.xbib.net.ldap.SearchResultReference; + +/** + * Marker interface for a search reference handler. + * + */ +public interface SearchReferenceHandler extends Consumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/SearchResultHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/SearchResultHandler.java new file mode 100644 index 0000000..65489b7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/SearchResultHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Function; +import org.xbib.net.ldap.SearchResponse; + +/** + * Marker interface for a search result handler. + * + */ +public interface SearchResultHandler extends Function { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/SortResultHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/SortResultHandler.java new file mode 100644 index 0000000..07cf03d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/SortResultHandler.java @@ -0,0 +1,51 @@ + +package org.xbib.net.ldap.handler; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchResponse; + +/** + * Sorts the entries, attributes, and attribute values contained in a search response. + * + */ +public class SortResultHandler extends AbstractEntryHandler implements SearchResultHandler { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 853; + + + /** + * Default constructor. + */ + public SortResultHandler() { + } + + + @Override + public SearchResponse apply(final SearchResponse response) { + return SearchResponse.sort(response); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + return o instanceof SortResultHandler; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED); + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/handler/UnsolicitedNotificationHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/handler/UnsolicitedNotificationHandler.java new file mode 100644 index 0000000..51f7f20 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/handler/UnsolicitedNotificationHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.handler; + +import java.util.function.Consumer; +import org.xbib.net.ldap.extended.UnsolicitedNotification; + +/** + * Marker interface for an intermediate response handler. + * + */ +public interface UnsolicitedNotificationHandler extends Consumer { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/ClasspathResourceLoader.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/ClasspathResourceLoader.java new file mode 100644 index 0000000..c377d01 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/ClasspathResourceLoader.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.io; + +import java.io.InputStream; +import org.xbib.net.ldap.LdapUtils; + +/** + * Creates an {@link InputStream} from a string that is prefixed with 'classpath:'. See {@link + * Class#getResourceAsStream(String)}. + * + */ +public class ClasspathResourceLoader implements ResourceLoader { + + /** + * Prefix used to indicate a classpath resource. + */ + private static final String PREFIX = "classpath:"; + + + @Override + public boolean supports(final String path) { + return path != null && path.startsWith(PREFIX); + } + + + @Override + public InputStream load(final String path) { + if (!supports(path)) { + throw new IllegalArgumentException("Path '" + path + "' must start with " + PREFIX); + } + // load the resource using a class in the base package + final InputStream is = LdapUtils.class.getResourceAsStream(path.substring(PREFIX.length())); + if (is == null) { + throw new NullPointerException("Could not get stream from '" + path + "' classpath"); + } + return is; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/FileResourceLoader.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/FileResourceLoader.java new file mode 100644 index 0000000..cdd655f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/FileResourceLoader.java @@ -0,0 +1,36 @@ + +package org.xbib.net.ldap.io; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Creates an {@link InputStream} from a string that is prefixed with 'file:'. See {@link + * FileInputStream#FileInputStream(File)}. + * + */ +public class FileResourceLoader implements ResourceLoader { + + /** + * Prefix used to indicate a file resource. + */ + private static final String PREFIX = "file:"; + + + @Override + public boolean supports(final String path) { + return path != null && path.startsWith(PREFIX); + } + + + @Override + public InputStream load(final String path) + throws IOException { + if (!supports(path)) { + throw new IllegalArgumentException("Path '" + path + "' must start with " + PREFIX); + } + return new FileInputStream(path.substring(PREFIX.length())); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/Hex.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/Hex.java new file mode 100644 index 0000000..048b319 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/Hex.java @@ -0,0 +1,146 @@ + +package org.xbib.net.ldap.io; + +import java.util.Arrays; + +/** + * Utility for hexidecimal encoding and decoding. + * + */ +public final class Hex { + + /** + * Hexidecimal characters. + */ + private static final char[] HEX_CHARS = { + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + }; + + /** + * Decode table which stores characters from 0 to f. Anything higher than 'f' is invalid so that is the max size of + * the array. + */ + private static final byte[] DECODE = new byte['f' + 1]; + + // Initialize the DECODE table + // CheckStyle:MagicNumber OFF + static { + // set all values to -1 to indicate error + Arrays.fill(DECODE, (byte) -1); + // set values for hex 0-9 + for (int i = '0'; i <= '9'; i++) { + DECODE[i] = (byte) (i - '0'); + } + // set values for hex A-F + for (int i = 'A'; i <= 'F'; i++) { + DECODE[i] = (byte) (i - 'A' + 10); + } + // set values for hex a-f + for (int i = 'a'; i <= 'f'; i++) { + DECODE[i] = (byte) (i - 'a' + 10); + } + } + // CheckStyle:MagicNumber ON + + + /** + * Default constructor. + */ + private Hex() { + } + + + /** + * This will convert the supplied value to a hex encoded string. Returns null if the supplied value is null. + * + * @param value to hex encode + * @return hex encoded value + */ + public static char[] encode(final byte... value) { + if (value == null) { + return null; + } + + final int l = value.length; + final char[] encoded = new char[l << 1]; + // CheckStyle:MagicNumber OFF + for (int i = 0, j = 0; i < l; i++) { + encoded[j++] = HEX_CHARS[(0xF0 & value[i]) >>> 4]; + encoded[j++] = HEX_CHARS[0x0F & value[i]]; + } + // CheckStyle:MagicNumber ON + return encoded; + } + + + /** + * This will convert the supplied value from a hex encoded string. Returns null if the supplied value is null. + * + * @param value to hex decode + * @return hex decoded value + * @throws IllegalArgumentException if value is not valid hexidecimal + */ + public static byte[] decode(final char... value) { + if (value == null) { + return null; + } + + final int l = value.length; + // CheckStyle:MagicNumber OFF + if ((l & 0x01) != 0) { + throw new IllegalArgumentException( + String.format("Cannot decode odd number of characters for %s", String.valueOf(value))); + } + // CheckStyle:MagicNumber ON + + final byte[] decoded = new byte[l >> 1]; + + // CheckStyle:MagicNumber OFF + for (int i = 0, j = 0; j < l; i++, j += 2) { + final int high = decode(value, j) << 4; + final int low = decode(value, j + 1); + decoded[i] = (byte) ((high | low) & 0xFF); + } + // CheckStyle:MagicNumber ON + return decoded; + } + + + /** + * Decodes the supplied character to its corresponding nibble. + * + * @param hex to read character from + * @param i index of hex to read + * @return 0-15 integer + * @throws IllegalArgumentException if the character is not valid hex + */ + private static int decode(final char[] hex, final int i) { + final char c = hex[i]; + if (c > 'f') { + throw new IllegalArgumentException( + String.format("Invalid hex character '%s' at position %s in %s", c, i, Arrays.toString(hex))); + } + + final byte b = DECODE[c]; + if (b < 0) { + throw new IllegalArgumentException( + String.format("Invalid hex character '%s' at position %s in %s", c, i, Arrays.toString(hex))); + } + return b; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/LdifReader.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/LdifReader.java new file mode 100644 index 0000000..2e739a5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/LdifReader.java @@ -0,0 +1,213 @@ + +package org.xbib.net.ldap.io; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.SearchResultReference; + +/** + * Reads an LDIF from a {@link Reader} and returns a {@link SearchResponse}. This implementation only supports entry + * records. It does not support change records or include statements. + * + */ +public class LdifReader implements SearchResultReader { + + /** + * Mark read back buffer size. + */ + private static final int READ_AHEAD_LIMIT = 1024; + + + /** + * Reader to read from. + */ + private final Reader ldifReader; + + + /** + * Creates a new ldif reader. + * + * @param reader to read LDIF from + */ + public LdifReader(final Reader reader) { + ldifReader = reader; + } + + + /** + * Reads LDIF data from the reader and returns a search result. + * + * @return search result derived from the LDIF + * @throws IOException if an error occurs using the reader + */ + @Override + public SearchResponse read() + throws IOException { + final SearchResponse result = new SearchResponse(); + final BufferedReader br = new BufferedReader(ldifReader); + String line; + br.mark(READ_AHEAD_LIMIT); + while ((line = br.readLine()) != null) { + if (!"".equals(line)) { + br.reset(); + final List section = readSection(br); + if (!section.isEmpty()) { + if (section.get(0).startsWith("version")) { + section.remove(0); + } + if (!section.isEmpty()) { + if (section.get(0).startsWith("dn")) { + result.addEntries(parseEntry(section)); + } else if (section.get(0).startsWith("ref")) { + result.addReferences(parseReference(section)); + } + } + } + } + br.mark(READ_AHEAD_LIMIT); + } + return result; + } + + + /** + * Reads the supplied reader line-by-line until the reader is empty or a empty line is encountered. Lines containing + * comments are ignored. + * + * @param reader to read + * @return list of a lines in the section + * @throws IOException if an error occurs reading + */ + private List readSection(final BufferedReader reader) + throws IOException { + final List section = new ArrayList<>(); + boolean readingComment = false; + String line; + while ((line = reader.readLine()) != null) { + if ("".equals(line)) { + // end of section + break; + } else if (line.startsWith("#")) { + readingComment = true; + } else if (line.startsWith(" ")) { + if (!readingComment) { + section.add(section.remove(section.size() - 1) + line.substring(1)); + } + } else { + readingComment = false; + section.add(line); + } + } + return section; + } + + + /** + * Parses the supplied array of LDIF lines and returns an LDAP entry. + * + * @param section of LDIF lines + * @return ldap entry + * @throws IOException if an errors occurs reading a URI in the LDIF + */ + private LdapEntry parseEntry(final List section) + throws IOException { + final LdapEntry entry = new LdapEntry(); + for (String line : section) { + if (!line.contains(":")) { + throw new IllegalArgumentException("Invalid LDAP entry data: " + line); + } + final LdapAttribute newAttr = parseAttribute(line); + if ("dn".equals(newAttr.getName())) { + entry.setDn(newAttr.getStringValue()); + } else { + final LdapAttribute existingAttr = entry.getAttribute(newAttr.getName()); + if (existingAttr == null) { + entry.addAttributes(newAttr); + } else { + if (existingAttr.isBinary()) { + existingAttr.addBinaryValues(newAttr.getBinaryValue()); + } else { + existingAttr.addStringValues(newAttr.getStringValue()); + } + } + } + } + return entry; + } + + + /** + * Parses the supplied line and returns an attribute with a single value found in the line. + * + * @param line to parse + * @return ldap attribute + * @throws IOException if an errors occurs reading a URI in the LDIF + */ + private LdapAttribute parseAttribute(final String line) + throws IOException { + boolean isBase64 = false; + boolean isUrl = false; + final String[] parts = line.split(":", 2); + final String attrName = parts[0]; + String attrValue = parts[1]; + if (attrValue.startsWith(":")) { + isBase64 = true; + attrValue = attrValue.substring(1); + } else if (attrValue.startsWith("<")) { + isUrl = true; + attrValue = attrValue.substring(1); + } + // remove leading whitespace + while (attrValue.startsWith(" ")) { + attrValue = attrValue.substring(1); + } + final LdapAttribute attr = new LdapAttribute(attrName); + if (isBase64) { + attr.addBinaryValues(LdapUtils.base64Decode(attrValue)); + } else if (isUrl) { + final byte[] b; + if (ResourceUtils.isResource(attrValue)) { + b = ResourceUtils.readResource(attrValue); + } else { + b = ResourceUtils.readResource(attrValue, new URLResourceLoader()); + } + attr.addBinaryValues(b); + } else { + attr.addStringValues(attrValue); + } + return attr; + } + + + /** + * Parses the supplied array of LDIF lines and returns a search reference. + * + * @param section of LDIF lines + * @return search reference + */ + private SearchResultReference parseReference(final List section) { + final SearchResultReference ref = new SearchResultReference(); + for (String line : section) { + if (!line.contains(":")) { + throw new IllegalArgumentException("Invalid LDAP reference data: " + line); + } + final String[] parts = line.split(":", 2); + if (!"ref".equals(parts[0])) { + throw new IllegalArgumentException("Invalid LDAP reference data: " + line); + } + if (parts[1].startsWith(" ")) { + ref.addUris(parts[1].substring(1)); + } else if (parts[1].length() > 0) { + throw new IllegalArgumentException("Invalid LDAP reference data: " + line); + } + } + return ref; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/LdifWriter.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/LdifWriter.java new file mode 100644 index 0000000..5746e8c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/LdifWriter.java @@ -0,0 +1,148 @@ + +package org.xbib.net.ldap.io; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.SearchResultReference; + +/** + * Writes a {@link SearchResponse} as LDIF to a {@link Writer}. + * + */ +public class LdifWriter implements SearchResultWriter { + + /** + * Line separator. + */ + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + /** + * Writer to write to. + */ + private final Writer ldifWriter; + + + /** + * Creates a new ldif writer. + * + * @param writer to write LDIF to + */ + public LdifWriter(final Writer writer) { + ldifWriter = writer; + } + + + /** + * Writes the supplied search result to the writer. + * + * @param result search result to write + * @throws IOException if an error occurs using the writer + */ + @Override + public void write(final SearchResponse result) + throws IOException { + ldifWriter.write(createLdif(result)); + ldifWriter.flush(); + } + + + /** + * Creates an LDIF using the supplied search result. + * + * @param result search result + * @return LDIF + */ + protected String createLdif(final SearchResponse result) { + // build string from results + final StringBuilder ldif = new StringBuilder(); + if (result != null) { + for (LdapEntry le : result.getEntries()) { + ldif.append(createLdifEntry(le)); + } + for (SearchResultReference sr : result.getReferences()) { + ldif.append(createSearchReference(sr)); + } + } + + return ldif.toString(); + } + + + /** + * Creates an LDIF using the supplied ldap entry. + * + * @param entry ldap entry + * @return LDIF + */ + protected String createLdifEntry(final LdapEntry entry) { + if (entry == null) { + return ""; + } + + final StringBuilder entryLdif = new StringBuilder(); + final String dn = entry.getDn(); + if (dn != null) { + if (LdapUtils.shouldBase64Encode(dn)) { + entryLdif.append("dn:: ").append(LdapUtils.base64Encode(dn)).append(LINE_SEPARATOR); + } else { + entryLdif.append("dn: ").append(dn).append(LINE_SEPARATOR); + } + } + + for (LdapAttribute attr : entry.getAttributes()) { + final String attrName = attr.getName(); + final Collection ldifLines = attr.getValues( + bytes -> { + final StringBuilder sb = new StringBuilder(attrName); + if (attr.isBinary()) { + sb.append(":: ").append(LdapUtils.base64Encode(bytes)).append(LINE_SEPARATOR); + } else if (LdapUtils.shouldBase64Encode(bytes)) { + sb.append(":: ").append(LdapUtils.base64Encode(bytes)).append(LINE_SEPARATOR); + } else { + sb.append(": ").append(LdapUtils.utf8Encode(bytes)).append(LINE_SEPARATOR); + } + return sb.toString(); + }); + for (String line : ldifLines) { + entryLdif.append(line); + } + } + + if (entryLdif.length() > 0) { + entryLdif.append(LINE_SEPARATOR); + } + return entryLdif.toString(); + } + + + /** + * Creates an LDIF using the supplied search reference. + * + * @param ref search reference + * @return LDIF + */ + protected String createSearchReference(final SearchResultReference ref) { + if (ref == null) { + return ""; + } + + final StringBuilder refLdif = new StringBuilder(); + for (String url : ref.getUris()) { + if (LdapUtils.shouldBase64Encode(url)) { + refLdif.append("ref:: ").append(LdapUtils.base64Encode(url)).append(LINE_SEPARATOR); + } else { + refLdif.append("ref: ").append(url).append(LINE_SEPARATOR); + } + } + + if (refLdif.length() > 0) { + refLdif.append(LINE_SEPARATOR); + } + return refLdif.toString(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/ResourceLoader.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/ResourceLoader.java new file mode 100644 index 0000000..0554ba4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/ResourceLoader.java @@ -0,0 +1,32 @@ + +package org.xbib.net.ldap.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Creates an {@link InputStream} from a string URI. + * + */ +public interface ResourceLoader { + + + /** + * Returns whether the supplied path can be loaded by this resource loader. + * + * @param path to check + * @return whether the supplied path can be loaded by this resource loader + */ + boolean supports(String path); + + + /** + * Reads an input stream from a path. + * + * @param path from which to read resource. + * @return input stream. + * @throws IOException On IO errors. + */ + InputStream load(String path) + throws IOException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/ResourceUtils.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/ResourceUtils.java new file mode 100644 index 0000000..20cdc10 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/ResourceUtils.java @@ -0,0 +1,141 @@ + +package org.xbib.net.ldap.io; + +import java.io.IOException; +import java.io.InputStream; +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides utility methods for resources. + * + */ +public final class ResourceUtils { + + /** + * Default resource loaders. + */ + private static final ResourceLoader[] DEFAULT_RESOURCE_LOADERS = new ResourceLoader[]{ + new ClasspathResourceLoader(), + new FileResourceLoader(), + }; + + /** + * Custom resource loaders. + */ + private static ResourceLoader[] customResourceLoaders; + + + /** + * Default constructor. + */ + private ResourceUtils() { + } + + + /** + * Sets the custom resource loaders. + * + * @param loaders custom resource loaders + */ + public static void setCustomResourceLoaders(final ResourceLoader... loaders) { + customResourceLoaders = loaders; + } + + + /** + * Returns whether the supplied path is supported by a {@link ResourceLoader}. + * + * @param path to inspect + * @param loaders to invoke {@link ResourceLoader#supports(String)} on + * @return whether the supplied string represents a resource + */ + public static boolean isResource(final String path, final ResourceLoader... loaders) { + for (ResourceLoader loader : loaders) { + if (loader.supports(path)) { + return true; + } + } + return false; + } + + + /** + * Invokes {@link #isResource(String, ResourceLoader...)} with {@link #DEFAULT_RESOURCE_LOADERS}. + * + * @param path to inspect + * @return whether the supplied string represents a resource + */ + public static boolean isResource(final String path) { + if (customResourceLoaders != null && customResourceLoaders.length > 0) { + return isResource(path, LdapUtils.concatArrays(DEFAULT_RESOURCE_LOADERS, customResourceLoaders)); + } + return isResource(path, DEFAULT_RESOURCE_LOADERS); + } + + + /** + * Attempts to find a {@link ResourceLoader} that supports the supplied path. If found, that resource loader is used + * to load the input stream. + * + * @param path that designates a resource + * @param loaders to invoke {@link ResourceLoader#load(String)} on + * @return input stream to read the resource + * @throws IOException if the resource cannot be read + * @throws IllegalArgumentException if path is not supported + */ + public static InputStream getResource(final String path, final ResourceLoader... loaders) + throws IOException { + for (ResourceLoader loader : loaders) { + if (loader.supports(path)) { + return loader.load(path); + } + } + throw new IllegalArgumentException("Could not find a resource loader for '" + path + "'"); + } + + + /** + * Invokes {@link #getResource(String, ResourceLoader...)} with {@link #DEFAULT_RESOURCE_LOADERS}. + * + * @param path that designates a resource + * @return input stream to read the resource + * @throws IOException if the resource cannot be read + * @throws IllegalArgumentException if path is not supported + */ + public static InputStream getResource(final String path) + throws IOException { + if (customResourceLoaders != null && customResourceLoaders.length > 0) { + return getResource(path, LdapUtils.concatArrays(DEFAULT_RESOURCE_LOADERS, customResourceLoaders)); + } + return getResource(path, DEFAULT_RESOURCE_LOADERS); + } + + + /** + * Reads the data from the supplied resource path using the supplied loaders. See {@link + * #getResource(String, ResourceLoader...)} and {@link LdapUtils#readInputStream(InputStream)}. + * + * @param path that designates a resource + * @param loaders to invoke {@link #getResource(String, ResourceLoader...)} with + * @return bytes read from the resource + * @throws IOException if the resource cannot be read + */ + public static byte[] readResource(final String path, final ResourceLoader... loaders) + throws IOException { + return LdapUtils.readInputStream(getResource(path, loaders)); + } + + + /** + * Reads the data from the supplied resource path. See {@link #getResource(String)} and {@link + * LdapUtils#readInputStream(InputStream)}. + * + * @param path that designates a resource + * @return bytes read from the resource + * @throws IOException if the resource cannot be read + */ + public static byte[] readResource(final String path) + throws IOException { + return LdapUtils.readInputStream(getResource(path)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/SearchResultReader.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/SearchResultReader.java new file mode 100644 index 0000000..51fc389 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/SearchResultReader.java @@ -0,0 +1,22 @@ + +package org.xbib.net.ldap.io; + +import java.io.IOException; +import org.xbib.net.ldap.SearchResponse; + +/** + * Interface for reading ldap search results. + * + */ +public interface SearchResultReader { + + + /** + * Reads an ldap result. + * + * @return ldap result + * @throws IOException if an error occurs using the reader + */ + SearchResponse read() + throws IOException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/SearchResultWriter.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/SearchResultWriter.java new file mode 100644 index 0000000..c6878c6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/SearchResultWriter.java @@ -0,0 +1,22 @@ + +package org.xbib.net.ldap.io; + +import java.io.IOException; +import org.xbib.net.ldap.SearchResponse; + +/** + * Interface for writing ldap search results. + * + */ +public interface SearchResultWriter { + + + /** + * Writes the supplied ldap result. + * + * @param result ldap result to write + * @throws IOException if an error occurs using the writer + */ + void write(SearchResponse result) + throws IOException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/io/URLResourceLoader.java b/net-ldap/src/main/java/org/xbib/net/ldap/io/URLResourceLoader.java new file mode 100644 index 0000000..78ccf5a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/io/URLResourceLoader.java @@ -0,0 +1,34 @@ + +package org.xbib.net.ldap.io; + +import java.io.InputStream; +import java.net.URI; +import java.net.URL; + +/** + * Creates an {@link InputStream} from a string that is a {@link URL}. + * + */ +public class URLResourceLoader implements ResourceLoader { + + + @Override + public boolean supports(final String path) { + try { + URI.create(path).toURL(); + return true; + } catch (Exception e) { + return false; + } + } + + + @Override + public InputStream load(final String path) { + try { + return URI.create(path).toURL().openStream(); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AbstractLoginModule.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AbstractLoginModule.java new file mode 100644 index 0000000..24aab4a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AbstractLoginModule.java @@ -0,0 +1,347 @@ + +package org.xbib.net.ldap.jaas; + +import java.io.IOException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +/** + * Provides functionality common to ldap based JAAS login modules. + * + */ +public abstract class AbstractLoginModule implements LoginModule { + + /** + * Constant for login name stored in shared state. + */ + public static final String LOGIN_NAME = "javax.security.auth.login.name"; + + /** + * Constant for entryDn stored in shared state. + */ + public static final String LOGIN_DN = "org.xbib.net.ldap.jaas.login.entryDn"; + + /** + * Constant for login password stored in shared state. + */ + public static final String LOGIN_PASSWORD = "javax.security.auth.login.password"; + + /** + * Default roles. + */ + protected final List defaultRole = new ArrayList<>(); + + /** + * Initialized subject. + */ + protected Subject subject; + + /** + * Initialized callback handler. + */ + protected CallbackHandler callbackHandler; + + /** + * Shared state from other login module. + */ + protected Map sharedState; + + /** + * Whether credentials from the shared state should be used. + */ + protected boolean useFirstPass; + + /** + * Whether credentials from the shared state should be used if they are available. + */ + protected boolean tryFirstPass; + + /** + * Whether credentials should be stored in the shared state map. + */ + protected boolean storePass; + + /** + * Whether credentials should be removed from the shared state map. + */ + protected boolean clearPass; + + /** + * Whether ldap principal data should be set. + */ + protected boolean setLdapPrincipal; + + /** + * Whether ldap dn principal data should be set. + */ + protected boolean setLdapDnPrincipal; + + /** + * Whether ldap credential data should be set. + */ + protected boolean setLdapCredential; + + /** + * Name of group to add all principals to. + */ + protected String principalGroupName; + + /** + * Name of group to add all roles to. + */ + protected String roleGroupName; + + /** + * Whether authentication was successful. + */ + protected boolean loginSuccess; + + /** + * Whether commit was successful. + */ + protected boolean commitSuccess; + + /** + * Principals to add to the subject. + */ + protected Set principals; + + /** + * Credentials to add to the subject. + */ + protected Set credentials; + + /** + * Roles to add to the subject. + */ + protected Set roles; + + + @Override + public void initialize( + final Subject subj, + final CallbackHandler handler, + final Map state, + final Map options) { + subject = subj; + callbackHandler = handler; + sharedState = state; + + for (String key : options.keySet()) { + final String value = (String) options.get(key); + if ("useFirstPass".equalsIgnoreCase(key)) { + useFirstPass = Boolean.parseBoolean(value); + } else if ("tryFirstPass".equalsIgnoreCase(key)) { + tryFirstPass = Boolean.parseBoolean(value); + } else if ("storePass".equalsIgnoreCase(key)) { + storePass = Boolean.parseBoolean(value); + } else if ("clearPass".equalsIgnoreCase(key)) { + clearPass = Boolean.parseBoolean(value); + } else if ("setLdapPrincipal".equalsIgnoreCase(key)) { + setLdapPrincipal = Boolean.parseBoolean(value); + } else if ("setLdapDnPrincipal".equalsIgnoreCase(key)) { + setLdapDnPrincipal = Boolean.parseBoolean(value); + } else if ("setLdapCredential".equalsIgnoreCase(key)) { + setLdapCredential = Boolean.parseBoolean(value); + } else if ("defaultRole".equalsIgnoreCase(key)) { + for (String s : value.split(",")) { + defaultRole.add(new LdapRole(s.trim())); + } + } else if ("principalGroupName".equalsIgnoreCase(key)) { + principalGroupName = value; + } else if ("roleGroupName".equalsIgnoreCase(key)) { + roleGroupName = value; + } + } + principals = new TreeSet<>(); + credentials = new HashSet<>(); + roles = new TreeSet<>(); + } + + + @Override + public boolean login() + throws LoginException { + final NameCallback nameCb = new NameCallback("Enter user: "); + final PasswordCallback passCb = new PasswordCallback("Enter user password: ", false); + return login(nameCb, passCb); + } + + + /** + * Authenticates a {@link Subject} with the supplied callbacks. + * + * @param nameCb callback handler for subject's name + * @param passCb callback handler for subject's password + * @return true if authentication succeeded, false to ignore this module + * @throws LoginException if the authentication fails + */ + protected abstract boolean login(NameCallback nameCb, PasswordCallback passCb) + throws LoginException; + + + @Override + public boolean commit() + throws LoginException { + if (!loginSuccess) { + return false; + } + + if (subject.isReadOnly()) { + clearState(); + throw new LoginException("Subject is read-only."); + } + subject.getPrincipals().addAll(principals); + subject.getPrivateCredentials().addAll(credentials); + subject.getPrincipals().addAll(roles); + if (principalGroupName != null) { + final LdapGroup group = new LdapGroup(principalGroupName); + principals.forEach(group::addMember); + subject.getPrincipals().add(group); + } + if (roleGroupName != null) { + final LdapGroup group = new LdapGroup(roleGroupName); + roles.forEach(group::addMember); + subject.getPrincipals().add(group); + } + clearState(); + commitSuccess = true; + return true; + } + + + @Override + public boolean abort() + throws LoginException { + if (!loginSuccess) { + return false; + } else if (!commitSuccess) { + loginSuccess = false; + clearState(); + } else { + logout(); + } + return true; + } + + + @Override + public boolean logout() + throws LoginException { + if (subject.isReadOnly()) { + clearState(); + throw new LoginException("Subject is read-only."); + } + + for (LdapPrincipal ldapPrincipal : subject.getPrincipals(LdapPrincipal.class)) { + subject.getPrincipals().remove(ldapPrincipal); + } + + for (LdapDnPrincipal ldapDnPrincipal : subject.getPrincipals(LdapDnPrincipal.class)) { + subject.getPrincipals().remove(ldapDnPrincipal); + } + + for (LdapRole ldapRole : subject.getPrincipals(LdapRole.class)) { + subject.getPrincipals().remove(ldapRole); + } + + for (LdapGroup ldapGroup : subject.getPrincipals(LdapGroup.class)) { + subject.getPrincipals().remove(ldapGroup); + } + + for (LdapCredential ldapCredential : subject.getPrivateCredentials(LdapCredential.class)) { + subject.getPrivateCredentials().remove(ldapCredential); + } + + clearState(); + loginSuccess = false; + commitSuccess = false; + return true; + } + + + /** + * Removes any stateful principals, credentials, or roles stored by login. Also removes shared state name, dn, and + * password if clearPass is set. + */ + protected void clearState() { + principals.clear(); + credentials.clear(); + roles.clear(); + if (clearPass) { + sharedState.remove(LOGIN_NAME); + sharedState.remove(LOGIN_PASSWORD); + sharedState.remove(LOGIN_DN); + } + } + + + /** + * Attempts to retrieve credentials for the supplied name and password callbacks. If useFirstPass or tryFirstPass is + * set, then name and password data is retrieved from shared state. Otherwise, a callback handler is used to get the + * data. Set useCallback to force a callback handler to be used. + * + * @param nameCb to set name for + * @param passCb to set password for + * @param useCallback whether to force a callback handler + * @throws LoginException if the callback handler fails + */ + protected void getCredentials(final NameCallback nameCb, final PasswordCallback passCb, final boolean useCallback) + throws LoginException { + try { + if ((useFirstPass || tryFirstPass) && !useCallback) { + nameCb.setName((String) sharedState.get(LOGIN_NAME)); + passCb.setPassword((char[]) sharedState.get(LOGIN_PASSWORD)); + } else if (callbackHandler != null) { + callbackHandler.handle(new Callback[]{nameCb, passCb}); + } else { + throw new LoginException( + "No CallbackHandler available. " + + "Set useFirstPass, tryFirstPass, or provide a CallbackHandler"); + } + } catch (IOException e) { + loginSuccess = false; + throw new LoginException(e.getMessage()); + } catch (UnsupportedCallbackException e) { + loginSuccess = false; + throw new LoginException(e.getMessage()); + } + } + + + /** + * Stores the supplied name, password, and entry dn in the stored state map. storePass must be set for this method to + * have any affect. + * + * @param nameCb to store + * @param passCb to store + * @param loginDn to store + */ + @SuppressWarnings("unchecked") + protected void storeCredentials(final NameCallback nameCb, final PasswordCallback passCb, final String loginDn) { + if (storePass) { + if (nameCb != null && nameCb.getName() != null) { + sharedState.put(LOGIN_NAME, nameCb.getName()); + } + if (passCb != null && passCb.getPassword() != null) { + sharedState.put(LOGIN_PASSWORD, passCb.getPassword()); + } + if (loginDn != null) { + sharedState.put(LOGIN_DN, loginDn); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AbstractPropertiesFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AbstractPropertiesFactory.java new file mode 100644 index 0000000..cd52e5b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AbstractPropertiesFactory.java @@ -0,0 +1,37 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.Map; +import java.util.Properties; +import org.xbib.net.ldap.props.PropertySource.PropertyDomain; + +/** + * Provides implementation common to properties based factories. + * + */ +public abstract class AbstractPropertiesFactory { + + /** + * Cache ID option used on the JAAS config. + */ + public static final String CACHE_ID = "cacheId"; + + + /** + * Returns context specific properties based on the supplied JAAS options. + * + * @param options to read properties from + * @return properties + */ + protected static Properties createProperties(final Map options) { + final Properties p = new Properties(); + for (Map.Entry entry : options.entrySet()) { + if (entry.getKey().contains(".")) { + p.setProperty(entry.getKey(), entry.getValue().toString()); + } else { + p.setProperty(PropertyDomain.AUTH.value() + entry.getKey(), entry.getValue().toString()); + } + } + return p; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AuthenticatorFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AuthenticatorFactory.java new file mode 100644 index 0000000..3db7c68 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/AuthenticatorFactory.java @@ -0,0 +1,31 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.Map; +import org.xbib.net.ldap.auth.AuthenticationRequest; +import org.xbib.net.ldap.auth.Authenticator; + +/** + * Provides an interface for creating authenticators needed by various JAAS modules. + * + */ +public interface AuthenticatorFactory { + + + /** + * Creates a new authenticator with the supplied JAAS options. + * + * @param jaasOptions JAAS configuration options + * @return authenticator + */ + Authenticator createAuthenticator(Map jaasOptions); + + + /** + * Creates a new authentication request with the supplied JAAS options. + * + * @param jaasOptions JAAS configuration options + * @return authentication request + */ + AuthenticationRequest createAuthenticationRequest(Map jaasOptions); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapCredential.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapCredential.java new file mode 100644 index 0000000..c301a75 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapCredential.java @@ -0,0 +1,65 @@ + +package org.xbib.net.ldap.jaas; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides a custom implementation for adding LDAP credentials to a subject. + * + */ +public class LdapCredential { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 401; + + /** + * LDAP credential. + */ + private final Object credential; + + + /** + * Creates a new ldap credential with the supplied credential. + * + * @param o credential to store + */ + public LdapCredential(final Object o) { + credential = o; + } + + + /** + * Returns the credential for this ldap credential. + * + * @return credential + */ + public Object getCredential() { + return credential; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof LdapCredential v) { + return LdapUtils.areEqual(credential, v.credential); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, credential); + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "credential=" + credential + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapDnAuthorizationModule.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapDnAuthorizationModule.java new file mode 100644 index 0000000..f453794 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapDnAuthorizationModule.java @@ -0,0 +1,100 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.Map; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.login.LoginException; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.auth.Authenticator; +import org.xbib.net.ldap.auth.User; + +/** + * Provides a JAAS authentication hook into LDAP DNs. No authentication is performed by this module. The LDAP entry DN + * can be stored and shared with other JAAS modules. + * + */ +public class LdapDnAuthorizationModule extends AbstractLoginModule { + + /** + * Whether failing to find a DN should raise an exception. + */ + private boolean noResultsIsError; + + /** + * Factory for creating authenticators with JAAS options. + */ + private AuthenticatorFactory authenticatorFactory; + + /** + * Authenticator to use against the LDAP. + */ + private Authenticator auth; + + @Override + public void initialize( + final Subject subject, + final CallbackHandler callbackHandler, + final Map sharedState, + final Map options) { + super.initialize(subject, callbackHandler, sharedState, options); + + for (String key : options.keySet()) { + final String value = (String) options.get(key); + if ("noResultsIsError".equalsIgnoreCase(key)) { + noResultsIsError = Boolean.parseBoolean(value); + } else if ("authenticatorFactory".equalsIgnoreCase(key)) { + try { + authenticatorFactory = (AuthenticatorFactory) Class.forName(value).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + } + + if (authenticatorFactory == null) { + authenticatorFactory = new PropertiesAuthenticatorFactory(); + } + + auth = authenticatorFactory.createAuthenticator(options); + } + + @Override + protected boolean login(final NameCallback nameCb, final PasswordCallback passCb) + throws LoginException { + try { + getCredentials(nameCb, passCb, false); + + if (nameCb.getName() == null && tryFirstPass) { + getCredentials(nameCb, passCb, true); + } + + final String loginName = nameCb.getName(); + if (loginName != null && setLdapPrincipal) { + principals.add(new LdapPrincipal(loginName, null)); + loginSuccess = true; + } + + final String loginDn = auth.resolveDn(new User(nameCb.getName())); + if (loginDn == null && noResultsIsError) { + loginSuccess = false; + throw new LoginException("Could not find DN for " + nameCb.getName()); + } + if (loginDn != null && setLdapDnPrincipal) { + principals.add(new LdapDnPrincipal(loginDn, null)); + loginSuccess = true; + } + if (defaultRole != null && !defaultRole.isEmpty()) { + roles.addAll(defaultRole); + loginSuccess = true; + } + storeCredentials(nameCb, passCb, loginDn); + } catch (LdapException e) { + loginSuccess = false; + throw new LoginException(e.getMessage()); + } + return true; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapDnPrincipal.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapDnPrincipal.java new file mode 100644 index 0000000..55fe24b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapDnPrincipal.java @@ -0,0 +1,89 @@ + +package org.xbib.net.ldap.jaas; + +import java.security.Principal; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides a custom implementation for adding LDAP principals to a subject. + * + */ +public class LdapDnPrincipal implements Principal, Comparable { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 409; + + /** + * LDAP user name. + */ + private final String ldapDn; + + /** + * User ldap entry. + */ + private final LdapEntry ldapEntry; + + + /** + * Creates a new ldap principal with the supplied name. + * + * @param name of an ldap DN + * @param entry ldap entry associated with this principal + */ + public LdapDnPrincipal(final String name, final LdapEntry entry) { + ldapDn = name; + ldapEntry = entry; + } + + + @Override + public String getName() { + return ldapDn; + } + + + /** + * Returns the ldap entry for this ldap principal. + * + * @return ldap entry + */ + public LdapEntry getLdapEntry() { + return ldapEntry; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof LdapDnPrincipal v) { + return LdapUtils.areEqual(ldapDn, v.ldapDn); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, ldapDn); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "ldapDn=" + ldapDn + ", " + + "ldapEntry=" + (ldapEntry != null ? ldapEntry : "") + "]"; + } + + + @Override + public int compareTo(final Principal p) { + return ldapDn.compareTo(p.getName()); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapGroup.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapGroup.java new file mode 100644 index 0000000..920d651 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapGroup.java @@ -0,0 +1,114 @@ + +package org.xbib.net.ldap.jaas; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides a custom implementation for grouping principals. + * + */ +@SuppressWarnings("serial") +public class LdapGroup implements Principal { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 431; + + /** + * LDAP group name. + */ + private final String groupName; + + /** + * Principal members. + */ + private final Set members = new HashSet<>(); + + /** + * Creates a new ldap group with the supplied name. + * + * @param name of the group + */ + public LdapGroup(final String name) { + groupName = name; + } + + + @Override + public String getName() { + return groupName; + } + + + /** + * Adds a member to this group. + * + * @param user to add + */ + public void addMember(final Principal user) { + members.add(user); + } + + + /** + * Removes a member from this group. + * + * @param user to remove + */ + public void removeMember(final Principal user) { + members.remove(user); + } + + + public boolean isMember(final Principal member) { + for (Principal p : members) { + if (p.getName() != null && p.getName().equals(member.getName())) { + return true; + } + } + return false; + } + + + /** + * Returns an unmodifiable set of the members in this group. + * + * @return set of member principals + */ + public Set getMembers() { + return Collections.unmodifiableSet(members); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof LdapGroup v) { + return LdapUtils.areEqual(groupName, v.groupName) && + LdapUtils.areEqual(members, v.members); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, groupName, members); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "groupName=" + groupName + ", " + + "members=" + members + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapLoginModule.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapLoginModule.java new file mode 100644 index 0000000..8f9e884 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapLoginModule.java @@ -0,0 +1,148 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.Map; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.login.LoginException; +import org.xbib.net.ldap.Credential; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ReturnAttributes; +import org.xbib.net.ldap.auth.AuthenticationRequest; +import org.xbib.net.ldap.auth.AuthenticationResponse; +import org.xbib.net.ldap.auth.Authenticator; +import org.xbib.net.ldap.auth.User; + +/** + * Provides a JAAS authentication hook for LDAP authentication. + * + */ +public class LdapLoginModule extends AbstractLoginModule { + + /** + * User attribute to add to role data. + */ + private String[] userRoleAttribute = ReturnAttributes.NONE.value(); + + /** + * Factory for creating authenticators with JAAS options. + */ + private AuthenticatorFactory authenticatorFactory; + + /** + * Authenticator to use against the LDAP. + */ + private Authenticator auth; + + /** + * Authentication request to use for authentication. + */ + private AuthenticationRequest authRequest; + + @Override + public void initialize( + final Subject subject, + final CallbackHandler callbackHandler, + final Map sharedState, + final Map options) { + setLdapPrincipal = true; + setLdapCredential = true; + + super.initialize(subject, callbackHandler, sharedState, options); + + for (String key : options.keySet()) { + final String value = (String) options.get(key); + if ("userRoleAttribute".equalsIgnoreCase(key)) { + if ("".equals(value)) { + userRoleAttribute = ReturnAttributes.NONE.value(); + } else if ("*".equals(value)) { + userRoleAttribute = ReturnAttributes.ALL_USER.value(); + } else { + userRoleAttribute = value.split(","); + } + } else if ("authenticatorFactory".equalsIgnoreCase(key)) { + try { + authenticatorFactory = (AuthenticatorFactory) Class.forName(value).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + } + + if (authenticatorFactory == null) { + authenticatorFactory = new PropertiesAuthenticatorFactory(); + } + + auth = authenticatorFactory.createAuthenticator(options); + + authRequest = authenticatorFactory.createAuthenticationRequest(options); + authRequest.setReturnAttributes(userRoleAttribute); + } + + @Override + protected boolean login(final NameCallback nameCb, final PasswordCallback passCb) + throws LoginException { + try { + getCredentials(nameCb, passCb, false); + authRequest.setUser(new User(nameCb.getName())); + authRequest.setCredential(new Credential(passCb.getPassword())); + + AuthenticationResponse response = auth.authenticate(authRequest); + LdapEntry entry = null; + if (response.isSuccess()) { + entry = response.getLdapEntry(); + if (entry != null) { + roles.addAll(LdapRole.toRoles(entry)); + if (defaultRole != null && !defaultRole.isEmpty()) { + roles.addAll(defaultRole); + } + } + loginSuccess = true; + } else { + if (tryFirstPass) { + getCredentials(nameCb, passCb, true); + response = auth.authenticate(authRequest); + if (response.isSuccess()) { + entry = response.getLdapEntry(); + if (entry != null) { + roles.addAll(LdapRole.toRoles(entry)); + } + if (defaultRole != null && !defaultRole.isEmpty()) { + roles.addAll(defaultRole); + } + loginSuccess = true; + } else { + loginSuccess = false; + } + } else { + loginSuccess = false; + } + } + + if (!loginSuccess) { + throw new LoginException("Authentication failed: " + response); + } else { + if (setLdapPrincipal) { + principals.add(new LdapPrincipal(nameCb.getName(), entry)); + } + + final String loginDn = response.getResolvedDn(); + if (loginDn != null && setLdapDnPrincipal) { + principals.add(new LdapDnPrincipal(loginDn, entry)); + } + + if (setLdapCredential) { + credentials.add(new LdapCredential(passCb.getPassword())); + } + storeCredentials(nameCb, passCb, loginDn); + } + } catch (LdapException e) { + loginSuccess = false; + throw new LoginException(e.getMessage()); + } + return true; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapPrincipal.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapPrincipal.java new file mode 100644 index 0000000..595a86f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapPrincipal.java @@ -0,0 +1,89 @@ + +package org.xbib.net.ldap.jaas; + +import java.security.Principal; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides a custom implementation for adding LDAP principals to a subject. + * + */ +public class LdapPrincipal implements Principal, Comparable { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 419; + + /** + * LDAP user name. + */ + private final String ldapName; + + /** + * User ldap entry. + */ + private final LdapEntry ldapEntry; + + + /** + * Creates a new ldap principal with the supplied name. + * + * @param name of this principal + * @param entry ldap entry associated with this principal + */ + public LdapPrincipal(final String name, final LdapEntry entry) { + ldapName = name; + ldapEntry = entry; + } + + + @Override + public String getName() { + return ldapName; + } + + + /** + * Returns the ldap entry for this ldap principal. + * + * @return ldap entry + */ + public LdapEntry getLdapEntry() { + return ldapEntry; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof LdapPrincipal v) { + return LdapUtils.areEqual(ldapName, v.ldapName); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, ldapName); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "ldapName=" + ldapName + ", " + + "ldapEntry=" + (ldapEntry != null ? ldapEntry : "") + "]"; + } + + + @Override + public int compareTo(final Principal p) { + return ldapName.compareTo(p.getName()); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapRole.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapRole.java new file mode 100644 index 0000000..e29138d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapRole.java @@ -0,0 +1,111 @@ + +package org.xbib.net.ldap.jaas; + +import java.security.Principal; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchResponse; + +/** + * Provides a custom implementation for adding LDAP principals to a subject that represent roles. + * + */ +@SuppressWarnings("serial") +public class LdapRole implements Principal, Comparable { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 421; + + /** + * LDAP role name. + */ + private final String roleName; + + + /** + * Creates a new ldap role with the supplied name. + * + * @param name of this role + */ + public LdapRole(final String name) { + roleName = name; + } + + /** + * Iterates over the supplied result and returns all attributes as a set of ldap roles. + * + * @param result to read + * @return ldap roles + */ + public static Set toRoles(final SearchResponse result) { + final Set r = new HashSet<>(); + for (LdapEntry le : result.getEntries()) { + r.addAll(toRoles(le)); + } + return r; + } + + /** + * Iterates over the supplied entry and returns all attributes as a set of ldap roles. + * + * @param entry to read + * @return ldap roles + */ + public static Set toRoles(final LdapEntry entry) { + return toRoles(entry.getAttributes()); + } + + /** + * Iterates over the supplied attributes and returns all values as a set of ldap roles. + * + * @param attributes to read + * @return ldap roles + */ + public static Set toRoles(final Collection attributes) { + final Set r = new HashSet<>(); + if (attributes != null) { + for (LdapAttribute ldapAttr : attributes) { + r.addAll(ldapAttr.getStringValues().stream().map(LdapRole::new).collect(Collectors.toList())); + } + } + return r; + } + + @Override + public String getName() { + return roleName; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof LdapRole v) { + return LdapUtils.areEqual(roleName, v.roleName); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, roleName); + } + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "roleName=" + roleName + "]"; + } + + @Override + public int compareTo(final Principal p) { + return roleName.compareTo(p.getName()); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapRoleAuthorizationModule.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapRoleAuthorizationModule.java new file mode 100644 index 0000000..1a59f30 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/LdapRoleAuthorizationModule.java @@ -0,0 +1,140 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.Map; +import java.util.Set; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.login.LoginException; +import org.xbib.net.ldap.FilterTemplate; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ReturnAttributes; +import org.xbib.net.ldap.SearchRequest; + +/** + * Provides a JAAS authentication hook into LDAP roles. No authentication is performed in this module. Role data is set + * for the login name in the shared state or for the name returned by the CallbackHandler. + * + */ +public class LdapRoleAuthorizationModule extends AbstractLoginModule { + + /** + * Ldap filter for role searches. + */ + private String roleFilter; + + /** + * Role attribute to add to role data. + */ + private String[] roleAttribute = ReturnAttributes.NONE.value(); + + /** + * Whether failing to find any roles should raise an exception. + */ + private boolean noResultsIsError; + + /** + * Factory for creating role resolvers with JAAS options. + */ + private RoleResolverFactory roleResolverFactory; + + /** + * To search for roles. + */ + private RoleResolver roleResolver; + + /** + * Search request to use for roles. + */ + private SearchRequest searchRequest; + + @Override + public void initialize( + final Subject subject, + final CallbackHandler callbackHandler, + final Map sharedState, + final Map options) { + super.initialize(subject, callbackHandler, sharedState, options); + + for (String key : options.keySet()) { + final String value = (String) options.get(key); + if ("roleFilter".equalsIgnoreCase(key)) { + roleFilter = value; + } else if ("roleAttribute".equalsIgnoreCase(key)) { + if ("".equals(value)) { + roleAttribute = ReturnAttributes.NONE.value(); + } else if ("*".equals(value)) { + roleAttribute = ReturnAttributes.ALL_USER.value(); + } else { + roleAttribute = value.split(","); + } + } else if ("noResultsIsError".equalsIgnoreCase(key)) { + noResultsIsError = Boolean.parseBoolean(value); + } else if ("roleResolverFactory".equalsIgnoreCase(key)) { + try { + roleResolverFactory = (RoleResolverFactory) Class.forName(value).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + } + + if (roleResolverFactory == null) { + roleResolverFactory = new PropertiesRoleResolverFactory(); + } + + roleResolver = roleResolverFactory.createRoleResolver(options); + + searchRequest = roleResolverFactory.createSearchRequest(options); + searchRequest.setReturnAttributes(roleAttribute); + } + + @Override + protected boolean login(final NameCallback nameCb, final PasswordCallback passCb) + throws LoginException { + try { + getCredentials(nameCb, passCb, false); + + if (nameCb.getName() == null && tryFirstPass) { + getCredentials(nameCb, passCb, true); + } + + final String loginName = nameCb.getName(); + if (loginName != null && setLdapPrincipal) { + principals.add(new LdapPrincipal(loginName, null)); + loginSuccess = true; + } + + final String loginDn = (String) sharedState.get(LOGIN_DN); + if (loginDn != null && setLdapDnPrincipal) { + principals.add(new LdapDnPrincipal(loginDn, null)); + loginSuccess = true; + } + + final FilterTemplate template = new FilterTemplate(roleFilter); + template.setParameter("dn", loginDn); + template.setParameter("user", loginName); + searchRequest.setFilter(template); + + final Set lr = roleResolver.search(searchRequest); + if (lr.isEmpty() && noResultsIsError) { + loginSuccess = false; + throw new LoginException("Could not find roles using " + roleFilter); + } + roles.addAll(lr); + if (defaultRole != null && !defaultRole.isEmpty()) { + roles.addAll(defaultRole); + } + if (!roles.isEmpty()) { + loginSuccess = true; + } + storeCredentials(nameCb, passCb, null); + } catch (LdapException e) { + loginSuccess = false; + throw new LoginException(e.getMessage()); + } + return true; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/PropertiesAuthenticatorFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/PropertiesAuthenticatorFactory.java new file mode 100644 index 0000000..c713abf --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/PropertiesAuthenticatorFactory.java @@ -0,0 +1,73 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.auth.AuthenticationRequest; +import org.xbib.net.ldap.auth.Authenticator; +import org.xbib.net.ldap.props.AuthenticationRequestPropertySource; +import org.xbib.net.ldap.props.AuthenticatorPropertySource; + +/** + * Provides a module authenticator factory implementation that uses the properties package in this library. + * + */ +public class PropertiesAuthenticatorFactory extends AbstractPropertiesFactory implements AuthenticatorFactory { + + /** + * Object CACHE. + */ + private static final Map CACHE = new HashMap<>(); + + /** + * Iterates over the CACHE and closes any managed dn resolvers and managed authentication handlers. + */ + public static void close() { + for (Map.Entry e : CACHE.entrySet()) { + final Authenticator a = e.getValue(); + a.close(); + } + } + + @Override + public Authenticator createAuthenticator(final Map jaasOptions) { + final Authenticator a; + if (jaasOptions.containsKey(CACHE_ID)) { + final String cacheId = (String) jaasOptions.get(CACHE_ID); + synchronized (CACHE) { + if (!CACHE.containsKey(cacheId)) { + a = createAuthenticatorInternal(jaasOptions); + CACHE.put(cacheId, a); + } else { + a = CACHE.get(cacheId); + } + } + } else { + a = createAuthenticatorInternal(jaasOptions); + } + return a; + } + + /** + * Initializes an authenticator using an authenticator property source. + * + * @param options to initialize authenticator + * @return authenticator + */ + protected Authenticator createAuthenticatorInternal(final Map options) { + final Authenticator a = new Authenticator(); + final AuthenticatorPropertySource source = new AuthenticatorPropertySource(a, createProperties(options)); + source.initialize(); + return a; + } + + @Override + public AuthenticationRequest createAuthenticationRequest(final Map jaasOptions) { + final AuthenticationRequest ar = new AuthenticationRequest(); + final AuthenticationRequestPropertySource source = new AuthenticationRequestPropertySource( + ar, + createProperties(jaasOptions)); + source.initialize(); + return ar; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/PropertiesRoleResolverFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/PropertiesRoleResolverFactory.java new file mode 100644 index 0000000..0a208c0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/PropertiesRoleResolverFactory.java @@ -0,0 +1,100 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.ConnectionFactoryManager; +import org.xbib.net.ldap.DefaultConnectionFactory; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.props.DefaultConnectionFactoryPropertySource; +import org.xbib.net.ldap.props.PropertySource.PropertyDomain; +import org.xbib.net.ldap.props.SearchRequestPropertySource; +import org.xbib.net.ldap.props.SearchRoleResolverPropertySource; + +/** + * Provides a module role resolver factory implementation that uses the properties package in this library. + * + */ +public class PropertiesRoleResolverFactory extends AbstractPropertiesFactory implements RoleResolverFactory { + + /** + * Object CACHE. + */ + private static final Map CACHE = new HashMap<>(); + + /** + * Iterates over the CACHE and closes all role resolvers. + */ + public static void close() { + CACHE.values().stream().filter(rr -> rr instanceof ConnectionFactoryManager).forEach(rr -> { + final ConnectionFactoryManager cfm = (ConnectionFactoryManager) rr; + cfm.getConnectionFactory().close(); + }); + } + + @Override + public RoleResolver createRoleResolver(final Map jaasOptions) { + final RoleResolver rr; + if (jaasOptions.containsKey(CACHE_ID)) { + final String cacheId = (String) jaasOptions.get(CACHE_ID); + synchronized (CACHE) { + if (!CACHE.containsKey(cacheId)) { + rr = createRoleResolverInternal(jaasOptions); + CACHE.put(cacheId, rr); + } else { + rr = CACHE.get(cacheId); + } + } + } else { + rr = createRoleResolverInternal(jaasOptions); + } + return rr; + } + + /** + * Initializes a role resolver using a role resolver property source. + * + * @param options to initialize role resolver + * @return role resolver + */ + protected RoleResolver createRoleResolverInternal(final Map options) { + final RoleResolver rr; + if (options.containsKey("roleResolver")) { + try { + final String className = (String) options.get("roleResolver"); + rr = (RoleResolver) Class.forName(className).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + if (rr instanceof ConnectionFactoryManager cfm) { + if (cfm.getConnectionFactory() == null) { + final DefaultConnectionFactory cf = new DefaultConnectionFactory(); + final DefaultConnectionFactoryPropertySource cfPropSource = new DefaultConnectionFactoryPropertySource( + cf, + PropertyDomain.AUTH, + createProperties(options)); + cfPropSource.initialize(); + cfm.setConnectionFactory(cf); + } + } + } else { + rr = new SearchRoleResolver(); + final SearchRoleResolverPropertySource source = new SearchRoleResolverPropertySource( + (SearchRoleResolver) rr, + createProperties(options)); + source.initialize(); + } + return rr; + } + + @Override + public SearchRequest createSearchRequest(final Map jaasOptions) { + final SearchRequest sr = new SearchRequest(); + final SearchRequestPropertySource source = new SearchRequestPropertySource( + sr, + PropertyDomain.AUTH, + createProperties(jaasOptions)); + source.initialize(); + return sr; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/RoleResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/RoleResolver.java new file mode 100644 index 0000000..ebbed0a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/RoleResolver.java @@ -0,0 +1,24 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.Set; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.SearchRequest; + +/** + * Looks up a user's roles using an LDAP search. + * + */ +public interface RoleResolver { + + + /** + * Executes a search request and converts any attributes to ldap roles. + * + * @param request to execute + * @return ldap roles + * @throws LdapException if the ldap operation fails + */ + Set search(SearchRequest request) + throws LdapException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/RoleResolverFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/RoleResolverFactory.java new file mode 100644 index 0000000..08108d2 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/RoleResolverFactory.java @@ -0,0 +1,30 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.Map; +import org.xbib.net.ldap.SearchRequest; + +/** + * Provides an interface for creating role resolver needed by various JAAS modules. + * + */ +public interface RoleResolverFactory { + + + /** + * Creates a new role resolver with the supplied JAAS options. + * + * @param jaasOptions JAAS configuration options + * @return role resolver + */ + RoleResolver createRoleResolver(Map jaasOptions); + + + /** + * Creates a new search request with the supplied JAAS options. + * + * @param jaasOptions JAAS configuration options + * @return search request + */ + SearchRequest createSearchRequest(Map jaasOptions); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/jaas/SearchRoleResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/SearchRoleResolver.java new file mode 100644 index 0000000..34f20c5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/jaas/SearchRoleResolver.java @@ -0,0 +1,52 @@ + +package org.xbib.net.ldap.jaas; + +import java.util.Set; +import org.xbib.net.ldap.AbstractSearchOperationFactory; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; + +/** + * Base class for search role resolver implementations. + * + */ +public class SearchRoleResolver extends AbstractSearchOperationFactory implements RoleResolver { + + + /** + * Default constructor. + */ + public SearchRoleResolver() { + } + + + /** + * Creates a new role resolver. + * + * @param cf connection factory + */ + public SearchRoleResolver(final ConnectionFactory cf) { + setConnectionFactory(cf); + } + + + @Override + public Set search(final SearchRequest request) + throws LdapException { + final SearchOperation op = createSearchOperation(); + final SearchResponse result = op.execute(request); + if (!result.isSuccess()) { + throw new LdapException("Unsuccessful role search: " + result); + } + return LdapRole.toRoles(result); + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "factory=" + getConnectionFactory() + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/AbstractConnectionPool.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/AbstractConnectionPool.java new file mode 100644 index 0000000..9fdf3e0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/AbstractConnectionPool.java @@ -0,0 +1,1235 @@ + +package org.xbib.net.ldap.pool; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionValidator; +import org.xbib.net.ldap.DefaultConnectionFactory; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.SearchConnectionValidator; +import org.xbib.net.ldap.concurrent.CallableWorker; + +/** + * Contains the base implementation for pooling connections. The main design objective for the supplied pooling + * implementations is to provide a pool that does not block on connection creation or destruction. This is what accounts + * for the multiple locks available on this class. The pool is backed by two queues, one for available connections and + * one for active connections. Connections that are available via {@link #getConnection()} exist in the available queue. + * Connections that are actively in use exist in the active queue. This implementation uses FIFO operations for each + * queue. + * + */ +public abstract class AbstractConnectionPool implements ConnectionPool { + + /** + * Default min pool size, value is {@value}. + */ + public static final int DEFAULT_MIN_POOL_SIZE = 3; + + /** + * Default max pool size, value is {@value}. + */ + public static final int DEFAULT_MAX_POOL_SIZE = 10; + + /** + * ID used for pool name. + */ + private static final AtomicInteger POOL_ID = new AtomicInteger(); + + /** + * Lock for the entire pool. + */ + protected final ReentrantLock poolLock = new ReentrantLock(); + + /** + * Condition for notifying threads that a connection was returned. + */ + protected final Condition poolNotEmpty = poolLock.newCondition(); + + /** + * Lock for check outs. + */ + protected final ReentrantLock checkOutLock = new ReentrantLock(); + + /** + * List of available connections in the pool. + */ + protected Queue available; + + /** + * List of connections in use. + */ + protected Queue active; + + /** + * Pool name. + */ + private String name = "pool-" + POOL_ID.incrementAndGet(); + + /** + * Minimum pool size. + */ + private int minPoolSize = DEFAULT_MIN_POOL_SIZE; + + /** + * Maximum pool size. + */ + private int maxPoolSize = DEFAULT_MAX_POOL_SIZE; + + /** + * Whether the ldap connection should be validated when returned to the pool. + */ + private boolean validateOnCheckIn; + + /** + * Whether the ldap connection should be validated when given from the pool. + */ + private boolean validateOnCheckOut; + + /** + * Whether the pool should be validated periodically. + */ + private boolean validatePeriodically; + + /** + * For activating connections. + */ + private ConnectionActivator activator = connection -> true; + + /** + * For passivating connections. + */ + private ConnectionPassivator passivator = connection -> true; + + /** + * For validating connections. + */ + private ConnectionValidator validator = new SearchConnectionValidator(); + + /** + * For removing connections. + */ + private PruneStrategy pruneStrategy = new IdlePruneStrategy(); + + /** + * Connection factory to create connections with. + */ + private DefaultConnectionFactory connectionFactory; + + /** + * Whether to connect to the ldap on connection creation. + */ + private boolean connectOnCreate = true; + + /** + * Type of queue. LIFO or FIFO. + */ + private QueueType queueType = QueueType.LIFO; + + /** + * Executor for scheduling pool tasks. + */ + private ScheduledExecutorService poolExecutor; + + /** + * Whether {@link #initialize()} has been successfully invoked. + */ + private boolean initialized; + + /** + * Whether {@link #initialize()} should throw if pooling configuration requirements are not met. + */ + private boolean failFastInitialize = true; + + + /** + * Returns the name for this pool. + * + * @return pool name + */ + public String getName() { + return name; + } + + + /** + * Sets the name for this pool. + * + * @param s pool name + */ + public void setName(final String s) { + name = s; + } + + + /** + * Returns the min pool size. Default value is {@link #DEFAULT_MIN_POOL_SIZE}. This value represents the size of the + * pool after a prune has occurred. + * + * @return min pool size + */ + public int getMinPoolSize() { + return minPoolSize; + } + + + /** + * Sets the min pool size. + * + * @param size min pool size, greater than or equal to zero + */ + public void setMinPoolSize(final int size) { + if (size < 0) { + throw new IllegalArgumentException("Minimum pool size must be greater than or equal to 0 for pool " + getName()); + } + minPoolSize = size; + } + + + /** + * Returns the max pool size. Default value is {@link #DEFAULT_MAX_POOL_SIZE}. This value may or may not be strictly + * enforced depending on the pooling implementation. + * + * @return max pool size + */ + public int getMaxPoolSize() { + return maxPoolSize; + } + + + /** + * Sets the max pool size. + * + * @param size max pool size, greater than or equal to zero + */ + public void setMaxPoolSize(final int size) { + // allow a max size of zero for configurations that need to create a pool but don't want it to function + if (size < 0) { + throw new IllegalArgumentException("Maximum pool size must be greater than or equal to 0 for pool " + getName()); + } + maxPoolSize = size; + } + + + /** + * Returns the validate on check in flag. + * + * @return validate on check in + */ + public boolean isValidateOnCheckIn() { + return validateOnCheckIn; + } + + + /** + * Sets the validate on check in flag. + * + * @param b validate on check in + */ + public void setValidateOnCheckIn(final boolean b) { + validateOnCheckIn = b; + } + + + /** + * Returns the validate on check out flag. + * + * @return validate on check in + */ + public boolean isValidateOnCheckOut() { + return validateOnCheckOut; + } + + + /** + * Sets the validate on check out flag. + * + * @param b validate on check out + */ + public void setValidateOnCheckOut(final boolean b) { + validateOnCheckOut = b; + } + + + /** + * Returns the validate periodically flag. + * + * @return validate periodically + */ + public boolean isValidatePeriodically() { + return validatePeriodically; + } + + + /** + * Sets the validate periodically flag. + * + * @param b validate periodically + */ + public void setValidatePeriodically(final boolean b) { + validatePeriodically = b; + } + + + /** + * Returns the activator for this pool. + * + * @return activator + */ + public ConnectionActivator getActivator() { + return activator; + } + + + /** + * Sets the activator for this pool. + * + * @param a activator + */ + public void setActivator(final ConnectionActivator a) { + activator = a; + } + + + /** + * Returns the passivator for this pool. + * + * @return passivator + */ + public ConnectionPassivator getPassivator() { + return passivator; + } + + + /** + * Sets the passivator for this pool. + * + * @param p passivator + */ + public void setPassivator(final ConnectionPassivator p) { + passivator = p; + } + + + /** + * Returns the connection validator for this pool. + * + * @return connection validator + */ + public ConnectionValidator getValidator() { + return validator; + } + + + /** + * Sets the connection validator for this pool. + * + * @param cv connection validator + */ + public void setValidator(final ConnectionValidator cv) { + validator = cv; + } + + + /** + * Returns the prune strategy for this pool. + * + * @return prune strategy + */ + public PruneStrategy getPruneStrategy() { + return pruneStrategy; + } + + + /** + * Sets the prune strategy for this pool. + * + * @param ps prune strategy + */ + public void setPruneStrategy(final PruneStrategy ps) { + pruneStrategy = ps; + } + + + /** + * Returns the connection factory for this pool. + * + * @return connection factory + */ + public DefaultConnectionFactory getDefaultConnectionFactory() { + return connectionFactory; + } + + + /** + * Sets the connection factory for this pool. + * + * @param cf connection factory + */ + public void setDefaultConnectionFactory(final DefaultConnectionFactory cf) { + connectionFactory = cf; + } + + + /** + * Returns whether connections will attempt to connect after creation. Default is true. + * + * @return whether connections will attempt to connect after creation + */ + public boolean getConnectOnCreate() { + return connectOnCreate; + } + + + /** + * Sets whether newly created connections will attempt to connect. Default is true. + * + * @param b connect on create + */ + public void setConnectOnCreate(final boolean b) { + connectOnCreate = b; + } + + + /** + * Returns the type of queue used for this connection pool. + * + * @return queue type + */ + public QueueType getQueueType() { + return queueType; + } + + + /** + * Sets the type of queue used for this connection pool. This property may have an impact on the success of the prune + * strategy. + * + * @param type of queue + */ + public void setQueueType(final QueueType type) { + queueType = type; + } + + + /** + * Returns whether {@link #initialize()} should throw if pooling configuration requirements are not met. + * + * @return whether {@link #initialize()} should throw + */ + public boolean getFailFastInitialize() { + return failFastInitialize; + } + + + /** + * Sets whether {@link #initialize()} should throw if pooling configuration requirements are not met. + * + * @param b whether {@link #initialize()} should throw + */ + public void setFailFastInitialize(final boolean b) { + failFastInitialize = b; + } + + + /** + * Returns whether this pool has been initialized. + * + * @return whether this pool has been initialized + */ + public boolean isInitialized() { + return initialized; + } + + + /** + * Used to determine whether {@link #initialize()} has been invoked for this pool. + * + * @throws IllegalStateException if this pool has not been initialized + */ + protected void throwIfNotInitialized() { + if (!initialized) { + throw new IllegalStateException("Pool " + getName() + " is not initialized"); + } + } + + + /** + * Initialize this pool for use. + * + * @throws IllegalStateException if this pool has already been initialized, the pooling configuration is + * inconsistent or the pool does not contain at least one connection and its minimum + * size is greater than zero + */ + @Override + public synchronized void initialize() { + if (initialized) { + throw new IllegalStateException("Pool " + getName() + " has already been initialized"); + } + + if (pruneStrategy == null) { + throw new IllegalStateException("No prune strategy configured for pool " + getName()); + } + if (activator == null) { + throw new IllegalStateException("No activator configured for pool " + getName()); + } + if (passivator == null) { + throw new IllegalStateException("No passivator configured for pool " + getName()); + } + + available = new Queue<>(queueType); + active = new Queue<>(queueType); + + IllegalStateException growException = null; + try { + grow(minPoolSize, true); + } catch (IllegalStateException e) { + growException = e; + } + if (available.isEmpty() && minPoolSize > 0) { + if (failFastInitialize) { + closeAllConnections(); + throw new IllegalStateException( + "Could not initialize pool size for pool " + getName(), + growException != null ? growException.getCause() : null); + } else { + // + } + } + + final String threadPoolName = name != null ? name + "-" + getClass().getSimpleName() : getClass().getSimpleName(); + poolExecutor = Executors.newSingleThreadScheduledExecutor( + r -> { + final Thread t = new Thread(r, threadPoolName + "@" + hashCode()); + t.setDaemon(true); + return t; + }); + + poolExecutor.scheduleAtFixedRate( + () -> { + try { + prune(); + } catch (Exception e) { + // + } + }, + pruneStrategy.getPrunePeriod().toMillis(), + pruneStrategy.getPrunePeriod().toMillis(), + TimeUnit.MILLISECONDS); + + if (validatePeriodically) { + poolExecutor.scheduleAtFixedRate( + () -> { + try { + validate(); + } catch (Exception e) { + // + } + }, + validator.getValidatePeriod().toMillis(), + validator.getValidatePeriod().toMillis(), + TimeUnit.MILLISECONDS); + } + + initialized = true; + } + + + /** + * Attempts to grow the pool to the supplied size. If the pool size is greater than or equal to the supplied size, + * this method is a no-op. + * + * @param size to grow the pool to + * @param throwOnFailure whether to throw illegal state exception + * @throws IllegalStateException if the pool cannot grow to the supplied size and {@link + * #createAvailableConnection(boolean)} throws + */ + protected void grow(final int size, final boolean throwOnFailure) { + if (checkOutLock.tryLock()) { + try { + poolLock.lock(); + try { + final int currentPoolSize = active.size() + available.size(); + + final int numConnsToAdd = size - currentPoolSize; + if (numConnsToAdd > 0) { + createAvailableConnections(numConnsToAdd, throwOnFailure); + } else { + } + } finally { + poolLock.unlock(); + } + } finally { + checkOutLock.unlock(); + } + } else { + // + } + } + + + /** + * Empty this pool, freeing any resources. + * + * @throws IllegalStateException if this pool has not been initialized + */ + @Override + public synchronized void close() { + throwIfNotInitialized(); + poolLock.lock(); + try { + closeAllConnections(); + } finally { + poolLock.unlock(); + } + + poolExecutor.shutdown(); + initialized = false; + } + + + /** + * Closes all connections in the pool. + */ + private synchronized void closeAllConnections() { + poolLock.lock(); + try { + final List> removeConns = new ArrayList<>(available.size() + active.size()); + while (!available.isEmpty()) { + final PooledConnectionProxy pc = available.remove(); + removeConns.add(() -> { + pc.getConnection().close(); + return pc; + }); + } + while (!active.isEmpty()) { + final PooledConnectionProxy pc = active.remove(); + removeConns.add(() -> { + pc.getConnection().close(); + return pc; + }); + } + + if (!removeConns.isEmpty()) { + final CallableWorker callableWorker = new CallableWorker<>(getClass().getSimpleName()); + try { + final List exceptions = callableWorker.execute(removeConns, pc -> { + }); + for (ExecutionException e : exceptions) { + // + } + } finally { + callableWorker.shutdown(); + } + } + } finally { + poolLock.unlock(); + } + } + + + /** + * Returns a connection from the pool. + * + * @return connection + * @throws PoolException if this operation fails + * @throws BlockingTimeoutException if this pool is configured with a block time and it occurs + * @throws IllegalStateException if this pool has not been initialized + */ + @Override + public abstract Connection getConnection() + throws PoolException; + + + /** + * Returns a connection to the pool. + * + * @param c connection + * @throws IllegalStateException if this pool has not been initialized + */ + public abstract void putConnection(Connection c); + + + /** + * Create a new connection. If {@link #connectOnCreate} is true, the connection will be opened. + * + * @param throwOnFailure whether to throw illegal state exception + * @return pooled connection or null + * @throws IllegalStateException if {@link #connectOnCreate} is true and the connection cannot be opened + */ + protected PooledConnectionProxy createConnection(final boolean throwOnFailure) { + Connection c = connectionFactory.getConnection(); + if (connectOnCreate) { + try { + c.open(); + } catch (Exception e) { + c.close(); + c = null; + if (throwOnFailure) { + throw new IllegalStateException("Unable to open connection for pool " + getName(), e); + } + } + } + if (c != null) { + return new DefaultPooledConnectionProxy(c); + } else { + return null; + } + } + + + /** + * Asynchronously creates new connections and adds them to the available queue if the connection can be successfully + * passivated and validated. See {@link #passivateAndValidateConnection(PooledConnectionProxy)}. This method can make + * up to (count * 2) attempts in a best effort to create the number of connections requested. + * + * @param count number of connections to attempt to create + * @param throwOnFailure whether to throw illegal state exception on any connection creation failure + * @throws IllegalStateException if throwOnFailure is true and count connections are not successfully created + */ + protected void createAvailableConnections(final int count, final boolean throwOnFailure) { + poolLock.lock(); + try { + final CallableWorker callableWorker = new CallableWorker<>(getClass().getSimpleName()); + try { + final AtomicInteger createdCount = new AtomicInteger(); + final List exceptions = callableWorker.execute( + () -> { + PooledConnectionProxy pc = null; + int i = 0; + // make two attempts on each thread to open a connection + while (pc == null && i < 2) { + try { + pc = createConnection(true); + if (pc != null && connectOnCreate) { + if (!passivateAndValidateConnection(pc)) { + pc.getConnection().close(); + pc = null; + } + } + } catch (IllegalStateException e) { + if (i == 1) { + throw e; + } + pc = null; + } + i++; + } + return pc; + }, + count, + pc -> { + if (pc != null) { + available.add(pc); + pc.getPooledConnectionStatistics().addAvailableStat(); + createdCount.incrementAndGet(); + } + }); + if (createdCount.get() < count && throwOnFailure) { + if (!exceptions.isEmpty()) { + final ExecutionException e = exceptions.get(0); + if (e.getCause() instanceof IllegalStateException) { + throw (IllegalStateException) e.getCause(); + } else { + throw new IllegalStateException(e.getCause() == null ? e : e.getCause()); + } + } else { + throw new IllegalStateException("Could not create the requested number of connections"); + } + } + } finally { + callableWorker.shutdown(); + } + } finally { + poolLock.unlock(); + } + } + + + /** + * Create a new connection and place it in the available pool. + * + * @param throwOnFailure whether to throw illegal state exception + * @return connection that was placed in the available pool + * @throws IllegalStateException if {@link #createConnection(boolean)} throws + */ + protected PooledConnectionProxy createAvailableConnection(final boolean throwOnFailure) { + final PooledConnectionProxy pc = createConnection(throwOnFailure); + if (pc != null) { + poolLock.lock(); + try { + available.add(pc); + pc.getPooledConnectionStatistics().addAvailableStat(); + } finally { + poolLock.unlock(); + } + } else { + // + } + return pc; + } + + + /** + * Create a new connection and place it in the active queue. This method creates the connection and then attempts to + * acquire the pool lock in order to add the connection to the active queue. Therefore, this method can be invoked + * both with and without acquiring the pool lock. + * + * @param throwOnFailure whether to throw illegal state exception on connection creation failure + * @return connection that was placed in the active pool + * @throws IllegalStateException if {@link #createConnection(boolean)} throws + */ + protected PooledConnectionProxy createActiveConnection(final boolean throwOnFailure) { + final PooledConnectionProxy pc = createConnection(throwOnFailure); + if (pc != null) { + poolLock.lock(); + try { + active.add(pc); + pc.getPooledConnectionStatistics().addActiveStat(); + } finally { + poolLock.unlock(); + } + } else { + // + } + return pc; + } + + + /** + * Remove a connection from the available pool. + * + * @param pc connection that is in the available pool + */ + protected void removeAvailableConnection(final PooledConnectionProxy pc) { + boolean destroy = false; + poolLock.lock(); + try { + if (available.remove(pc)) { + destroy = true; + } else { + // + } + } finally { + poolLock.unlock(); + } + if (destroy) { + pc.getConnection().close(); + } + } + + + /** + * Remove a connection from the active pool. + * + * @param pc connection that is in the active pool + */ + protected void removeActiveConnection(final PooledConnectionProxy pc) { + boolean destroy = false; + poolLock.lock(); + try { + if (active.remove(pc)) { + destroy = true; + } else { + // + } + } finally { + poolLock.unlock(); + } + if (destroy) { + pc.getConnection().close(); + } + } + + + /** + * Remove a connection from both the available and active pools. + * + * @param pc connection that is in both the available and active pools + */ + protected void removeAvailableAndActiveConnection(final PooledConnectionProxy pc) { + boolean destroy = false; + poolLock.lock(); + try { + if (available.remove(pc)) { + destroy = true; + } + if (active.remove(pc)) { + destroy = true; + } + } finally { + poolLock.unlock(); + } + if (destroy) { + pc.getConnection().close(); + } + } + + + /** + * Attempts to activate and validate a connection. Performed before a connection is returned from {@link + * #getConnection()}. Validation only occurs if {@link #validateOnCheckOut} is true. If a connection fails either + * activation or validation it is removed from the pool. + * + * @param pc connection + * @throws PoolException if either activation or validation fails + */ + protected void activateAndValidateConnection(final PooledConnectionProxy pc) + throws PoolException { + if (!activator.apply(pc.getConnection())) { + removeAvailableAndActiveConnection(pc); + throw new ActivationException("Activation of connection failed for pool " + getName()); + } + if (validateOnCheckOut && !validator.apply(pc.getConnection())) { + removeAvailableAndActiveConnection(pc); + throw new ValidationException("Validation of connection failed for pool " + getName()); + } + } + + + /** + * Attempts to passivate and validate a connection. Performed when a connection is given to {@link + * #putConnection(Connection)} and when a new connection enters the pool. Validation only occurs if {@link + * #validateOnCheckIn} is true. + * + * @param pc connection + * @return whether both passivation and validation succeeded + */ + protected boolean passivateAndValidateConnection(final PooledConnectionProxy pc) { + if (!pc.getConnection().isOpen()) { + return false; + } + + boolean valid = false; + if (passivator.apply(pc.getConnection())) { + if (validateOnCheckIn) { + if (validator.apply(pc.getConnection())) { + valid = true; + } else { + // + } + } else { + valid = true; + } + } else { + // + } + return valid; + } + + + /** + * Attempts to reduce the size of the pool back to its configured minimum. + * + * @throws IllegalStateException if this pool has not been initialized + */ + public void prune() { + throwIfNotInitialized(); + poolLock.lock(); + try { + if (!available.isEmpty()) { + final int currentPoolSize = active.size() + available.size(); + if (currentPoolSize > minPoolSize) { + final int numConnAboveMin = currentPoolSize - minPoolSize; + final int numConnToPrune = available.size() < numConnAboveMin ? available.size() : numConnAboveMin; + final List> callables = new ArrayList<>(numConnToPrune); + for (PooledConnectionProxy pc : available) { + callables.add(() -> { + if (pruneStrategy.apply(pc)) { + return pc; + } + return null; + }); + } + + final AtomicInteger numConnPruned = new AtomicInteger(); + final CallableWorker callableWorker = new CallableWorker<>(getClass().getSimpleName()); + try { + final List exceptions = callableWorker.execute( + callables, + pc -> { + if (pc != null) { + if (numConnPruned.get() < numConnToPrune) { + available.remove(pc); + pc.getConnection().close(); + numConnPruned.getAndIncrement(); + } else { + // + } + } + }); + for (ExecutionException e : exceptions) { + // + } + } finally { + callableWorker.shutdown(); + } + if (numConnToPrune == available.size()) { + // + } else { + // + } + } else { + // + } + } else { + // + } + } finally { + poolLock.unlock(); + } + } + + + /** + * Attempts to validate all connections in the pool. + * + * @throws IllegalStateException if this pool has not been initialized + */ + public void validate() { + throwIfNotInitialized(); + poolLock.lock(); + try { + if (!available.isEmpty()) { + final List remove = new ArrayList<>(); + final Map> results = new HashMap<>(available.size()); + for (PooledConnectionProxy pc : available) { + results.put(pc, validator.applyAsync(pc.getConnection())); + } + for (Map.Entry> entry : results.entrySet()) { + // blocks until a result is received + final Boolean validateResult = entry.getValue().get(); + if (validateResult != null && validateResult) { + // + } else { + remove.add(entry.getKey()); + } + } + for (PooledConnectionProxy pc : remove) { + available.remove(pc); + pc.getConnection().close(); + } + } else { + // + } + grow(minPoolSize, false); + } finally { + poolLock.unlock(); + } + } + + + @Override + public int availableCount() { + if (available == null) { + return 0; + } + return available.size(); + } + + + @Override + public int activeCount() { + if (active == null) { + return 0; + } + return active.size(); + } + + + @Override + public Set getPooledConnectionStatistics() { + throwIfNotInitialized(); + + final Set stats = new HashSet<>(); + poolLock.lock(); + try { + for (PooledConnectionProxy cp : available) { + stats.add(cp.getPooledConnectionStatistics()); + } + for (PooledConnectionProxy cp : active) { + stats.add(cp.getPooledConnectionStatistics()); + } + } finally { + poolLock.unlock(); + } + return Collections.unmodifiableSet(stats); + } + + + /** + * Creates a connection proxy using the supplied pool connection. + * + * @param pc pool connection to create proxy with + * @return connection proxy + */ + protected Connection createConnectionProxy(final PooledConnectionProxy pc) { + return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[]{Connection.class}, pc); + } + + + /** + * Retrieves the invocation handler from the supplied connection proxy. + * + * @param proxy connection proxy + * @return pooled connection proxy + */ + protected PooledConnectionProxy retrieveConnectionProxy(final Connection proxy) { + return (PooledConnectionProxy) Proxy.getInvocationHandler(proxy); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "name=" + getName() + ", " + + "minPoolSize=" + minPoolSize + ", " + + "maxPoolSize=" + maxPoolSize + ", " + + "validateOnCheckIn=" + validateOnCheckIn + ", " + + "validateOnCheckOut=" + validateOnCheckOut + ", " + + "validatePeriodically=" + validatePeriodically + ", " + + "activator=" + activator + ", " + + "passivator=" + passivator + ", " + + "validator=" + validator + ", " + + "pruneStrategy=" + pruneStrategy + ", " + + "connectOnCreate=" + connectOnCreate + ", " + + "connectionFactory=" + connectionFactory + ", " + + "failFastInitialize=" + failFastInitialize + ", " + + "initialized=" + initialized + ", " + + "availableCount=" + availableCount() + ", " + + "activeCount=" + activeCount(); + } + + + /** + * Contains a connection that is participating in this pool. Used to track how long a connection has been in use and + * override certain method invocations. + */ + protected class DefaultPooledConnectionProxy implements PooledConnectionProxy { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 503; + + /** + * Underlying connection. + */ + private final Connection conn; + + /** + * Time this connection was created. + */ + private final long createdTime = System.currentTimeMillis(); + + /** + * Statistics for this connection. + */ + private final PooledConnectionStatistics statistics = new PooledConnectionStatistics( + pruneStrategy.getStatisticsSize()); + + + /** + * Creates a new pooled connection. + * + * @param c connection to participate in this pool + */ + public DefaultPooledConnectionProxy(final Connection c) { + conn = c; + } + + + @Override + public ConnectionPool getConnectionPool() { + return AbstractConnectionPool.this; + } + + + @Override + public Connection getConnection() { + return conn; + } + + + @Override + public long getCreatedTime() { + return createdTime; + } + + + @Override + public PooledConnectionStatistics getPooledConnectionStatistics() { + return statistics; + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof DefaultPooledConnectionProxy v) { + return LdapUtils.areEqual(conn, v.conn); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, conn); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "conn=" + conn + ", " + + "createdTime=" + createdTime + ", " + + "statistics=" + statistics; + } + + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) + throws Throwable { + Object retValue = null; + if ("open".equals(method.getName())) { + // if the connection has been closed, invoke open + if (!conn.isOpen()) { + try { + retValue = method.invoke(conn, args); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } + } else if ("close".equals(method.getName())) { + putConnection((Connection) proxy); + } else { + try { + retValue = method.invoke(conn, args); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } + return retValue; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/AbstractPruneStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/AbstractPruneStrategy.java new file mode 100644 index 0000000..a79bc2d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/AbstractPruneStrategy.java @@ -0,0 +1,95 @@ + +package org.xbib.net.ldap.pool; + +import java.time.Duration; + +/** + * Base class for prune strategy implementations. + * + */ +public abstract class AbstractPruneStrategy implements PruneStrategy { + + /** + * Default prune period in seconds. Value is 5 minutes. + */ + protected static final Duration DEFAULT_PRUNE_PERIOD = Duration.ofMinutes(5); + + /** + * Prune period. + */ + private Duration prunePeriod; + + + @Override + public Duration getPrunePeriod() { + return prunePeriod; + } + + + /** + * Sets the prune period. + * + * @param period to set + */ + public void setPrunePeriod(final Duration period) { + if (period == null || period.isNegative() || period.isZero()) { + throw new IllegalArgumentException("Prune period cannot be null, negative or zero"); + } + prunePeriod = period; + } + + + /** + * Base class for prune strategy builders. + * + * @param type of builder + * @param type of validator + */ + protected abstract static class AbstractBuilder { + + /** + * Prune strategy to build. + */ + protected final T object; + + + /** + * Creates a new abstract builder. + * + * @param t validator to build + */ + protected AbstractBuilder(final T t) { + object = t; + } + + + /** + * Returns this builder. + * + * @return builder + */ + protected abstract B self(); + + + /** + * Sets the prune period. + * + * @param period to set + * @return this builder + */ + public B period(final Duration period) { + object.setPrunePeriod(period); + return self(); + } + + + /** + * Returns the prune strategy. + * + * @return prune strategy + */ + public T build() { + return object; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/ActivationException.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ActivationException.java new file mode 100644 index 0000000..5e2f8df --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ActivationException.java @@ -0,0 +1,45 @@ + +package org.xbib.net.ldap.pool; + +/** + * Thrown when an attempt to activate a pooled connection fails. + * + */ +public class ActivationException extends PoolException { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = 5547712224386623996L; + + + /** + * Creates a new activation exception. + * + * @param msg describing this exception + */ + public ActivationException(final String msg) { + super(msg); + } + + + /** + * Creates a new activation exception. + * + * @param e pooling specific exception + */ + public ActivationException(final Exception e) { + super(e); + } + + + /** + * Creates a new activation exception. + * + * @param msg describing this exception + * @param e pooling specific exception + */ + public ActivationException(final String msg, final Exception e) { + super(msg, e); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/BindConnectionPassivator.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/BindConnectionPassivator.java new file mode 100644 index 0000000..f5caf2c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/BindConnectionPassivator.java @@ -0,0 +1,77 @@ + +package org.xbib.net.ldap.pool; + +import org.xbib.net.ldap.AnonymousBindRequest; +import org.xbib.net.ldap.BindRequest; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.Result; + +/** + * Passivates a connection by performing a bind operation on it. + * + */ +public class BindConnectionPassivator implements ConnectionPassivator { + + /** + * Bind request to perform passivation with. + */ + private BindRequest bindRequest; + + + /** + * Creates a new bind passivator. + */ + public BindConnectionPassivator() { + this(new AnonymousBindRequest()); + } + + + /** + * Creates a new bind passivator. + * + * @param br to use for binds + */ + public BindConnectionPassivator(final BindRequest br) { + bindRequest = br; + } + + + /** + * Returns the bind request. + * + * @return bind request + */ + public BindRequest getBindRequest() { + return bindRequest; + } + + + /** + * Sets the bind request. + * + * @param br bind request + */ + public void setBindRequest(final BindRequest br) { + bindRequest = br; + } + + + @Override + public Boolean apply(final Connection conn) { + if (conn != null) { + try { + final Result result = conn.operation(bindRequest).execute(); + return result.isSuccess(); + } catch (Exception e) { + // + } + } + return false; + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "bindRequest=" + bindRequest + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/BlockingConnectionPool.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/BlockingConnectionPool.java new file mode 100644 index 0000000..75586e3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/BlockingConnectionPool.java @@ -0,0 +1,229 @@ + +package org.xbib.net.ldap.pool; + +import java.time.Duration; +import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.DefaultConnectionFactory; + +/** + * Implements a pool of connections that has a set minimum and maximum size. The pool will not grow beyond the maximum + * size and when the pool is exhausted, requests for new connections will block. The length of time the pool will block + * is determined by {@link #getBlockWaitTime()}. By default, the pool will block for 1 minute and there is no guarantee + * that waiting threads will be serviced in the order in which they made their request. This implementation should be + * used when you need to control the exact number of connections that can be created. See {@link + * AbstractConnectionPool}. + * + */ +public class BlockingConnectionPool extends AbstractConnectionPool { + + /** + * Duration to wait for an available connection. + */ + private Duration blockWaitTime = Duration.ofMinutes(1); + + + /** + * Creates a new blocking pool. + */ + public BlockingConnectionPool() { + } + + + /** + * Creates a new blocking pool. + * + * @param cf connection factory + */ + public BlockingConnectionPool(final DefaultConnectionFactory cf) { + setDefaultConnectionFactory(cf); + } + + + /** + * Returns the block wait time. Default time is 1 minute. + * + * @return time to wait for available connections + */ + public Duration getBlockWaitTime() { + return blockWaitTime; + } + + + /** + * Sets the block wait time. Default time is 1 minute. + * + * @param time to wait for available connections + */ + public void setBlockWaitTime(final Duration time) { + if (time == null || time.isNegative()) { + throw new IllegalArgumentException("Block wait time cannot be null or negative"); + } + blockWaitTime = time; + } + + + @Override + public Connection getConnection() + throws PoolException { + throwIfNotInitialized(); + + PooledConnectionProxy pc = null; + boolean create = false; + poolLock.lock(); + try { + // if an available connection exists, use it + // if no available connections and the pool can grow, attempt to create + // otherwise the pool is full, block until a connection is returned + if (!available.isEmpty()) { + try { + pc = retrieveAvailableConnection(); + } catch (NoSuchElementException e) { + throw new IllegalStateException("Pool is empty", e); + } + } else if (active.size() < getMaxPoolSize()) { + create = true; + } else { + pc = blockAvailableConnection(); + } + } finally { + poolLock.unlock(); + } + + if (create) { + // previous block determined a creation should occur + // block here until create occurs without locking the whole pool + // if the pool is already maxed or creates are failing, + // block until a connection is available + try { + if (Duration.ZERO.equals(blockWaitTime)) { + checkOutLock.lock(); + } else { + if (!checkOutLock.tryLock(blockWaitTime.toMillis(), TimeUnit.MILLISECONDS)) { + throw new BlockingTimeoutException( + "Block time of " + blockWaitTime + " exceeded waiting for check out on pool " + getName() + + " with max size of " + getMaxPoolSize()); + } + } + try { + boolean b = true; + poolLock.lock(); + try { + if (available.size() + active.size() == getMaxPoolSize()) { + b = false; + } + } finally { + poolLock.unlock(); + } + if (b) { + pc = createActiveConnection(false); + } + } finally { + checkOutLock.unlock(); + } + } catch (InterruptedException e) { + throw new PoolException("Interrupted while waiting to create a connection", e); + } + if (pc == null) { + if (available.isEmpty() && active.isEmpty()) { + throw new PoolExhaustedException("Pool is empty and connection creation failed"); + } + pc = blockAvailableConnection(); + } else { + } + } + + if (pc != null) { + activateAndValidateConnection(pc); + } else { + throw new PoolExhaustedException("Pool is empty and connection creation failed"); + } + + return createConnectionProxy(pc); + } + + + /** + * Attempts to retrieve a connection from the available queue. + * + * @return connection from the pool + * @throws NoSuchElementException if the available queue is empty + */ + protected PooledConnectionProxy retrieveAvailableConnection() { + final PooledConnectionProxy pc; + poolLock.lock(); + try { + pc = available.remove(); + active.add(pc); + pc.getPooledConnectionStatistics().addActiveStat(); + } finally { + poolLock.unlock(); + } + return pc; + } + + + /** + * This blocks until a connection can be acquired. + * + * @return connection from the pool + * @throws PoolException if this method fails + * @throws BlockingTimeoutException if this pool is configured with a block time and it occurs + */ + protected PooledConnectionProxy blockAvailableConnection() + throws PoolException { + PooledConnectionProxy pc = null; + poolLock.lock(); + try { + while (pc == null) { + if (Duration.ZERO.equals(blockWaitTime)) { + poolNotEmpty.await(); + } else { + if (!poolNotEmpty.await(blockWaitTime.toMillis(), TimeUnit.MILLISECONDS)) { + throw new BlockingTimeoutException( + "Block time of " + blockWaitTime + " exceeded waiting for connection on pool " + getName() + + " with max size of " + getMaxPoolSize()); + } + } + try { + pc = retrieveAvailableConnection(); + } catch (NoSuchElementException e) { + // + } + } + } catch (InterruptedException e) { + throw new PoolException("Interrupted while waiting for an available connection", e); + } finally { + poolLock.unlock(); + } + return pc; + } + + + @Override + public void putConnection(final Connection c) { + throwIfNotInitialized(); + + final PooledConnectionProxy pc = retrieveConnectionProxy(c); + final boolean valid = passivateAndValidateConnection(pc); + poolLock.lock(); + try { + if (!valid) { + removeAvailableAndActiveConnection(pc); + } else if (active.remove(pc)) { + available.add(pc); + pc.getPooledConnectionStatistics().addAvailableStat(); + poolNotEmpty.signal(); + } + } finally { + poolLock.unlock(); + } + } + + + @Override + public String toString() { + return super.toString() + ", " + "blockWaitTime=" + blockWaitTime; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/BlockingTimeoutException.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/BlockingTimeoutException.java new file mode 100644 index 0000000..f252a17 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/BlockingTimeoutException.java @@ -0,0 +1,45 @@ + +package org.xbib.net.ldap.pool; + +/** + * Thrown when a blocking operation times out. See {@link ConnectionPool#getConnection()}. + * + */ +public class BlockingTimeoutException extends PoolException { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = 6013765020562222482L; + + + /** + * Creates a new blocking timeout exception. + * + * @param msg describing this exception + */ + public BlockingTimeoutException(final String msg) { + super(msg); + } + + + /** + * Creates a new blocking timeout exception. + * + * @param e pooling specific exception + */ + public BlockingTimeoutException(final Exception e) { + super(e); + } + + + /** + * Creates a new blocking timeout exception. + * + * @param msg describing this exception + * @param e pooling specific exception + */ + public BlockingTimeoutException(final String msg, final Exception e) { + super(msg, e); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionActivator.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionActivator.java new file mode 100644 index 0000000..540ba4a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionActivator.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.pool; + +import java.util.function.Function; +import org.xbib.net.ldap.Connection; + +/** + * Provides an interface for activating connections when they are checked out from the pool. + * + */ +public interface ConnectionActivator extends Function { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionPassivator.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionPassivator.java new file mode 100644 index 0000000..0336400 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionPassivator.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.pool; + +import java.util.function.Function; +import org.xbib.net.ldap.Connection; + +/** + * Provides an interface for passivating connections when they are checked back into the pool. + * + */ +public interface ConnectionPassivator extends Function { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionPool.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionPool.java new file mode 100644 index 0000000..1d17d89 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ConnectionPool.java @@ -0,0 +1,91 @@ + +package org.xbib.net.ldap.pool; + +import java.util.Set; +import org.xbib.net.ldap.Connection; + +/** + * Provides an interface for connection pooling. + * + */ +public interface ConnectionPool { + + + /** + * Returns the activator for this pool. + * + * @return activator + */ + ConnectionActivator getActivator(); + + + /** + * Sets the activator for this pool. + * + * @param a activator + */ + void setActivator(ConnectionActivator a); + + + /** + * Returns the passivator for this pool. + * + * @return passivator + */ + ConnectionPassivator getPassivator(); + + + /** + * Sets the passivator for this pool. + * + * @param p passivator + */ + void setPassivator(ConnectionPassivator p); + + + /** + * Initialize this pool for use. + */ + void initialize(); + + + /** + * Returns an object from the pool. + * + * @return pooled object + * @throws PoolException if this operation fails + * @throws BlockingTimeoutException if this pool is configured with a block time and it occurs + */ + Connection getConnection() + throws PoolException; + + + /** + * Returns the number of connections available for use. + * + * @return count + */ + int availableCount(); + + + /** + * Returns the number of connections in use. + * + * @return count + */ + int activeCount(); + + + /** + * Returns the statistics for each connection in the pool. + * + * @return connection statistics + */ + Set getPooledConnectionStatistics(); + + + /** + * Empty this pool, freeing any resources. + */ + void close(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/IdlePruneStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/IdlePruneStrategy.java new file mode 100644 index 0000000..0b65ec6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/IdlePruneStrategy.java @@ -0,0 +1,141 @@ + +package org.xbib.net.ldap.pool; + +import java.time.Duration; +import java.time.Instant; + +/** + * Removes connections from the pool based on how long they have been idle in the available queue. By default, this + * implementation executes every 5 minutes and prunes connections that have been idle for more than 10 minutes. + * + */ +public class IdlePruneStrategy extends AbstractPruneStrategy { + + /** + * Default number of statistics to store. Value is {@value}. + */ + private static final int DEFAULT_STATISTICS_SIZE = 1; + + /** + * Default idle time. Value is 10 minutes. + */ + private static final Duration DEFAULT_IDLE_TIME = Duration.ofMinutes(10); + + /** + * Idle time. + */ + private Duration idleTime; + + + /** + * Creates a new idle prune strategy. + */ + public IdlePruneStrategy() { + this(DEFAULT_PRUNE_PERIOD, DEFAULT_IDLE_TIME); + } + + + /** + * Creates a new idle prune strategy. Sets the prune period to half of the supplied idle time. + * + * @param idle time at which a connection should be pruned + */ + public IdlePruneStrategy(final Duration idle) { + setPrunePeriod(idle.dividedBy(2)); + setIdleTime(idle); + } + + + /** + * Creates a new idle prune strategy. + * + * @param period to execute the prune task + * @param idle time at which a connection should be pruned + */ + public IdlePruneStrategy(final Duration period, final Duration idle) { + setPrunePeriod(period); + setIdleTime(idle); + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static IdlePruneStrategy.Builder builder() { + return new IdlePruneStrategy.Builder(); + } + + @Override + public Boolean apply(final PooledConnectionProxy conn) { + final Instant timeAvailable = conn.getPooledConnectionStatistics().getLastAvailableStat(); + return timeAvailable == null || timeAvailable.plus(idleTime).isBefore(Instant.now()); + } + + @Override + public int getStatisticsSize() { + return DEFAULT_STATISTICS_SIZE; + } + + /** + * Returns the idle time. + * + * @return idle time + */ + public Duration getIdleTime() { + return idleTime; + } + + /** + * Sets the idle time. + * + * @param time that a connection has been idle and should be pruned + */ + public void setIdleTime(final Duration time) { + if (time == null || time.isNegative()) { + throw new IllegalArgumentException("Idle time cannot be null or negative"); + } + idleTime = time; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "prunePeriod=" + getPrunePeriod() + ", " + + "idleTime=" + idleTime + "]"; + } + + /** + * Idle prune strategy builder. + */ + public static class Builder extends + AbstractPruneStrategy.AbstractBuilder { + + + /** + * Creates a new builder. + */ + protected Builder() { + super(new IdlePruneStrategy()); + } + + + @Override + protected IdlePruneStrategy.Builder self() { + return this; + } + + + /** + * Sets the prune idle time. + * + * @param time to set + * @return this builder + */ + public IdlePruneStrategy.Builder idle(final Duration time) { + object.setIdleTime(time); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/PoolException.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PoolException.java new file mode 100644 index 0000000..14f9321 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PoolException.java @@ -0,0 +1,47 @@ + +package org.xbib.net.ldap.pool; + +import org.xbib.net.ldap.LdapException; + +/** + * Base exception thrown when a pool operation fails. + * + */ +public class PoolException extends LdapException { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = 6320399208563015506L; + + + /** + * Creates a new pool exception. + * + * @param msg describing this exception + */ + public PoolException(final String msg) { + super(msg); + } + + + /** + * Creates a new pool exception. + * + * @param e pooling specific exception + */ + public PoolException(final Exception e) { + super(e); + } + + + /** + * Creates a new pool exception. + * + * @param msg describing this exception + * @param e pooling specific exception + */ + public PoolException(final String msg, final Exception e) { + super(msg, e); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/PoolExhaustedException.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PoolExhaustedException.java new file mode 100644 index 0000000..bd40104 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PoolExhaustedException.java @@ -0,0 +1,45 @@ + +package org.xbib.net.ldap.pool; + +/** + * Thrown when the pool is empty and no new requests can be serviced. + * + */ +public class PoolExhaustedException extends PoolException { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = -2092251274513447389L; + + + /** + * Creates a new pool exhausted exception. + * + * @param msg describing this exception + */ + public PoolExhaustedException(final String msg) { + super(msg); + } + + + /** + * Creates a new pool exhausted exception. + * + * @param e pooling specific exception + */ + public PoolExhaustedException(final Exception e) { + super(e); + } + + + /** + * Creates a new pool exhausted exception. + * + * @param msg describing this exception + * @param e pooling specific exception + */ + public PoolExhaustedException(final String msg, final Exception e) { + super(msg, e); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/PooledConnectionProxy.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PooledConnectionProxy.java new file mode 100644 index 0000000..7594337 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PooledConnectionProxy.java @@ -0,0 +1,44 @@ + +package org.xbib.net.ldap.pool; + +import java.lang.reflect.InvocationHandler; +import org.xbib.net.ldap.Connection; + +/** + * Provides an interface for metadata surrounding a connection that is participating in the connection pool. + * + */ +public interface PooledConnectionProxy extends InvocationHandler { + + + /** + * Returns the connection pool that this proxy is participating in. + * + * @return connection pool + */ + ConnectionPool getConnectionPool(); + + + /** + * Returns the connection that is being proxied. + * + * @return underlying connection + */ + Connection getConnection(); + + + /** + * Returns the time this proxy was created. + * + * @return creation timestamp in milliseconds + */ + long getCreatedTime(); + + + /** + * Returns the statistics associated with this connection's activity in the pool. + * + * @return pooled connection statistics + */ + PooledConnectionStatistics getPooledConnectionStatistics(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/PooledConnectionStatistics.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PooledConnectionStatistics.java new file mode 100644 index 0000000..ec098a6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PooledConnectionStatistics.java @@ -0,0 +1,153 @@ + +package org.xbib.net.ldap.pool; + +import java.time.Instant; +import java.util.Deque; +import java.util.LinkedList; + +/** + * Statistics associated with a connection's activity in the pool. Exposes the timestamps when this connection entered + * both the available pool and the active pool. A size of 512 uses approximately 50 kilobytes of memory per connection. + * + */ +public class PooledConnectionStatistics { + + /** + * Number of available and active timestamps to store. + */ + private final int size; + + /** + * Available stats. + */ + private final Deque availableStats; + + /** + * Active stats. + */ + private final Deque activeStats; + + + /** + * Creates a new pooled connection statistics. + * + * @param i number of timestamps to store + */ + public PooledConnectionStatistics(final int i) { + size = i; + availableStats = new LinkedList<>() { + + + @Override + public boolean add(final Instant e) { + if (size < 1) { + return false; + } + + final boolean b = super.add(e); + while (size() > size) { + remove(); + } + return b; + } + }; + activeStats = new LinkedList<>() { + + + @Override + public boolean add(final Instant e) { + if (size < 1) { + return false; + } + + final boolean b = super.add(e); + while (size() > size) { + remove(); + } + return b; + } + }; + } + + + /** + * Returns all the available timestamp statistics. + * + * @return available timestamp statistics + */ + public Deque getAvailableStats() { + return availableStats; + } + + + /** + * Returns the last timestamp at which this connection was made available. + * + * @return millisecond timestamp + */ + public Instant getLastAvailableStat() { + return availableStats.peekLast(); + } + + + /** + * Inserts the current timestamp into the available statistics. + */ + public synchronized void addAvailableStat() { + availableStats.add(Instant.now()); + } + + + /** + * Inserts the supplied timestamp into the available statistics. This method is intended for testing. + * + * @param instant to add + */ + synchronized void addAvailableStat(final Instant instant) { + availableStats.add(instant); + } + + + /** + * Returns all the active timestamp statistics. + * + * @return active timestamp statistics + */ + public Deque getActiveStats() { + return activeStats; + } + + + /** + * Returns the last timestamp at which this connection was made active. + * + * @return millisecond timestamp + */ + public Instant getLastActiveStat() { + return activeStats.peekLast(); + } + + + /** + * Inserts the current timestamp into the active statistics. + */ + public synchronized void addActiveStat() { + activeStats.add(Instant.now()); + } + + + /** + * Inserts the supplied timestamp into the active statistics. This method is intended for testing. + * + * @param instant to add + */ + synchronized void addActiveStat(final Instant instant) { + activeStats.add(instant); + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "size=" + size + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/PruneStrategy.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PruneStrategy.java new file mode 100644 index 0000000..eba3d25 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/PruneStrategy.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.pool; + +import java.time.Duration; +import java.util.function.Function; + +/** + * Provides an interface for pruning connections from the pool. + * + */ +public interface PruneStrategy extends Function { + + + /** + * Returns the number of statistics to store for this prune strategy. See {@link PooledConnectionStatistics}. + * + * @return number of statistics to store + */ + int getStatisticsSize(); + + + /** + * Returns the interval at which the prune task will be executed. + * + * @return prune period + */ + Duration getPrunePeriod(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/Queue.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/Queue.java new file mode 100644 index 0000000..72d5750 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/Queue.java @@ -0,0 +1,129 @@ + +package org.xbib.net.ldap.pool; + +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * Provides a wrapper around a {@link Deque} to support LIFO and FIFO operations. + * + * @param type of object in the queue + */ +public class Queue implements Iterable { + + /** + * How will objects be inserted into the queue. + */ + private final QueueType queueType; + + /** + * Underlying queue. + */ + private final Deque queue; + + + /** + * Creates a new queue. + * + * @param type how will objects be inserted into the queue + */ + public Queue(final QueueType type) { + queueType = type; + queue = new LinkedList<>(); + } + + + /** + * Adds an object to the queue based on the queue type. See {@link Deque#addFirst(Object)} and {@link + * Deque#addLast(Object)}. + * + * @param t to add + */ + public void add(final T t) { + if (QueueType.LIFO == queueType) { + queue.addFirst(t); + } else if (QueueType.FIFO == queueType) { + queue.addLast(t); + } else { + throw new IllegalStateException("Unknown queue type: " + queueType); + } + } + + + /** + * Removes the first element in the queue. See {@link Deque#removeFirst()}. + * + * @return first element in the queue + */ + public T remove() { + return queue.removeFirst(); + } + + + /** + * Removes the supplied element from the queue. See {@link Deque#remove(Object)}. + * + * @param t to remove + * @return whether t was removed + */ + public boolean remove(final T t) { + return queue.remove(t); + } + + + /** + * Retrieves, but does not remove, the first element in the queue. See {@link Deque#getFirst()}. + * + * @return first element in the queue + */ + public T element() { + return queue.getFirst(); + } + + + /** + * Returns whether t is in the queue. See {@link Deque#contains(Object)}. + * + * @param t that may be in the queue + * @return whether t is in the queue + */ + public boolean contains(final T t) { + return queue.contains(t); + } + + + /** + * Returns whether or not the queue is empty. See {@link Deque#isEmpty()}}. + * + * @return whether the queue is empty + */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + + /** + * Returns the number of elements in the queue. See {@link Deque#size()}. + * + * @return number of elements in the queue + */ + public int size() { + return queue.size(); + } + + + @Override + public Iterator iterator() { + return queue.iterator(); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "queueType=" + queueType + ", " + + "queue=" + queue + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/QueueType.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/QueueType.java new file mode 100644 index 0000000..7556b22 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/QueueType.java @@ -0,0 +1,19 @@ + +package org.xbib.net.ldap.pool; + +/** + * Enum to define queue type. + * + */ +public enum QueueType { + + /** + * first in, first out ordering. + */ + FIFO, + + /** + * last in, first out ordering. + */ + LIFO +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/ValidationException.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ValidationException.java new file mode 100644 index 0000000..91dc263 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ValidationException.java @@ -0,0 +1,45 @@ + +package org.xbib.net.ldap.pool; + +/** + * Thrown when an attempt to validate a pooled connection fails. + * + */ +public class ValidationException extends PoolException { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = -5043560632396467010L; + + + /** + * Creates a new validation exception. + * + * @param msg describing this exception + */ + public ValidationException(final String msg) { + super(msg); + } + + + /** + * Creates a new validation exception. + * + * @param e pooling specific exception + */ + public ValidationException(final Exception e) { + super(e); + } + + + /** + * Creates a new validation exception. + * + * @param msg describing this exception + * @param e pooling specific exception + */ + public ValidationException(final String msg, final Exception e) { + super(msg, e); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/pool/ValidationExceptionHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ValidationExceptionHandler.java new file mode 100644 index 0000000..0986b86 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/pool/ValidationExceptionHandler.java @@ -0,0 +1,12 @@ + +package org.xbib.net.ldap.pool; + +import java.util.function.Function; +import org.xbib.net.ldap.Connection; + +/** + * Marker interface for a validation exception handler. + * + */ +public interface ValidationExceptionHandler extends Function { +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractConnectionFactoryManagerPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractConnectionFactoryManagerPropertySource.java new file mode 100644 index 0000000..e62dd9b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractConnectionFactoryManagerPropertySource.java @@ -0,0 +1,67 @@ + +package org.xbib.net.ldap.props; + +import java.util.Properties; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.ConnectionFactoryManager; +import org.xbib.net.ldap.DefaultConnectionFactory; +import org.xbib.net.ldap.PooledConnectionFactory; + +/** + * Property source for classes that contain a connection factory. + * + * @param type of connection factory manager + */ +public abstract class AbstractConnectionFactoryManagerPropertySource + extends AbstractPropertySource { + + + /** + * Creates a new search dn resolver property source. + * + * @param resolver search dn resolver to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public AbstractConnectionFactoryManagerPropertySource( + final T resolver, + final PropertyDomain domain, + final Properties props) { + super(resolver, domain, props); + } + + + @Override + public void initialize() { + ConnectionFactory cf = object.getConnectionFactory(); + if (cf == null) { + cf = new DefaultConnectionFactory(); + final DefaultConnectionFactoryPropertySource cfPropSource = new DefaultConnectionFactoryPropertySource( + (DefaultConnectionFactory) cf, + propertiesDomain, + properties); + cfPropSource.initialize(); + object.setConnectionFactory(cf); + } else { + if (cf instanceof DefaultConnectionFactory) { + final DefaultConnectionFactoryPropertySource cfPropSource = new DefaultConnectionFactoryPropertySource( + (DefaultConnectionFactory) cf, + propertiesDomain, + properties); + cfPropSource.initialize(); + } else if (cf instanceof PooledConnectionFactory) { + final PooledConnectionFactoryPropertySource cfPropSource = new PooledConnectionFactoryPropertySource( + (PooledConnectionFactory) cf, + propertiesDomain, + properties); + cfPropSource.initialize(); + } else { + final SimplePropertySource sPropSource = new SimplePropertySource<>( + cf, + propertiesDomain, + properties); + sPropSource.initialize(); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractPropertyInvoker.java new file mode 100644 index 0000000..4d0f624 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractPropertyInvoker.java @@ -0,0 +1,376 @@ + +package org.xbib.net.ldap.props; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Provides methods common to property invokers. + * + */ +public abstract class AbstractPropertyInvoker implements PropertyInvoker { + + /** + * Cache of properties. + */ + private static final Map> PROPERTIES_CACHE = new HashMap<>(); + + /** + * Class to invoke methods on. + */ + private Class clazz; + + /** + * Map of all properties to their getter and setter methods. + */ + private Map properties; + + /** + * Creates an instance of the supplied type. + * + * @param type of class returned + * @param type of class to create + * @param className to create + * @return class of type T + * @throws IllegalArgumentException if the supplied class name cannot create a new instance of T + */ + public static T instantiateType(final T type, final String className) { + try { + try { + final Class clazz = createClass(className); + final Constructor con = clazz.getDeclaredConstructor((Class[]) null); + @SuppressWarnings("unchecked") final T t = (T) con.newInstance(); + return t; + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + } catch (RuntimeException e) { + throw new IllegalArgumentException("Error instantiating type " + type + " using " + className, e); + } + } + + /** + * Creates the class with the supplied name. + * + * @param className to create + * @return class + * @throws IllegalArgumentException if the supplied class name cannot be created + */ + public static Class createClass(final String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(String.format("Could not find class '%s'", className), e); + } + } + + /** + * Returns the enum for the supplied type and value. + * + * @param clazz of the enum + * @param value of the enum + * @return enum that matches the supplied value + */ + protected static Enum getEnum(final Class clazz, final String value) { + for (Object o : clazz.getEnumConstants()) { + final Enum e = (Enum) o; + if (e.name().equals(value)) { + return e; + } + } + throw new IllegalArgumentException(String.format("Unknown enum value %s for %s", value, clazz)); + } + + /** + * Invokes the supplied method on the supplied object with the supplied argument. + * + * @param method to invoke + * @param object to invoke method on + * @param arg to invoke method with + * @return object produced by the invocation + * @throws IllegalArgumentException if an error occurs invoking the method + */ + public static Object invokeMethod(final Method method, final Object object, final Object arg) { + try { + try { + Object[] params = new Object[]{arg}; + if (arg == null && method.getParameterTypes().length == 0) { + params = null; + } + return method.invoke(object, params); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + } catch (RuntimeException e) { + throw new IllegalArgumentException("Error invoking " + method + " on " + object + " with param " + arg, e); + } + } + + /** + * Initializes the properties cache with the supplied class. The cache contains a map of properties to an array of the + * setter and getter methods. If a method named 'initialize' is found, it is also cached. + * + * @param c to read methods from + */ + protected synchronized void initialize(final Class c) { + final String cacheKey = c.getName(); + if (PROPERTIES_CACHE.containsKey(cacheKey)) { + properties = PROPERTIES_CACHE.get(cacheKey); + } else { + properties = new HashMap<>(); + // look for get, is, and initialize + for (Method method : c.getMethods()) { + if (!method.isBridge()) { + if (method.getName().startsWith("get") && method.getParameterTypes().length == 0) { + final String mName = method.getName().substring(3); + final String pName = Character.toLowerCase(mName.charAt(0)) + mName.substring(1); + if (properties.containsKey(pName)) { + final Method[] m = properties.get(pName); + m[0] = method; + } else { + properties.put(pName, new Method[]{method, null}); + } + } else if (method.getName().startsWith("is") && method.getParameterTypes().length == 0) { + final String mName = method.getName().substring(2); + final String pName = Character.toLowerCase(mName.charAt(0)) + mName.substring(1); + if (properties.containsKey(pName)) { + final Method[] m = properties.get(pName); + // prefer the first method we find + if (m[0] == null) { + m[0] = method; + } + } else { + properties.put(pName, new Method[]{method, null}); + } + } else if ("initialize".equals(method.getName()) && method.getParameterTypes().length == 0) { + final String pName = method.getName(); + properties.put(pName, new Method[]{method, method}); + } + } + } + // look for setters + for (Method method : c.getMethods()) { + if (!method.isBridge()) { + if (method.getName().startsWith("set") && method.getParameterTypes().length == 1) { + final String mName = method.getName().substring(3); + final String pName = Character.toLowerCase(mName.charAt(0)) + mName.substring(1); + if (properties.containsKey(pName)) { + final Method[] m = properties.get(pName); + if (m[0] != null && method.getParameterTypes()[0].equals(m[0].getReturnType())) { + m[1] = method; + } + } + } + } + } + + // remove any properties that don't have both getters and setters + properties.values().removeIf(method -> method[0] == null || method[1] == null); + + PROPERTIES_CACHE.put(cacheKey, Collections.unmodifiableMap(properties)); + } + clazz = c; + } + + /** + * This invokes the setter method for the supplied property name with the supplied value. + * + * @param object to invoke method on + * @param name of the property + * @param value of the property + * @throws IllegalArgumentException if an invocation exception occurs + */ + @Override + public void setProperty(final Object object, final String name, final String value) { + if (!clazz.isInstance(object)) { + throw new IllegalArgumentException( + "Illegal attempt to set property for class " + clazz.getName() + " on object of type " + + object.getClass().getName()); + } + + final Method getter = properties.get(name) != null ? properties.get(name)[0] : null; + if (getter == null) { + throw new IllegalArgumentException("No getter method found for " + name + " on object " + clazz.getName()); + } + + final Method setter = properties.get(name) != null ? properties.get(name)[1] : null; + if (setter == null) { + throw new IllegalArgumentException("No setter method found for " + name + " on object " + clazz.getName()); + } + + invokeMethod(setter, object, convertValue(getter.getReturnType(), value)); + } + + /** + * Converts the supplied string value into an Object of the supplied type. If value cannot be converted it is returned + * as is. + * + * @param type of object to convert value into + * @param value to parse + * @return object of the supplied type + */ + protected abstract Object convertValue(Class type, String value); + + /** + * Returns whether the supplied property exists for this invoker. + * + * @param name to check + * @return whether the supplied property exists + */ + @Override + public boolean hasProperty(final String name) { + return properties.containsKey(name); + } + + /** + * Returns the property keys for this invoker. + * + * @return set of property names + */ + @Override + public Set getProperties() { + return Collections.unmodifiableSet(properties.keySet()); + } + + /** + * Converts simple types that are common to all property invokers. If value cannot be converted it is returned as is. + * + * @param type of object to convert value into + * @param value to parse + * @return object of the supplied type + */ + protected Object convertSimpleType(final Class type, final String value) { + Object newValue = value; + if (Class.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(Class.class, value); + } else if (Class[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(Class.class, value); + } else if (type.isEnum()) { + newValue = getEnum(type, value); + } else if (type.isArray() && type.getComponentType().isEnum()) { + newValue = createArrayEnumFromPropertyValue(type.getComponentType(), value); + } else if (String[].class == type) { + newValue = value.split(","); + } else if (Object[].class == type) { + newValue = value.split(","); + } else if (Map.class == type) { + newValue = Stream.of(value.split(",")).map(s -> s.split("=")).collect(Collectors.toMap(v -> v[0], v -> v[1])); + } else if (float.class == type || Float.class == type) { + newValue = Float.parseFloat(value); + } else if (int.class == type || Integer.class == type) { + newValue = Integer.parseInt(value); + } else if (long.class == type || Long.class == type) { + newValue = Long.parseLong(value); + } else if (short.class == type || Short.class == type) { + newValue = Short.parseShort(value); + } else if (double.class == type || Double.class == type) { + newValue = Double.parseDouble(value); + } else if (boolean.class == type || Boolean.class == type) { + newValue = Boolean.valueOf(value); + } else if (Duration.class == type) { + newValue = Duration.parse(value); + } + return newValue; + } + + /** + * Returns the object which represents the supplied class given the supplied string representation. + * + * @param c type to instantiate + * @param s property value to parse + * @return the supplied type or null + */ + protected Object createTypeFromPropertyValue(final Class c, final String s) { + final Object newObject; + if ("null".equals(s)) { + newObject = null; + } else { + if (PropertyValueParser.isConfig(s)) { + final PropertyValueParser configParser = new PropertyValueParser(s); + newObject = configParser.initializeType(); + } else { + if (Class.class == c) { + newObject = createClass(s); + } else { + newObject = instantiateType(c, s); + } + } + } + return newObject; + } + + /** + * Returns the object which represents an array of the supplied class given the supplied string representation. + * + * @param c type to instantiate + * @param s property value to parse + * @return an array or null + */ + protected Object createArrayTypeFromPropertyValue(final Class c, final String s) { + final Object newObject; + if ("null".equals(s)) { + newObject = null; + } else { + if (s.contains("},")) { + final String[] classes = s.split("\\},"); + newObject = Array.newInstance(c, classes.length); + for (int i = 0; i < classes.length; i++) { + classes[i] = classes[i] + "}"; + if (PropertyValueParser.isConfig(classes[i])) { + final PropertyValueParser configParser = new PropertyValueParser(classes[i]); + Array.set(newObject, i, configParser.initializeType()); + } else { + throw new IllegalArgumentException(String.format("Could not parse property string: %s", classes[i])); + } + } + } else { + final String[] classes = s.split(","); + newObject = Array.newInstance(c, classes.length); + for (int i = 0; i < classes.length; i++) { + if (PropertyValueParser.isConfig(classes[i])) { + final PropertyValueParser configParser = new PropertyValueParser(classes[i]); + Array.set(newObject, i, configParser.initializeType()); + } else { + if (Class.class == c) { + Array.set(newObject, i, createClass(classes[i])); + } else { + Array.set(newObject, i, instantiateType(c, classes[i])); + } + } + } + } + } + return newObject; + } + + /** + * Returns the enum array which represents the supplied class given the supplied string representation. + * + * @param c type to instantiate + * @param s property value to parse + * @return Enum[] of the supplied type or null + */ + protected Object createArrayEnumFromPropertyValue(final Class c, final String s) { + final Object newObject; + if ("null".equals(s)) { + newObject = null; + } else { + final String[] values = s.split(","); + newObject = Array.newInstance(c, values.length); + for (int i = 0; i < values.length; i++) { + Array.set(newObject, i, getEnum(c, values[i])); + } + } + return newObject; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractPropertySource.java new file mode 100644 index 0000000..da54d74 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/AbstractPropertySource.java @@ -0,0 +1,140 @@ + +package org.xbib.net.ldap.props; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import org.xbib.net.ldap.io.ResourceUtils; + +/** + * Provides methods common to property source implementations. + * + * @param type of object to invoke properties on + */ +public abstract class AbstractPropertySource implements PropertySource { + + /** + * Default file to read properties from, value is {@value}. + */ + public static final String PROPERTIES_FILE = "classpath:/org/xbib/net/ldap//ldap.properties"; + + /** + * Object to initialize with properties. + */ + protected final T object; + + /** + * Domain that properties are in. + */ + protected final PropertyDomain propertiesDomain; + + /** + * Properties to set. + */ + protected final Properties properties; + + /** + * Properties that are not in our domain. + */ + protected final Map extraProps = new HashMap<>(); + + + /** + * Creates a new abstract property source. + * + * @param t to set properties on + * @param pd domain that properties reside in + * @param p properties to set + */ + public AbstractPropertySource(final T t, final PropertyDomain pd, final Properties p) { + object = t; + propertiesDomain = pd; + properties = p; + } + + + /** + * Creates properties from the supplied file paths. See {@link #loadProperties(Reader...)}. + * + * @param paths to read properties from + * @return initialized properties object. + */ + protected static Properties loadProperties(final String... paths) { + try { + final Reader[] readers = new Reader[paths.length]; + for (int i = 0; i < paths.length; i++) { + readers[i] = new InputStreamReader(ResourceUtils.getResource(paths[i])); + } + return loadProperties(readers); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + + /** + * Creates properties from the supplied reader. See {@link Properties#load(Reader)}. Readers supplied to this method + * will be closed. + * + * @param readers to read properties from + * @return initialized properties object. + */ + protected static Properties loadProperties(final Reader... readers) { + try { + final Properties properties = new Properties(); + for (Reader r : readers) { + try (r) { + properties.load(r); + } + } + return properties; + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + + /** + * Iterates over the properties and uses the invoker to set those properties on the object. Any properties that do not + * belong to the object are set in the extraProps map. + * + * @param invoker to set properties on the object + */ + protected void initializeObject(final PropertyInvoker invoker) { + final Map props = new HashMap<>(); + final Enumeration en = properties.keys(); + if (en != null) { + while (en.hasMoreElements()) { + final String name = (String) en.nextElement(); + final String value = (String) properties.get(name); + if (!name.startsWith(PropertyDomain.LDAP.value())) { + extraProps.put(name, value); + } else { + // strip out the method name + final int split = name.lastIndexOf('.') + 1; + final String propName = name.substring(split); + final String propDomain = name.substring(0, split); + // if we have this property, set it last + if (propertiesDomain.value().equals(propDomain)) { + if (invoker.hasProperty(propName)) { + props.put(propName, value); + } + // check if this is a super class property + // if it is, set it now, it may be overridden with the props map + } else if (propertiesDomain.value().startsWith(propDomain)) { + if (invoker.hasProperty(propName)) { + invoker.setProperty(object, propName, value); + } + } + } + } + for (Map.Entry entry : props.entrySet()) { + invoker.setProperty(object, entry.getKey(), entry.getValue()); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticationRequestPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticationRequestPropertyInvoker.java new file mode 100644 index 0000000..644a1ab --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticationRequestPropertyInvoker.java @@ -0,0 +1,45 @@ + +package org.xbib.net.ldap.props; + +import java.io.IOException; +import org.xbib.net.ldap.Credential; +import org.xbib.net.ldap.io.ResourceUtils; + +/** + * Handles properties for {@link org.xbib.net.ldap.auth.AuthenticationRequest}. + * + */ +public class AuthenticationRequestPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new authentication request property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public AuthenticationRequestPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (Credential.class.isAssignableFrom(type)) { + if (ResourceUtils.isResource(value)) { + try { + newValue = new Credential(ResourceUtils.readResource(value)); + } catch (IOException e) { + throw new IllegalArgumentException("Could not read resource: " + value, e); + } + } else { + newValue = new Credential(value); + } + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticationRequestPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticationRequestPropertySource.java new file mode 100644 index 0000000..27838c4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticationRequestPropertySource.java @@ -0,0 +1,93 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.auth.AuthenticationRequest; + +/** + * Reads properties specific to {@link org.xbib.net.ldap.auth.AuthenticationRequest} and returns an initialized object of + * that type. + * + */ +public final class AuthenticationRequestPropertySource extends AbstractPropertySource { + + /** + * Invoker for authentication request. + */ + private static final AuthenticationRequestPropertyInvoker INVOKER = new AuthenticationRequestPropertyInvoker( + AuthenticationRequest.class); + + + /** + * Creates a new authentication request property source using the default properties file. + * + * @param request authentication request to set properties on + */ + public AuthenticationRequestPropertySource(final AuthenticationRequest request) { + this(request, PROPERTIES_FILE); + } + + + /** + * Creates a new authentication request property source. + * + * @param request authentication request to set properties on + * @param paths to read properties from + */ + public AuthenticationRequestPropertySource(final AuthenticationRequest request, final String... paths) { + this(request, loadProperties(paths)); + } + + + /** + * Creates a new authentication request property source. + * + * @param request authentication request to set properties on + * @param readers to read properties from + */ + public AuthenticationRequestPropertySource(final AuthenticationRequest request, final Reader... readers) { + this(request, loadProperties(readers)); + } + + + /** + * Creates a new authentication request property source. + * + * @param request authentication request to set properties on + * @param props to read properties from + */ + public AuthenticationRequestPropertySource(final AuthenticationRequest request, final Properties props) { + this(request, PropertyDomain.AUTH, props); + } + + + /** + * Creates a new authentication request property source. + * + * @param request authentication request to set properties on + * @param domain that properties are in + * @param props to read properties from + */ + public AuthenticationRequestPropertySource( + final AuthenticationRequest request, + final PropertyDomain domain, + final Properties props) { + super(request, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticatorPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticatorPropertyInvoker.java new file mode 100644 index 0000000..d0f1b5e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticatorPropertyInvoker.java @@ -0,0 +1,44 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.auth.AuthenticationHandler; +import org.xbib.net.ldap.auth.AuthenticationResponseHandler; +import org.xbib.net.ldap.auth.DnResolver; +import org.xbib.net.ldap.auth.EntryResolver; + +/** + * Handles properties for {@link org.xbib.net.ldap.auth.Authenticator}. + * + */ +public class AuthenticatorPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new authenticator property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public AuthenticatorPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (DnResolver.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(DnResolver.class, value); + } else if (AuthenticationHandler.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(AuthenticationHandler.class, value); + } else if (AuthenticationResponseHandler[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(AuthenticationResponseHandler.class, value); + } else if (EntryResolver.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(EntryResolver.class, value); + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticatorPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticatorPropertySource.java new file mode 100644 index 0000000..6093f19 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/AuthenticatorPropertySource.java @@ -0,0 +1,206 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.ConnectionFactoryManager; +import org.xbib.net.ldap.DefaultConnectionFactory; +import org.xbib.net.ldap.auth.AuthenticationHandler; +import org.xbib.net.ldap.auth.Authenticator; +import org.xbib.net.ldap.auth.CompareAuthenticationHandler; +import org.xbib.net.ldap.auth.DnResolver; +import org.xbib.net.ldap.auth.EntryResolver; +import org.xbib.net.ldap.auth.SearchDnResolver; +import org.xbib.net.ldap.auth.SearchEntryResolver; +import org.xbib.net.ldap.auth.SimpleBindAuthenticationHandler; + +/** + * Reads properties specific to {@link org.xbib.net.ldap.auth.Authenticator} and returns an initialized object of that type. + * + */ +public final class AuthenticatorPropertySource extends AbstractPropertySource { + + /** + * Invoker for authenticator. + */ + private static final AuthenticatorPropertyInvoker INVOKER = new AuthenticatorPropertyInvoker(Authenticator.class); + + + /** + * Creates a new authenticator property source using the default properties file. + * + * @param a authenticator to set properties on + */ + public AuthenticatorPropertySource(final Authenticator a) { + this(a, PROPERTIES_FILE); + } + + + /** + * Creates a new authenticator property source. + * + * @param a authenticator to set properties on + * @param paths to read properties from + */ + public AuthenticatorPropertySource(final Authenticator a, final String... paths) { + this(a, loadProperties(paths)); + } + + + /** + * Creates a new authenticator property source. + * + * @param a authenticator to set properties on + * @param readers to read properties from + */ + public AuthenticatorPropertySource(final Authenticator a, final Reader... readers) { + this(a, loadProperties(readers)); + } + + + /** + * Creates a new authenticator property source. + * + * @param a authenticator to set properties on + * @param props to read properties from + */ + public AuthenticatorPropertySource(final Authenticator a, final Properties props) { + this(a, PropertyDomain.AUTH, props); + } + + + /** + * Creates a new authenticator property source. + * + * @param a authenticator to set properties on + * @param domain that properties are in + * @param props to read properties from + */ + public AuthenticatorPropertySource(final Authenticator a, final PropertyDomain domain, final Properties props) { + super(a, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + + // initialize a SearchDnResolver by default + DnResolver dnResolver = object.getDnResolver(); + if (dnResolver == null) { + dnResolver = new SearchDnResolver(); + final SearchDnResolverPropertySource dnPropSource = new SearchDnResolverPropertySource( + (SearchDnResolver) dnResolver, + propertiesDomain, + properties); + dnPropSource.initialize(); + object.setDnResolver(dnResolver); + } else { + if (dnResolver instanceof SearchDnResolver) { + final SearchDnResolverPropertySource dnPropSource = new SearchDnResolverPropertySource( + (SearchDnResolver) dnResolver, + propertiesDomain, + properties); + dnPropSource.initialize(); + } else { + final SimplePropertySource sPropSource = new SimplePropertySource<>( + dnResolver, + propertiesDomain, + properties); + sPropSource.initialize(); + if (dnResolver instanceof ConnectionFactoryManager resolverManager) { + if (resolverManager.getConnectionFactory() == null) { + initConnectionFactoryManager(resolverManager); + } + } + } + } + + // initialize the EntryResolver if supplied + final EntryResolver entryResolver = object.getEntryResolver(); + if (entryResolver != null) { + if (entryResolver instanceof SearchEntryResolver) { + final SearchEntryResolverPropertySource entryPropSource = new SearchEntryResolverPropertySource( + (SearchEntryResolver) entryResolver, + propertiesDomain, + properties); + entryPropSource.initialize(); + } else { + final SimplePropertySource sPropSource = new SimplePropertySource<>( + entryResolver, + propertiesDomain, + properties); + sPropSource.initialize(); + if (entryResolver instanceof ConnectionFactoryManager resolverManager) { + if (resolverManager.getConnectionFactory() == null) { + initConnectionFactoryManager(resolverManager); + } + } + } + } + + // initialize a BindAuthenticationHandler by default + AuthenticationHandler authHandler = object.getAuthenticationHandler(); + if (authHandler == null) { + authHandler = new SimpleBindAuthenticationHandler(); + final SimpleBindAuthenticationHandlerPropertySource ahPropSource = + new SimpleBindAuthenticationHandlerPropertySource( + (SimpleBindAuthenticationHandler) authHandler, + propertiesDomain, + properties); + ahPropSource.initialize(); + object.setAuthenticationHandler(authHandler); + } else { + if (authHandler instanceof SimpleBindAuthenticationHandler) { + final SimpleBindAuthenticationHandlerPropertySource ahPropSource = + new SimpleBindAuthenticationHandlerPropertySource( + (SimpleBindAuthenticationHandler) authHandler, + propertiesDomain, + properties); + ahPropSource.initialize(); + } else if (authHandler instanceof CompareAuthenticationHandler) { + final CompareAuthenticationHandlerPropertySource ahPropSource = + new CompareAuthenticationHandlerPropertySource( + (CompareAuthenticationHandler) authHandler, + propertiesDomain, + properties); + ahPropSource.initialize(); + } else { + final SimplePropertySource sPropSource = new SimplePropertySource<>( + authHandler, + propertiesDomain, + properties); + sPropSource.initialize(); + if (authHandler instanceof ConnectionFactoryManager handlerManager) { + if (handlerManager.getConnectionFactory() == null) { + initConnectionFactoryManager(handlerManager); + } + } + } + } + } + + /** + * Initializes the supplied connection factory manager using the properties in this property source. + * + * @param cfm to initialize + */ + private void initConnectionFactoryManager(final ConnectionFactoryManager cfm) { + final DefaultConnectionFactory cf = new DefaultConnectionFactory(); + final DefaultConnectionFactoryPropertySource cfPropSource = new DefaultConnectionFactoryPropertySource( + cf, + propertiesDomain, + properties); + cfPropSource.initialize(); + cfm.setConnectionFactory(cf); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/BindConnectionInitializerPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/BindConnectionInitializerPropertyInvoker.java new file mode 100644 index 0000000..89d07a4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/BindConnectionInitializerPropertyInvoker.java @@ -0,0 +1,63 @@ + +package org.xbib.net.ldap.props; + +import java.io.IOException; +import org.xbib.net.ldap.Credential; +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.io.ResourceUtils; +import org.xbib.net.ldap.sasl.SaslConfig; + +/** + * Handles properties for {@link org.xbib.net.ldap.BindConnectionInitializer}. + * + */ +public class BindConnectionInitializerPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new bind connection initializer property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public BindConnectionInitializerPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (SaslConfig.class.isAssignableFrom(type)) { + if ("null".equals(value)) { + newValue = null; + } else { + if (PropertyValueParser.isParamsOnlyConfig(value)) { + final PropertyValueParser configParser = new PropertyValueParser(value, "org.xbib.net.ldap.sasl.SaslConfig"); + newValue = configParser.initializeType(); + } else if (PropertyValueParser.isConfig(value)) { + final PropertyValueParser configParser = new PropertyValueParser(value); + newValue = configParser.initializeType(); + } else { + newValue = instantiateType(SaslConfig.class, value); + } + } + } else if (RequestControl[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(RequestControl.class, value); + } else if (Credential.class.isAssignableFrom(type)) { + if (ResourceUtils.isResource(value)) { + try { + newValue = new Credential(ResourceUtils.readResource(value)); + } catch (IOException e) { + throw new IllegalArgumentException("Could not read resource: " + value, e); + } + } else { + newValue = new Credential(value); + } + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/BindConnectionInitializerPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/BindConnectionInitializerPropertySource.java new file mode 100644 index 0000000..c35eb98 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/BindConnectionInitializerPropertySource.java @@ -0,0 +1,92 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.BindConnectionInitializer; + +/** + * Reads properties specific to {@link BindConnectionInitializer} and returns an initialized object of that type. + * + */ +public final class BindConnectionInitializerPropertySource extends AbstractPropertySource { + + /** + * Invoker for bind connection initializer. + */ + private static final BindConnectionInitializerPropertyInvoker INVOKER = new BindConnectionInitializerPropertyInvoker( + BindConnectionInitializer.class); + + + /** + * Creates a new bind connection initializer property source using the default properties file. + * + * @param initializer bind connection initializer to invoke properties on + */ + public BindConnectionInitializerPropertySource(final BindConnectionInitializer initializer) { + this(initializer, PROPERTIES_FILE); + } + + + /** + * Creates a new bind connection initializer property source. + * + * @param initializer bind connection initializer to invoke properties on + * @param paths to read properties from + */ + public BindConnectionInitializerPropertySource(final BindConnectionInitializer initializer, final String... paths) { + this(initializer, loadProperties(paths)); + } + + + /** + * Creates a new bind connection initializer property source. + * + * @param initializer bind connection initializer to invoke properties on + * @param readers to read properties from + */ + public BindConnectionInitializerPropertySource(final BindConnectionInitializer initializer, final Reader... readers) { + this(initializer, loadProperties(readers)); + } + + + /** + * Creates a new bind connection initializer property source. + * + * @param initializer bind connection initializer to invoke properties on + * @param props to read properties from + */ + public BindConnectionInitializerPropertySource(final BindConnectionInitializer initializer, final Properties props) { + this(initializer, PropertyDomain.LDAP, props); + } + + + /** + * Creates a new bind connection initializer property source. + * + * @param initializer bind connection initializer to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public BindConnectionInitializerPropertySource( + final BindConnectionInitializer initializer, + final PropertyDomain domain, + final Properties props) { + super(initializer, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/BlockingConnectionPoolPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/BlockingConnectionPoolPropertyInvoker.java new file mode 100644 index 0000000..fda4e8e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/BlockingConnectionPoolPropertyInvoker.java @@ -0,0 +1,44 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.ConnectionValidator; +import org.xbib.net.ldap.pool.ConnectionActivator; +import org.xbib.net.ldap.pool.ConnectionPassivator; +import org.xbib.net.ldap.pool.PruneStrategy; + +/** + * Handles properties for {@link org.xbib.net.ldap.pool.BlockingConnectionPool}. + * + */ +public class BlockingConnectionPoolPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new blocking connection pool property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public BlockingConnectionPoolPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (ConnectionActivator.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(ConnectionActivator.class, value); + } else if (ConnectionPassivator.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(ConnectionPassivator.class, value); + } else if (ConnectionValidator.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(ConnectionValidator.class, value); + } else if (PruneStrategy.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(PruneStrategy.class, value); + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/BlockingConnectionPoolPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/BlockingConnectionPoolPropertySource.java new file mode 100644 index 0000000..4ee6f43 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/BlockingConnectionPoolPropertySource.java @@ -0,0 +1,131 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.ConnectionValidator; +import org.xbib.net.ldap.DefaultConnectionFactory; +import org.xbib.net.ldap.SearchConnectionValidator; +import org.xbib.net.ldap.pool.BlockingConnectionPool; + +/** + * Reads properties specific to {@link BlockingConnectionPool} and returns an initialized object of that type. + * + */ +public final class BlockingConnectionPoolPropertySource extends AbstractPropertySource { + + /** + * Invoker for connection factory. + */ + private static final BlockingConnectionPoolPropertyInvoker INVOKER = new BlockingConnectionPoolPropertyInvoker( + BlockingConnectionPool.class); + + + /** + * Creates a new blocking connection pool property source using the default properties file. + * + * @param cp connection pool to invoke properties on + */ + public BlockingConnectionPoolPropertySource(final BlockingConnectionPool cp) { + this(cp, PROPERTIES_FILE); + } + + + /** + * Creates a new blocking connection pool property source. + * + * @param cp connection pool to invoke properties on + * @param paths to read properties from + */ + public BlockingConnectionPoolPropertySource(final BlockingConnectionPool cp, final String... paths) { + this(cp, loadProperties(paths)); + } + + + /** + * Creates a new blocking connection pool property source. + * + * @param cp connection pool to invoke properties on + * @param readers to read properties from + */ + public BlockingConnectionPoolPropertySource(final BlockingConnectionPool cp, final Reader... readers) { + this(cp, loadProperties(readers)); + } + + + /** + * Creates a new blocking connection pool property source. + * + * @param cp connection pool to invoke properties on + * @param props to read properties from + */ + public BlockingConnectionPoolPropertySource(final BlockingConnectionPool cp, final Properties props) { + this(cp, PropertyDomain.POOL, props); + } + + + /** + * Creates a new blocking connection pool property source. + * + * @param cp connection pool to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public BlockingConnectionPoolPropertySource( + final BlockingConnectionPool cp, + final PropertyDomain domain, + final Properties props) { + super(cp, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + + DefaultConnectionFactory cf = object.getDefaultConnectionFactory(); + if (cf == null) { + cf = new DefaultConnectionFactory(); + object.setDefaultConnectionFactory(cf); + } + final DefaultConnectionFactoryPropertySource cfPropSource = new DefaultConnectionFactoryPropertySource( + cf, + propertiesDomain, + properties); + cfPropSource.initialize(); + + ConnectionValidator cv = object.getValidator(); + if (cv == null) { + cv = new SearchConnectionValidator(); + final SearchConnectionValidatorPropertySource cvPropSource = new SearchConnectionValidatorPropertySource( + (SearchConnectionValidator) cv, + propertiesDomain, + properties); + cvPropSource.initialize(); + object.setValidator(cv); + } else { + if (cv instanceof SearchConnectionValidator) { + final SearchConnectionValidatorPropertySource cvPropSource = new SearchConnectionValidatorPropertySource( + (SearchConnectionValidator) cv, + propertiesDomain, + properties); + cvPropSource.initialize(); + } else { + final SimplePropertySource sPropSource = new SimplePropertySource<>( + cv, + propertiesDomain, + properties); + sPropSource.initialize(); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/CompareAuthenticationHandlerPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/CompareAuthenticationHandlerPropertyInvoker.java new file mode 100644 index 0000000..cd64e88 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/CompareAuthenticationHandlerPropertyInvoker.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Handles properties for {@link org.xbib.net.ldap.auth.CompareAuthenticationHandler}. + * + */ +public class CompareAuthenticationHandlerPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new compare authentication handler property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public CompareAuthenticationHandlerPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (ConnectionFactory.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(ConnectionFactory.class, value); + } else if (RequestControl[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(RequestControl.class, value); + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/CompareAuthenticationHandlerPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/CompareAuthenticationHandlerPropertySource.java new file mode 100644 index 0000000..ce9e901 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/CompareAuthenticationHandlerPropertySource.java @@ -0,0 +1,94 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.auth.CompareAuthenticationHandler; + +/** + * Reads properties specific to {@link CompareAuthenticationHandler} and returns an initialized object of that type. + * + */ +public final class CompareAuthenticationHandlerPropertySource + extends AbstractConnectionFactoryManagerPropertySource { + + /** + * Invoker for compare authentication handler. + */ + private static final CompareAuthenticationHandlerPropertyInvoker INVOKER = + new CompareAuthenticationHandlerPropertyInvoker(CompareAuthenticationHandler.class); + + + /** + * Creates a new compare authentication handler property source using the default properties file. + * + * @param handler compare authentication handler to invoke properties on + */ + public CompareAuthenticationHandlerPropertySource(final CompareAuthenticationHandler handler) { + this(handler, PROPERTIES_FILE); + } + + + /** + * Creates a new compare authentication handler property source. + * + * @param handler compare authentication handler to invoke properties on + * @param paths to read properties from + */ + public CompareAuthenticationHandlerPropertySource(final CompareAuthenticationHandler handler, final String... paths) { + this(handler, loadProperties(paths)); + } + + + /** + * Creates a new compare authentication handler property source. + * + * @param handler compare authentication handler to invoke properties on + * @param readers to read properties from + */ + public CompareAuthenticationHandlerPropertySource(final CompareAuthenticationHandler handler, final Reader... readers) { + this(handler, loadProperties(readers)); + } + + + /** + * Creates a new compare authentication handler property source. + * + * @param handler compare authentication handler to invoke properties on + * @param props to read properties from + */ + public CompareAuthenticationHandlerPropertySource(final CompareAuthenticationHandler handler, final Properties props) { + this(handler, PropertyDomain.AUTH, props); + } + + + /** + * Creates a new compare authentication handler property source. + * + * @param handler compare authentication handler to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public CompareAuthenticationHandlerPropertySource( + final CompareAuthenticationHandler handler, + final PropertyDomain domain, + final Properties props) { + super(handler, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + super.initialize(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/ConnectionConfigPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/ConnectionConfigPropertyInvoker.java new file mode 100644 index 0000000..7927f53 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/ConnectionConfigPropertyInvoker.java @@ -0,0 +1,52 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.ActivePassiveConnectionStrategy; +import org.xbib.net.ldap.ConnectionInitializer; +import org.xbib.net.ldap.ConnectionStrategy; +import org.xbib.net.ldap.DnsSrvConnectionStrategy; +import org.xbib.net.ldap.RandomConnectionStrategy; +import org.xbib.net.ldap.RoundRobinConnectionStrategy; + +/** + * Handles properties for {@link org.xbib.net.ldap.ConnectionConfig}. + * + */ +public class ConnectionConfigPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new connection config property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public ConnectionConfigPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (ConnectionInitializer[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(ConnectionInitializer.class, value); + } else if (ConnectionStrategy.class.isAssignableFrom(type)) { + if ("ACTIVE_PASSIVE".equals(value)) { + newValue = new ActivePassiveConnectionStrategy(); + } else if ("ROUND_ROBIN".equals(value)) { + newValue = new RoundRobinConnectionStrategy(); + } else if ("RANDOM".equals(value)) { + newValue = new RandomConnectionStrategy(); + } else if ("DNS_SRV".equals(value)) { + newValue = new DnsSrvConnectionStrategy(); + } else { + newValue = createTypeFromPropertyValue(ConnectionStrategy.class, value); + } + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/ConnectionConfigPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/ConnectionConfigPropertySource.java new file mode 100644 index 0000000..84c6fd3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/ConnectionConfigPropertySource.java @@ -0,0 +1,128 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.BindConnectionInitializer; +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.ConnectionInitializer; +import org.xbib.net.ldap.ssl.SslConfig; + +/** + * Reads properties specific to {@link ConnectionConfig} and returns an initialized object of that type. + * + */ +public final class ConnectionConfigPropertySource extends AbstractPropertySource { + + /** + * Invoker for connection config. + */ + private static final ConnectionConfigPropertyInvoker INVOKER = new ConnectionConfigPropertyInvoker( + ConnectionConfig.class); + + + /** + * Creates a new connection config property source using the default properties file. + * + * @param cc connection config to invoke properties on + */ + public ConnectionConfigPropertySource(final ConnectionConfig cc) { + this(cc, PROPERTIES_FILE); + } + + + /** + * Creates a new connection config property source. + * + * @param cc connection config to invoke properties on + * @param paths to read properties from + */ + public ConnectionConfigPropertySource(final ConnectionConfig cc, final String... paths) { + this(cc, loadProperties(paths)); + } + + + /** + * Creates a new connection config property source. + * + * @param cc connection config to invoke properties on + * @param readers to read properties from + */ + public ConnectionConfigPropertySource(final ConnectionConfig cc, final Reader... readers) { + this(cc, loadProperties(readers)); + } + + + /** + * Creates a new connection config property source. + * + * @param cc connection config to invoke properties on + * @param props to read properties from + */ + public ConnectionConfigPropertySource(final ConnectionConfig cc, final Properties props) { + this(cc, PropertyDomain.LDAP, props); + } + + + /** + * Creates a new connection config property source. + * + * @param cc connection config to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public ConnectionConfigPropertySource(final ConnectionConfig cc, final PropertyDomain domain, final Properties props) { + super(cc, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + + SslConfig sc = object.getSslConfig(); + if (sc == null) { + sc = new SslConfig(); + final SslConfigPropertySource scSource = new SslConfigPropertySource(sc, propertiesDomain, properties); + scSource.initialize(); + if (!sc.isEmpty()) { + object.setSslConfig(sc); + } + } else { + final SslConfigPropertySource scSource = new SslConfigPropertySource(sc, propertiesDomain, properties); + scSource.initialize(); + } + + + final ConnectionInitializer[] initializers = object.getConnectionInitializers(); + // configure a bind connection initializer if bind properties are found + if (initializers == null) { + final BindConnectionInitializer bci = new BindConnectionInitializer(); + final BindConnectionInitializerPropertySource bciSource = new BindConnectionInitializerPropertySource( + bci, + propertiesDomain, + properties); + bciSource.initialize(); + if (!bci.isEmpty()) { + object.setConnectionInitializers(bci); + } + } else { + for (ConnectionInitializer init : initializers) { + final SimplePropertySource sPropSource = new SimplePropertySource<>( + init, + propertiesDomain, + properties); + sPropSource.initialize(); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/CredentialConfigParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/CredentialConfigParser.java new file mode 100644 index 0000000..a4966bf --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/CredentialConfigParser.java @@ -0,0 +1,54 @@ + +package org.xbib.net.ldap.props; + +import java.util.regex.Matcher; + +/** + * Parses the configuration data associated with credential configs. The format of the property string should be like: + * + *

+ * KeyStoreCredentialConfig
+ * {{trustStore=file:/tmp/my.truststore}{trustStoreType=JKS}}
+ * 
+ * + *

or

+ * + *
+ * {{trustCertificates=file:/tmp/my.crt}}
+ * 
+ * + */ +public class CredentialConfigParser extends PropertyValueParser { + + /** + * Credential config class found in the config. + */ + protected static final String DEFAULT_CREDENTIAL_CONFIG_CLASS = "org.xbib.net.ldap.ssl.X509CredentialConfig"; + + + /** + * Creates a new credential config parser. + * + * @param config containing configuration data + */ + public CredentialConfigParser(final String config) { + final Matcher credentialOnlyMatcher = CONFIG_PATTERN.matcher(config); + final Matcher paramsOnlyMatcher = PARAMS_ONLY_CONFIG_PATTERN.matcher(config); + if (credentialOnlyMatcher.matches()) { + initialize(credentialOnlyMatcher.group(1).trim(), credentialOnlyMatcher.group(2).trim()); + } else if (paramsOnlyMatcher.matches()) { + initialize(DEFAULT_CREDENTIAL_CONFIG_CLASS, paramsOnlyMatcher.group(1).trim()); + } + } + + + /** + * Returns whether the supplied configuration data contains a credential config. + * + * @param config containing configuration data + * @return whether the supplied configuration data contains a credential config + */ + public static boolean isCredentialConfig(final String config) { + return isConfig(config) || isParamsOnlyConfig(config); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/DefaultConnectionFactoryPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/DefaultConnectionFactoryPropertyInvoker.java new file mode 100644 index 0000000..7a9ab1f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/DefaultConnectionFactoryPropertyInvoker.java @@ -0,0 +1,35 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.transport.Transport; + +/** + * Handles properties for {@link org.xbib.net.ldap.DefaultConnectionFactory}. + * + */ +public class DefaultConnectionFactoryPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new default connection factory property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public DefaultConnectionFactoryPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (Transport.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(Transport.class, value); + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/DefaultConnectionFactoryPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/DefaultConnectionFactoryPropertySource.java new file mode 100644 index 0000000..3500b9e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/DefaultConnectionFactoryPropertySource.java @@ -0,0 +1,102 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.DefaultConnectionFactory; + +/** + * Reads properties specific to {@link org.xbib.net.ldap.DefaultConnectionFactory} and returns an initialized object of that + * type. + * + */ +public final class DefaultConnectionFactoryPropertySource extends AbstractPropertySource { + + /** + * Invoker for connection factory. + */ + private static final DefaultConnectionFactoryPropertyInvoker INVOKER = new DefaultConnectionFactoryPropertyInvoker( + DefaultConnectionFactory.class); + + + /** + * Creates a new default connection factory property source using the default properties file. + * + * @param cf connection factory to invoke properties on + */ + public DefaultConnectionFactoryPropertySource(final DefaultConnectionFactory cf) { + this(cf, PROPERTIES_FILE); + } + + + /** + * Creates a new default connection factory property source. + * + * @param cf connection factory to invoke properties on + * @param paths to read properties from + */ + public DefaultConnectionFactoryPropertySource(final DefaultConnectionFactory cf, final String... paths) { + this(cf, loadProperties(paths)); + } + + + /** + * Creates a new default connection factory property source. + * + * @param cf connection factory to invoke properties on + * @param readers to read properties from + */ + public DefaultConnectionFactoryPropertySource(final DefaultConnectionFactory cf, final Reader... readers) { + this(cf, loadProperties(readers)); + } + + + /** + * Creates a new default connection factory property source. + * + * @param cf connection factory to invoke properties on + * @param props to read properties from + */ + public DefaultConnectionFactoryPropertySource(final DefaultConnectionFactory cf, final Properties props) { + this(cf, PropertyDomain.LDAP, props); + } + + + /** + * Creates a new default connection factory property source. + * + * @param cf connection factory to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public DefaultConnectionFactoryPropertySource( + final DefaultConnectionFactory cf, + final PropertyDomain domain, + final Properties props) { + super(cf, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + + final ConnectionConfig cc = new ConnectionConfig(); + final ConnectionConfigPropertySource ccPropSource = new ConnectionConfigPropertySource( + cc, + propertiesDomain, + properties); + ccPropSource.initialize(); + object.setConnectionConfig(cc); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/PooledConnectionFactoryPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/PooledConnectionFactoryPropertyInvoker.java new file mode 100644 index 0000000..299833d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/PooledConnectionFactoryPropertyInvoker.java @@ -0,0 +1,35 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.transport.Transport; + +/** + * Handles properties for {@link org.xbib.net.ldap.PooledConnectionFactory}. + * + */ +public class PooledConnectionFactoryPropertyInvoker extends BlockingConnectionPoolPropertyInvoker { + + + /** + * Creates a new pooled connection factory property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public PooledConnectionFactoryPropertyInvoker(final Class c) { + super(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (Transport.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(Transport.class, value); + } else { + newValue = super.convertValue(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/PooledConnectionFactoryPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/PooledConnectionFactoryPropertySource.java new file mode 100644 index 0000000..4c02245 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/PooledConnectionFactoryPropertySource.java @@ -0,0 +1,112 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Collections; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.PooledConnectionFactory; + +/** + * Reads properties specific to {@link PooledConnectionFactory} and returns an initialized object of that type. + * + */ +public final class PooledConnectionFactoryPropertySource extends AbstractPropertySource { + + /** + * Invoker for connection factory. + */ + private static final PooledConnectionFactoryPropertyInvoker INVOKER = new PooledConnectionFactoryPropertyInvoker( + PooledConnectionFactory.class); + + + /** + * Creates a new pooled connection factory property source using the default properties file. + * + * @param cf connection factory to invoke properties on + */ + public PooledConnectionFactoryPropertySource(final PooledConnectionFactory cf) { + this(cf, PROPERTIES_FILE); + } + + + /** + * Creates a new pooled connection factory property source. + * + * @param cf connection factory to invoke properties on + * @param paths to read properties from + */ + public PooledConnectionFactoryPropertySource(final PooledConnectionFactory cf, final String... paths) { + this(cf, loadProperties(paths)); + } + + + /** + * Creates a new pooled connection factory property source. + * + * @param cf connection factory to invoke properties on + * @param readers to read properties from + */ + public PooledConnectionFactoryPropertySource(final PooledConnectionFactory cf, final Reader... readers) { + this(cf, loadProperties(readers)); + } + + + /** + * Creates a new pooled connection factory property source. + * + * @param cf connection factory to invoke properties on + * @param props to read properties from + */ + public PooledConnectionFactoryPropertySource(final PooledConnectionFactory cf, final Properties props) { + this(cf, PropertyDomain.POOL, props); + } + + + /** + * Creates a new pooled connection factory property source. + * + * @param cf connection factory to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public PooledConnectionFactoryPropertySource( + final PooledConnectionFactory cf, + final PropertyDomain domain, + final Properties props) { + super(cf, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return Collections.emptySet(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + + ConnectionConfig cc = object.getConnectionConfig(); + if (cc == null) { + cc = new ConnectionConfig(); + final ConnectionConfigPropertySource ccPropSource = new ConnectionConfigPropertySource( + cc, + propertiesDomain, + properties); + ccPropSource.initialize(); + object.setConnectionConfig(cc); + } + + final BlockingConnectionPoolPropertySource cpPropSource = new BlockingConnectionPoolPropertySource( + object, + propertiesDomain, + properties); + cpPropSource.initialize(); + object.initialize(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertyInvoker.java new file mode 100644 index 0000000..acfa483 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertyInvoker.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.props; + +import java.util.Set; + +/** + * Interface for property driven object method invocation. + * + */ +public interface PropertyInvoker { + + + /** + * Invokes the setter method on the supplied object for the supplied property name and value. + * + * @param object to invoke property setter on + * @param name of the property to invoke + * @param value of the property to set + */ + void setProperty(Object object, String name, String value); + + + /** + * Returns whether a property with the supplied name exists on this invoker. + * + * @param name of the property to check + * @return whether a property with the supplied name exists on this invoker + */ + boolean hasProperty(String name); + + + /** + * Returns the property names for this invoker. + * + * @return set of property names + */ + Set getProperties(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertySource.java new file mode 100644 index 0000000..fd59c32 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertySource.java @@ -0,0 +1,62 @@ + +package org.xbib.net.ldap.props; + +/** + * Interface for property driven object initialization. + * + * @param type of object to invoke properties on + */ +public interface PropertySource { + + /** + * Initializes the object for this property source. + */ + void initialize(); + + + /** + * Enum to define the domain for properties. + */ + enum PropertyDomain { + + /** + * ldap property domain. + */ + LDAP("org.xbib.net.ldap."), + + /** + * auth property domain. + */ + AUTH("org.xbib.net.ldap.auth."), + + /** + * pool property domain. + */ + POOL("org.xbib.net.ldap.pool."); + + /** + * properties domain. + */ + private final String domain; + + + /** + * Creates a new property domain. + * + * @param s properties domain + */ + PropertyDomain(final String s) { + domain = s; + } + + + /** + * Returns the properties domain value. + * + * @return properties domain + */ + public String value() { + return domain; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertyValueParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertyValueParser.java new file mode 100644 index 0000000..5077372 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/PropertyValueParser.java @@ -0,0 +1,189 @@ + +package org.xbib.net.ldap.props; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses the configuration data associated with classes that contain setter properties. The format of the property + * string should be like: + * + *
+ * MyClass{{propertyOne=foo}{propertyTwo=bar}}
+ * 
+ * + *

If the class name is supplied to the constructor, the property string need not contain the class declaration.

+ * + */ +public class PropertyValueParser { + + /** + * Property string containing configuration. + */ + protected static final Pattern CONFIG_PATTERN = Pattern.compile("([^\\{]+)\\s*\\{(.*)\\}\\s*"); + + /** + * Property string for configuring a config where the class is known. + */ + protected static final Pattern PARAMS_ONLY_CONFIG_PATTERN = Pattern.compile("\\s*\\{\\s*(.*)\\s*\\}\\s*"); + + /** + * Pattern for finding properties. + */ + protected static final Pattern PROPERTY_PATTERN = Pattern.compile("([^\\}\\{])+"); + /** + * Properties found in the config to set on the class. + */ + private final Map properties = new HashMap<>(); + /** + * Class found in the config. + */ + private String className; + + + /** + * Default constructor. + */ + protected PropertyValueParser() { + } + + + /** + * Creates a new config parser. + * + * @param config containing configuration data + */ + public PropertyValueParser(final String config) { + final Matcher matcher = CONFIG_PATTERN.matcher(config); + if (matcher.matches()) { + initialize(matcher.group(1).trim(), matcher.group(2).trim()); + } + } + + + /** + * Creates a new config parser. + * + * @param config containing configuration data + * @param clazz fully qualified class name + */ + public PropertyValueParser(final String config, final String clazz) { + final Matcher matcher = PARAMS_ONLY_CONFIG_PATTERN.matcher(config); + if (matcher.matches()) { + initialize(clazz, matcher.group(1).trim()); + } + } + + /** + * Returns whether the supplied configuration data contains a config. + * + * @param config containing configuration data + * @return whether the supplied configuration data contains a config + */ + public static boolean isConfig(final String config) { + return CONFIG_PATTERN.matcher(config).matches(); + } + + /** + * Returns whether the supplied configuration data contains a params only config. + * + * @param config containing configuration data + * @return whether the supplied configuration data contains a params only config + */ + public static boolean isParamsOnlyConfig(final String config) { + return PARAMS_ONLY_CONFIG_PATTERN.matcher(config).matches(); + } + + /** + * Invokes {@link #setClassName(String)} and {@link #initializeProperties(Matcher)}. + * + * @param clazz type to create and initialize + * @param props to set on the class + */ + protected void initialize(final String clazz, final String props) { + setClassName(clazz); + if (!"".equals(props)) { + initializeProperties(PROPERTY_PATTERN.matcher(props)); + } + } + + /** + * Finds all the matches in the supplied matcher puts them into the properties map. Properties are split on '='. + * + * @param matcher to find matches + */ + protected void initializeProperties(final Matcher matcher) { + while (matcher.find()) { + final String input = matcher.group().trim(); + if (!"".equals(input)) { + final String[] s = input.split("=", 2); + if (s.length < 2) { + throw new IllegalArgumentException("Invalid property syntax: " + input); + } + properties.put(s[0].trim(), s[1].trim()); + } + } + } + + /** + * Returns the class name of the object to initialize. + * + * @return class name + */ + public String getClassName() { + return className; + } + + /** + * Sets the class name of the object to initialize. + * + * @param name of the object class type + */ + protected void setClassName(final String name) { + className = name; + } + + /** + * Returns the properties from the configuration. + * + * @return map of property name to value + */ + public Map getProperties() { + return properties; + } + + /** + * Initialize an instance of the class type with the properties contained in this config. + * + * @return object of the type the config parsed + */ + public Object initializeType() { + final Class c = SimplePropertyInvoker.createClass(getClassName()); + final Object o = SimplePropertyInvoker.instantiateType(c, getClassName()); + setProperties(c, o); + return o; + } + + + /** + * Sets the properties on the supplied object. + * + * @param c type of the supplied object + * @param o to invoke properties on + */ + protected void setProperties(final Class c, final Object o) { + final SimplePropertyInvoker invoker = new SimplePropertyInvoker(c); + for (Map.Entry entry : getProperties().entrySet()) { + invoker.setProperty(o, entry.getKey(), entry.getValue()); + } + if (invoker.getProperties().contains("initialize")) { + try { + invoker.setProperty(o, "initialize", null); + } catch (Throwable t) { + // + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchConnectionValidatorPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchConnectionValidatorPropertySource.java new file mode 100644 index 0000000..df1ec55 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchConnectionValidatorPropertySource.java @@ -0,0 +1,91 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.SearchConnectionValidator; + +/** + * Reads properties specific to {@link SearchConnectionValidator} and returns an initialized object of that type. + * + */ +public final class SearchConnectionValidatorPropertySource extends AbstractPropertySource { + + /** + * Invoker for search connection validator. + */ + private static final SimplePropertyInvoker INVOKER = new SimplePropertyInvoker(SearchConnectionValidator.class); + + + /** + * Creates a new search connection validator property source using the default properties file. + * + * @param cv connection validator to invoke properties on + */ + public SearchConnectionValidatorPropertySource(final SearchConnectionValidator cv) { + this(cv, PROPERTIES_FILE); + } + + + /** + * Creates a new search connection validator property source. + * + * @param cv connection validator to invoke properties on + * @param paths to read properties from + */ + public SearchConnectionValidatorPropertySource(final SearchConnectionValidator cv, final String... paths) { + this(cv, loadProperties(paths)); + } + + + /** + * Creates a new search connection validator property source. + * + * @param cv connection validator to invoke properties on + * @param readers to read properties from + */ + public SearchConnectionValidatorPropertySource(final SearchConnectionValidator cv, final Reader... readers) { + this(cv, loadProperties(readers)); + } + + + /** + * Creates a new search connection validator property source. + * + * @param cv connection validator to invoke properties on + * @param props to read properties from + */ + public SearchConnectionValidatorPropertySource(final SearchConnectionValidator cv, final Properties props) { + this(cv, PropertyDomain.POOL, props); + } + + + /** + * Creates a new search connection validator property source. + * + * @param cv connection validator to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public SearchConnectionValidatorPropertySource( + final SearchConnectionValidator cv, + final PropertyDomain domain, + final Properties props) { + super(cv, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchDnResolverPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchDnResolverPropertySource.java new file mode 100644 index 0000000..f56fc09 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchDnResolverPropertySource.java @@ -0,0 +1,94 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.auth.SearchDnResolver; + +/** + * Reads properties specific to {@link SearchDnResolver} and returns an initialized object of that type. + * + */ +public final class SearchDnResolverPropertySource + extends AbstractConnectionFactoryManagerPropertySource { + + /** + * Invoker for search dn resolver. + */ + private static final SearchOperationFactoryPropertyInvoker INVOKER = + new SearchOperationFactoryPropertyInvoker(SearchDnResolver.class); + + + /** + * Creates a new search dn resolver property source using the default properties file. + * + * @param resolver search dn resolver to invoke properties on + */ + public SearchDnResolverPropertySource(final SearchDnResolver resolver) { + this(resolver, PROPERTIES_FILE); + } + + + /** + * Creates a new search dn resolver property source. + * + * @param resolver search dn resolver to invoke properties on + * @param paths to read properties from + */ + public SearchDnResolverPropertySource(final SearchDnResolver resolver, final String... paths) { + this(resolver, loadProperties(paths)); + } + + + /** + * Creates a new search dn resolver property source. + * + * @param resolver search dn resolver to invoke properties on + * @param readers to read properties from + */ + public SearchDnResolverPropertySource(final SearchDnResolver resolver, final Reader... readers) { + this(resolver, loadProperties(readers)); + } + + + /** + * Creates a new search dn resolver property source. + * + * @param resolver search dn resolver to invoke properties on + * @param props to read properties from + */ + public SearchDnResolverPropertySource(final SearchDnResolver resolver, final Properties props) { + this(resolver, PropertyDomain.AUTH, props); + } + + + /** + * Creates a new search dn resolver property source. + * + * @param resolver search dn resolver to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public SearchDnResolverPropertySource( + final SearchDnResolver resolver, + final PropertyDomain domain, + final Properties props) { + super(resolver, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + super.initialize(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchEntryResolverPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchEntryResolverPropertySource.java new file mode 100644 index 0000000..ecf7713 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchEntryResolverPropertySource.java @@ -0,0 +1,94 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.auth.SearchEntryResolver; + +/** + * Reads properties specific to {@link SearchEntryResolver} and returns an initialized object of that type. + * + */ +public final class SearchEntryResolverPropertySource + extends AbstractConnectionFactoryManagerPropertySource { + + /** + * Invoker for search entry resolver. + */ + private static final SearchOperationFactoryPropertyInvoker INVOKER = + new SearchOperationFactoryPropertyInvoker(SearchEntryResolver.class); + + + /** + * Creates a new search entry resolver property source using the default properties file. + * + * @param resolver search entry resolver to invoke properties on + */ + public SearchEntryResolverPropertySource(final SearchEntryResolver resolver) { + this(resolver, PROPERTIES_FILE); + } + + + /** + * Creates a new search entry resolver property source. + * + * @param resolver search entry resolver to invoke properties on + * @param paths to read properties from + */ + public SearchEntryResolverPropertySource(final SearchEntryResolver resolver, final String... paths) { + this(resolver, loadProperties(paths)); + } + + + /** + * Creates a new search entry resolver property source. + * + * @param resolver search entry resolver to invoke properties on + * @param readers to read properties from + */ + public SearchEntryResolverPropertySource(final SearchEntryResolver resolver, final Reader... readers) { + this(resolver, loadProperties(readers)); + } + + + /** + * Creates a new search entry resolver property source. + * + * @param resolver search entry resolver to invoke properties on + * @param props to read properties from + */ + public SearchEntryResolverPropertySource(final SearchEntryResolver resolver, final Properties props) { + this(resolver, PropertyDomain.AUTH, props); + } + + + /** + * Creates a new search entry resolver property source. + * + * @param resolver search entry resolver to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public SearchEntryResolverPropertySource( + final SearchEntryResolver resolver, + final PropertyDomain domain, + final Properties props) { + super(resolver, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + super.initialize(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchOperationFactoryPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchOperationFactoryPropertyInvoker.java new file mode 100644 index 0000000..5a0f8a7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchOperationFactoryPropertyInvoker.java @@ -0,0 +1,47 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.LdapEntryHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.SearchResultHandler; + +/** + * Handles properties for implementations of {@link org.xbib.net.ldap.ConnectionFactoryManager}. + * + */ +public class SearchOperationFactoryPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new search dn resolver property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public SearchOperationFactoryPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (ConnectionFactory.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(ConnectionFactory.class, value); + } else if (ExceptionHandler.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(ExceptionHandler.class, value); + } else if (LdapEntryHandler[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(LdapEntryHandler.class, value); + } else if (ResultHandler[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(ResultHandler.class, value); + } else if (SearchResultHandler[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(SearchResultHandler.class, value); + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRequestPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRequestPropertyInvoker.java new file mode 100644 index 0000000..e38aa31 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRequestPropertyInvoker.java @@ -0,0 +1,44 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.filter.Filter; +import org.xbib.net.ldap.filter.FilterParseException; +import org.xbib.net.ldap.filter.FilterParser; + +/** + * Handles properties for {@link org.xbib.net.ldap.SearchRequest}. + * + */ +public class SearchRequestPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new search request property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public SearchRequestPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (Filter.class.isAssignableFrom(type)) { + try { + newValue = FilterParser.parse(value); + } catch (FilterParseException e) { + throw new IllegalArgumentException("Could not parse filter string '" + value + "'", e); + } + } else if (RequestControl[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(RequestControl.class, value); + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRequestPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRequestPropertySource.java new file mode 100644 index 0000000..39c0b6d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRequestPropertySource.java @@ -0,0 +1,88 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.SearchRequest; + +/** + * Reads properties specific to {@link SearchRequest} and returns an initialized object of that type. + * + */ +public final class SearchRequestPropertySource extends AbstractPropertySource { + + /** + * Invoker for search request. + */ + private static final SearchRequestPropertyInvoker INVOKER = new SearchRequestPropertyInvoker(SearchRequest.class); + + + /** + * Creates a new search request property source using the default properties file. + * + * @param request search request to invoke properties on + */ + public SearchRequestPropertySource(final SearchRequest request) { + this(request, PROPERTIES_FILE); + } + + + /** + * Creates a new search request property source. + * + * @param request search request to invoke properties on + * @param paths to read properties from + */ + public SearchRequestPropertySource(final SearchRequest request, final String... paths) { + this(request, loadProperties(paths)); + } + + + /** + * Creates a new search request property source. + * + * @param request search request to invoke properties on + * @param readers to read properties from + */ + public SearchRequestPropertySource(final SearchRequest request, final Reader... readers) { + this(request, loadProperties(readers)); + } + + + /** + * Creates a new search request property source. + * + * @param request search request to invoke properties on + * @param props to read properties from + */ + public SearchRequestPropertySource(final SearchRequest request, final Properties props) { + this(request, PropertyDomain.LDAP, props); + } + + + /** + * Creates a new search request property source. + * + * @param request search request to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public SearchRequestPropertySource(final SearchRequest request, final PropertyDomain domain, final Properties props) { + super(request, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRoleResolverPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRoleResolverPropertySource.java new file mode 100644 index 0000000..f690029 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SearchRoleResolverPropertySource.java @@ -0,0 +1,94 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.jaas.SearchRoleResolver; + +/** + * Reads properties specific to {@link SearchRoleResolver} and returns an initialized object of that type. + * + */ +public final class SearchRoleResolverPropertySource + extends AbstractConnectionFactoryManagerPropertySource { + + /** + * Invoker for search role resolver. + */ + private static final SearchOperationFactoryPropertyInvoker INVOKER = + new SearchOperationFactoryPropertyInvoker(SearchRoleResolver.class); + + + /** + * Creates a new search role resolver property source using the default properties file. + * + * @param resolver search role resolver to invoke properties on + */ + public SearchRoleResolverPropertySource(final SearchRoleResolver resolver) { + this(resolver, PROPERTIES_FILE); + } + + + /** + * Creates a new search role resolver property source. + * + * @param resolver search role resolver to invoke properties on + * @param paths to read properties from + */ + public SearchRoleResolverPropertySource(final SearchRoleResolver resolver, final String... paths) { + this(resolver, loadProperties(paths)); + } + + + /** + * Creates a new search role resolver property source. + * + * @param resolver search role resolver to invoke properties on + * @param readers to read properties from + */ + public SearchRoleResolverPropertySource(final SearchRoleResolver resolver, final Reader... readers) { + this(resolver, loadProperties(readers)); + } + + + /** + * Creates a new search role resolver property source. + * + * @param resolver search role resolver to invoke properties on + * @param props to read properties from + */ + public SearchRoleResolverPropertySource(final SearchRoleResolver resolver, final Properties props) { + this(resolver, PropertyDomain.AUTH, props); + } + + + /** + * Creates a new search role resolver property source. + * + * @param resolver search role resolver to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public SearchRoleResolverPropertySource( + final SearchRoleResolver resolver, + final PropertyDomain domain, + final Properties props) { + super(resolver, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + super.initialize(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SimpleBindAuthenticationHandlerPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SimpleBindAuthenticationHandlerPropertyInvoker.java new file mode 100644 index 0000000..4757a73 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SimpleBindAuthenticationHandlerPropertyInvoker.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.props; + +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.control.RequestControl; + +/** + * Handles properties for {@link org.xbib.net.ldap.auth.SimpleBindAuthenticationHandler}. + * + */ +public class SimpleBindAuthenticationHandlerPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new simple bind authentication handler property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public SimpleBindAuthenticationHandlerPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (ConnectionFactory.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(ConnectionFactory.class, value); + } else if (RequestControl[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(RequestControl.class, value); + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SimpleBindAuthenticationHandlerPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SimpleBindAuthenticationHandlerPropertySource.java new file mode 100644 index 0000000..ce3a5f2 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SimpleBindAuthenticationHandlerPropertySource.java @@ -0,0 +1,100 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.auth.SimpleBindAuthenticationHandler; + +/** + * Reads properties specific to {@link SimpleBindAuthenticationHandler} and returns an initialized object of that type. + * + */ +public final class SimpleBindAuthenticationHandlerPropertySource + extends AbstractConnectionFactoryManagerPropertySource { + + /** + * Invoker for simple bind authentication handler. + */ + private static final SimpleBindAuthenticationHandlerPropertyInvoker INVOKER = + new SimpleBindAuthenticationHandlerPropertyInvoker(SimpleBindAuthenticationHandler.class); + + + /** + * Creates a new simple bind authentication handler property source using the default properties file. + * + * @param handler simple bind authentication handler to invoke properties on + */ + public SimpleBindAuthenticationHandlerPropertySource(final SimpleBindAuthenticationHandler handler) { + this(handler, PROPERTIES_FILE); + } + + + /** + * Creates a new simple bind authentication handler property source. + * + * @param handler simple bind authentication handler to invoke properties on + * @param paths to read properties from + */ + public SimpleBindAuthenticationHandlerPropertySource( + final SimpleBindAuthenticationHandler handler, + final String... paths) { + this(handler, loadProperties(paths)); + } + + + /** + * Creates a new simple bind authentication handler property source. + * + * @param handler simple bind authentication handler to invoke properties on + * @param readers to read properties from + */ + public SimpleBindAuthenticationHandlerPropertySource( + final SimpleBindAuthenticationHandler handler, + final Reader... readers) { + this(handler, loadProperties(readers)); + } + + + /** + * Creates a new simple bind authentication handler property source. + * + * @param handler simple bind authentication handler to invoke properties on + * @param props to read properties from + */ + public SimpleBindAuthenticationHandlerPropertySource( + final SimpleBindAuthenticationHandler handler, + final Properties props) { + this(handler, PropertyDomain.AUTH, props); + } + + + /** + * Creates a new simple bind authentication handler property source. + * + * @param handler simple bind authentication handler to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public SimpleBindAuthenticationHandlerPropertySource( + final SimpleBindAuthenticationHandler handler, + final PropertyDomain domain, + final Properties props) { + super(handler, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + super.initialize(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SimplePropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SimplePropertyInvoker.java new file mode 100644 index 0000000..77bcba8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SimplePropertyInvoker.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.props; + +/** + * Handles simple properties common to all objects. + * + */ +public class SimplePropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new simple property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public SimplePropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + return convertSimpleType(type, value); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SimplePropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SimplePropertySource.java new file mode 100644 index 0000000..273f18e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SimplePropertySource.java @@ -0,0 +1,80 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; + +/** + * Reads simple properties and returns an initialized object of the supplied type. + * + * @param type of object to invoke properties on + */ +public final class SimplePropertySource extends AbstractPropertySource { + + /** + * Invoker for simple properties. + */ + private final SimplePropertyInvoker invoker; + + + /** + * Creates a new simple property source using the default properties file. + * + * @param t object to invoke properties on + */ + public SimplePropertySource(final T t) { + this(t, PROPERTIES_FILE); + } + + + /** + * Creates a new simple property source. + * + * @param t object to invoke properties on + * @param paths to read properties from + */ + public SimplePropertySource(final T t, final String... paths) { + this(t, loadProperties(paths)); + } + + + /** + * Creates a new simple property source. + * + * @param t object to invoke properties on + * @param readers to read properties from + */ + public SimplePropertySource(final T t, final Reader... readers) { + this(t, loadProperties(readers)); + } + + + /** + * Creates a new simple property source. + * + * @param t object to invoke properties on + * @param props to read properties from + */ + public SimplePropertySource(final T t, final Properties props) { + this(t, PropertyDomain.LDAP, props); + } + + + /** + * Creates a new simple property source. + * + * @param t object to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public SimplePropertySource(final T t, final PropertyDomain domain, final Properties props) { + super(t, domain, props); + invoker = new SimplePropertyInvoker(t.getClass()); + } + + + @Override + public void initialize() { + initializeObject(invoker); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SslConfigPropertyInvoker.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SslConfigPropertyInvoker.java new file mode 100644 index 0000000..0b991fb --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SslConfigPropertyInvoker.java @@ -0,0 +1,57 @@ + +package org.xbib.net.ldap.props; + +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.TrustManager; +import org.xbib.net.ldap.ssl.CertificateHostnameVerifier; +import org.xbib.net.ldap.ssl.CredentialConfig; +import org.xbib.net.ldap.ssl.SslConfig; + +/** + * Handles properties for {@link SslConfig}. + * + */ +public class SslConfigPropertyInvoker extends AbstractPropertyInvoker { + + + /** + * Creates a new ssl config property invoker for the supplied class. + * + * @param c class that has setter methods + */ + public SslConfigPropertyInvoker(final Class c) { + initialize(c); + } + + + @Override + protected Object convertValue(final Class type, final String value) { + Object newValue = value; + if (type != String.class) { + if (CredentialConfig.class.isAssignableFrom(type)) { + if ("null".equals(value)) { + newValue = null; + } else { + if (CredentialConfigParser.isCredentialConfig(value)) { + final CredentialConfigParser configParser = new CredentialConfigParser(value); + newValue = configParser.initializeType(); + } else if (PropertyValueParser.isConfig(value)) { + final PropertyValueParser configParser = new PropertyValueParser(value); + newValue = configParser.initializeType(); + } else { + newValue = instantiateType(SslConfig.class, value); + } + } + } else if (TrustManager[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(TrustManager.class, value); + } else if (CertificateHostnameVerifier.class.isAssignableFrom(type)) { + newValue = createTypeFromPropertyValue(CertificateHostnameVerifier.class, value); + } else if (HandshakeCompletedListener[].class.isAssignableFrom(type)) { + newValue = createArrayTypeFromPropertyValue(HandshakeCompletedListener.class, value); + } else { + newValue = convertSimpleType(type, value); + } + } + return newValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/props/SslConfigPropertySource.java b/net-ldap/src/main/java/org/xbib/net/ldap/props/SslConfigPropertySource.java new file mode 100644 index 0000000..adb83ac --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/props/SslConfigPropertySource.java @@ -0,0 +1,88 @@ + +package org.xbib.net.ldap.props; + +import java.io.Reader; +import java.util.Properties; +import java.util.Set; +import org.xbib.net.ldap.ssl.SslConfig; + +/** + * Reads properties specific to {@link SslConfig} and returns an initialized object of that type. + * + */ +public final class SslConfigPropertySource extends AbstractPropertySource { + + /** + * Invoker for ssl config. + */ + private static final SslConfigPropertyInvoker INVOKER = new SslConfigPropertyInvoker(SslConfig.class); + + + /** + * Creates a new ssl config property source using the default properties file. + * + * @param config ssl config to invoke properties on + */ + public SslConfigPropertySource(final SslConfig config) { + this(config, PROPERTIES_FILE); + } + + + /** + * Creates a new ssl config property source. + * + * @param config ssl config to invoke properties on + * @param paths to read properties from + */ + public SslConfigPropertySource(final SslConfig config, final String... paths) { + this(config, loadProperties(paths)); + } + + + /** + * Creates a new ssl config property source. + * + * @param config ssl config to invoke properties on + * @param readers to read properties from + */ + public SslConfigPropertySource(final SslConfig config, final Reader... readers) { + this(config, loadProperties(readers)); + } + + + /** + * Creates a new ssl config property source. + * + * @param config ssl config to invoke properties on + * @param props to read properties from + */ + public SslConfigPropertySource(final SslConfig config, final Properties props) { + this(config, PropertyDomain.LDAP, props); + } + + + /** + * Creates a new ssl config property source. + * + * @param config ssl config to invoke properties on + * @param domain that properties are in + * @param props to read properties from + */ + public SslConfigPropertySource(final SslConfig config, final PropertyDomain domain, final Properties props) { + super(config, domain, props); + } + + /** + * Returns the property names for this property source. + * + * @return all property names + */ + public static Set getProperties() { + return INVOKER.getProperties(); + } + + @Override + public void initialize() { + initializeObject(INVOKER); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/referral/AbstractFollowReferralHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/referral/AbstractFollowReferralHandler.java new file mode 100644 index 0000000..8d3f23b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/referral/AbstractFollowReferralHandler.java @@ -0,0 +1,157 @@ + +package org.xbib.net.ldap.referral; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapURL; +import org.xbib.net.ldap.Operation; +import org.xbib.net.ldap.Request; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.transport.MessageFunctional; + +/** + * Common implementation of referral handling. + * + * @param type of request + * @param type of result + */ +public abstract class AbstractFollowReferralHandler + extends MessageFunctional.Function { + + /** + * Default referral limit. Value is {@value}. + */ + protected static final int DEFAULT_REFERRAL_LIMIT = 10; + + /** + * Referral limit. + */ + protected final int referralLimit; + + /** + * Referral depth. + */ + protected final int referralDepth; + + /** + * Referral connection factory. + */ + private final ReferralConnectionFactory connectionFactory; + + + /** + * Creates a new abstract referral handler. + * + * @param limit number of referrals to follow + * @param depth number of referrals followed + * @param factory referral connection factory + */ + public AbstractFollowReferralHandler(final int limit, final int depth, final ReferralConnectionFactory factory) { + referralLimit = limit; + referralDepth = depth; + connectionFactory = factory; + } + + + /** + * Returns the maximum number of referrals to follow. + * + * @return referral limit + */ + public int getReferralLimit() { + return referralLimit; + } + + + /** + * Returns the referral depth of this handler. + * + * @return referral depth + */ + public int getReferralDepth() { + return referralDepth; + } + + + /** + * Returns the referral connection factory. + * + * @return referral connection factory + */ + public ReferralConnectionFactory getReferralConnectionFactory() { + return connectionFactory; + } + + + /** + * Creates a new request for this type of referral. + * + * @param url of the referral + * @return new request + */ + protected abstract Q createReferralRequest(LdapURL url); + + + /** + * Creates an operation for this type of referral. + * + * @param factory to get a connection with + * @return new operation + */ + protected abstract Operation createReferralOperation(ConnectionFactory factory); + + + /** + * Follows the supplied referral URLs in order until a SUCCESS or REFERRAL_LIMIT_EXCEEDED occurs. If neither of those + * conditions occurs this method returns null. + * + * @param referralUrls produced by the request + * @return referral response + */ + protected S followReferral(final String[] referralUrls) { + S referralResult = null; + final List urls = Arrays.asList(referralUrls); + Collections.shuffle(urls); + for (String url : urls) { + final LdapURL ldapUrl = new LdapURL(url); + if (ldapUrl.getHostname() == null) { + continue; + } + + final ConnectionFactory cf = connectionFactory.getConnectionFactory(ldapUrl.getHostnameWithSchemeAndPort()); + try { + final Q referralRequest = createReferralRequest(ldapUrl); + final Operation op = createReferralOperation(cf); + referralResult = op.execute(referralRequest); + } catch (LdapException e) { + // + } + if (referralResult != null && + (referralResult.getResultCode() == ResultCode.SUCCESS || + referralResult.getResultCode() == ResultCode.REFERRAL_LIMIT_EXCEEDED)) { + break; + } + } + return referralResult; + } + + + @Override + public S apply(final S result) { + if (result.getReferralURLs() == null || result.getReferralURLs().length == 0) { + return result; + } + S referralResult = result; + if (referralDepth <= referralLimit) { + final S r = followReferral(result.getReferralURLs()); + if (r != null) { + referralResult = r; + } + } + return referralResult; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/referral/DefaultReferralConnectionFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/referral/DefaultReferralConnectionFactory.java new file mode 100644 index 0000000..4fefcb8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/referral/DefaultReferralConnectionFactory.java @@ -0,0 +1,44 @@ + +package org.xbib.net.ldap.referral; + +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.DefaultConnectionFactory; + +/** + * Default implementation of a referral connection factory. Delegates to a {@link DefaultConnectionFactory}. + * + */ +public class DefaultReferralConnectionFactory implements ReferralConnectionFactory { + + /** + * Connection config for referrals. + */ + private final ConnectionConfig connectionConfig; + + + /** + * Creates a new default referral connection factory. + */ + public DefaultReferralConnectionFactory() { + this(new ConnectionConfig()); + } + + + /** + * Creates a new default referral connection factory. + * + * @param config connection configuration + */ + public DefaultReferralConnectionFactory(final ConnectionConfig config) { + connectionConfig = config; + } + + + @Override + public ConnectionFactory getConnectionFactory(final String url) { + final ConnectionConfig cc = ConnectionConfig.copy(connectionConfig); + cc.setLdapUrl(url); + return new DefaultConnectionFactory(cc); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/referral/FollowSearchReferralHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/referral/FollowSearchReferralHandler.java new file mode 100644 index 0000000..b35bc81 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/referral/FollowSearchReferralHandler.java @@ -0,0 +1,110 @@ + +package org.xbib.net.ldap.referral; + +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapURL; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.filter.FilterParseException; +import org.xbib.net.ldap.filter.FilterParser; +import org.xbib.net.ldap.handler.SearchResultHandler; +import org.xbib.net.ldap.transport.DefaultSearchOperationHandle; + +/** + * Provides handling of an ldap referral for search operations. + * + */ +public class FollowSearchReferralHandler extends AbstractFollowReferralHandler + implements SearchResultHandler { + + + /** + * Creates a new search referral handler. + */ + public FollowSearchReferralHandler() { + this(DEFAULT_REFERRAL_LIMIT, 1, new DefaultReferralConnectionFactory()); + } + + + /** + * Creates a new search referral handler. + * + * @param factory referral connection factory + */ + public FollowSearchReferralHandler(final ReferralConnectionFactory factory) { + this(DEFAULT_REFERRAL_LIMIT, 1, factory); + } + + + /** + * Creates a new search referral handler. + * + * @param limit number of referrals to follow + */ + public FollowSearchReferralHandler(final int limit) { + this(limit, 1, new DefaultReferralConnectionFactory()); + } + + + /** + * Creates a new search referral handler. + * + * @param limit number of referrals to follow + * @param factory referral connection factory + */ + public FollowSearchReferralHandler(final int limit, final ReferralConnectionFactory factory) { + this(limit, 1, factory); + } + + + /** + * Creates a new search referral handler. + * + * @param limit number of referrals to follow + * @param depth number of referrals followed + * @param factory referral connection factory + */ + private FollowSearchReferralHandler(final int limit, final int depth, final ReferralConnectionFactory factory) { + super(limit, depth, factory); + } + + + @Override + protected SearchRequest createReferralRequest(final LdapURL url) { + try { + return SearchRequest.builder() + .controls(getRequest().getControls()) + .scope(!url.isDefaultScope() ? url.getScope() : getRequest().getSearchScope()) + .dn(!url.isDefaultBaseDn() ? url.getBaseDn() : getRequest().getBaseDn()) + .filter(!url.isDefaultFilter() ? FilterParser.parse(url.getFilter()) : getRequest().getFilter()) + .sizeLimit(getRequest().getSizeLimit()) + .timeLimit(getRequest().getTimeLimit()) + .typesOnly(getRequest().isTypesOnly()) + .returnAttributes(getRequest().getReturnAttributes()) + .aliases(getRequest().getDerefAliases()) + .binaryAttributes(getRequest().getBinaryAttributes()) + .build(); + } catch (FilterParseException e) { + throw new IllegalStateException("Could not parse filter in the LDAP URL '" + url.getFilter() + "'", e); + } + } + + + @Override + protected SearchOperation createReferralOperation(final ConnectionFactory factory) { + final DefaultSearchOperationHandle handle = (DefaultSearchOperationHandle) getHandle(); + final SearchOperation op = new SearchOperation(factory); + op.setResultHandlers(handle.getOnResult()); + op.setEntryHandlers(handle.getOnEntry()); + op.setReferenceHandlers(handle.getOnReference()); + op.setControlHandlers(handle.getOnControl()); + op.setExceptionHandler(handle.getOnException()); + op.setIntermediateResponseHandlers(handle.getOnIntermediate()); + op.setReferralHandlers(handle.getOnReferral()); + op.setUnsolicitedNotificationHandlers(handle.getOnUnsolicitedNotification()); + op.setSearchResultHandlers( + new FollowSearchReferralHandler(getReferralLimit(), getReferralDepth() + 1, getReferralConnectionFactory())); + return op; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/referral/FollowSearchResultReferenceHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/referral/FollowSearchResultReferenceHandler.java new file mode 100644 index 0000000..fed9bd4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/referral/FollowSearchResultReferenceHandler.java @@ -0,0 +1,138 @@ + +package org.xbib.net.ldap.referral; + +import java.util.Iterator; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapURL; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.SearchResultReference; +import org.xbib.net.ldap.filter.FilterParseException; +import org.xbib.net.ldap.filter.FilterParser; +import org.xbib.net.ldap.handler.SearchResultHandler; +import org.xbib.net.ldap.transport.DefaultSearchOperationHandle; + +/** + * Provides handling of an ldap continuation reference for search operations. + * + */ +public class FollowSearchResultReferenceHandler extends AbstractFollowReferralHandler + implements SearchResultHandler { + + + /** + * Creates a new search result reference handler. + */ + public FollowSearchResultReferenceHandler() { + this(DEFAULT_REFERRAL_LIMIT, 1, new DefaultReferralConnectionFactory()); + } + + + /** + * Creates a new search result reference handler. + * + * @param factory referral connection factory + */ + public FollowSearchResultReferenceHandler(final ReferralConnectionFactory factory) { + this(DEFAULT_REFERRAL_LIMIT, 1, factory); + } + + + /** + * Creates a new search result reference handler. + * + * @param limit number of referrals to follow + */ + public FollowSearchResultReferenceHandler(final int limit) { + this(limit, 1, new DefaultReferralConnectionFactory()); + } + + + /** + * Creates a new search result reference handler. + * + * @param limit number of referrals to follow + * @param factory referral connection factory + */ + public FollowSearchResultReferenceHandler(final int limit, final ReferralConnectionFactory factory) { + this(limit, 1, factory); + } + + + /** + * Creates a new search result reference handler. + * + * @param limit number of referrals to follow + * @param depth number of referrals followed + * @param factory referral connection factory + */ + private FollowSearchResultReferenceHandler(final int limit, final int depth, final ReferralConnectionFactory factory) { + super(limit, depth, factory); + } + + + @Override + protected SearchRequest createReferralRequest(final LdapURL url) { + try { + return SearchRequest.builder() + .controls(getRequest().getControls()) + .scope(!url.isDefaultScope() ? url.getScope() : getRequest().getSearchScope()) + .dn(!url.isDefaultBaseDn() ? url.getBaseDn() : getRequest().getBaseDn()) + .filter(!url.isDefaultFilter() ? FilterParser.parse(url.getFilter()) : getRequest().getFilter()) + .sizeLimit(getRequest().getSizeLimit()) + .timeLimit(getRequest().getTimeLimit()) + .typesOnly(getRequest().isTypesOnly()) + .returnAttributes(getRequest().getReturnAttributes()) + .aliases(getRequest().getDerefAliases()) + .binaryAttributes(getRequest().getBinaryAttributes()) + .build(); + } catch (FilterParseException e) { + throw new IllegalStateException("Could not parse filter in the LDAP URL '" + url.getFilter() + "'", e); + } + } + + + @Override + protected SearchOperation createReferralOperation(final ConnectionFactory factory) { + final DefaultSearchOperationHandle handle = (DefaultSearchOperationHandle) getHandle(); + final SearchOperation op = new SearchOperation(factory); + op.setResultHandlers(handle.getOnResult()); + op.setEntryHandlers(handle.getOnEntry()); + op.setReferenceHandlers(handle.getOnReference()); + op.setControlHandlers(handle.getOnControl()); + op.setExceptionHandler(handle.getOnException()); + op.setIntermediateResponseHandlers(handle.getOnIntermediate()); + op.setReferralHandlers(handle.getOnReferral()); + op.setUnsolicitedNotificationHandlers(handle.getOnUnsolicitedNotification()); + op.setSearchResultHandlers( + new FollowSearchResultReferenceHandler( + getReferralLimit(), + getReferralDepth() + 1, + getReferralConnectionFactory())); + return op; + } + + + @Override + public SearchResponse apply(final SearchResponse result) { + if (result.getReferences() == null || result.getReferences().size() == 0) { + return result; + } + if (referralDepth <= referralLimit) { + final Iterator i = result.getReferences().iterator(); + while (i.hasNext()) { + final SearchResultReference ref = i.next(); + i.remove(); + final SearchResponse sr = followReferral(ref.getUris()); + if (sr != null) { + result.addEntries(sr.getEntries()); + if (sr.getReferralURLs() != null && sr.getReferralURLs().length > 0) { + result.addReferralURLs(sr.getReferralURLs()); + } + } + } + } + return result; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/referral/ReferralConnectionFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/referral/ReferralConnectionFactory.java new file mode 100644 index 0000000..971d6f1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/referral/ReferralConnectionFactory.java @@ -0,0 +1,20 @@ + +package org.xbib.net.ldap.referral; + +import org.xbib.net.ldap.ConnectionFactory; + +/** + * Factory for creating connections used by referrals. + * + */ +public interface ReferralConnectionFactory { + + + /** + * Returns a connection factory for use with a referral. + * + * @param url LDAP URL to the referral server + * @return connection factory + */ + ConnectionFactory getConnectionFactory(String url); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/CramMD5BindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/CramMD5BindRequest.java new file mode 100644 index 0000000..6e684ac --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/CramMD5BindRequest.java @@ -0,0 +1,74 @@ + +package org.xbib.net.ldap.sasl; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +/** + * LDAP CRAM-MD5 bind request. + * + */ +public class CramMD5BindRequest extends DefaultSaslClientRequest { + + /** + * CRAM-MD5 SASL mechanism. + */ + public static final Mechanism MECHANISM = Mechanism.CRAM_MD5; + + /** + * Authentication ID. + */ + private final String authenticationID; + + /** + * Password. + */ + private final String password; + + + /** + * Creates a new CRAM-MD5 bind request. + * + * @param authID to bind as + * @param pass password to bind with + */ + public CramMD5BindRequest(final String authID, final String pass) { + if (authID == null) { + throw new IllegalArgumentException("Authentication ID cannot be null"); + } + authenticationID = authID; + if (pass == null) { + throw new IllegalArgumentException("Password cannot be null"); + } + password = pass; + } + + + @Override + public void handle(final Callback[] callbacks) + throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(authenticationID); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(password.toCharArray()); + } else { + throw new UnsupportedCallbackException(callback, "Unsupported callback: " + callback); + } + } + } + + + @Override + public Mechanism getMechanism() { + return MECHANISM; + } + + + @Override + public String toString() { + return super.toString() + ", " + "authenticationID=" + authenticationID; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/DefaultSaslClientRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/DefaultSaslClientRequest.java new file mode 100644 index 0000000..1123862 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/DefaultSaslClientRequest.java @@ -0,0 +1,129 @@ + +package org.xbib.net.ldap.sasl; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.security.auth.callback.CallbackHandler; +import javax.security.sasl.Sasl; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.transport.DefaultSaslClient; + +/** + * Base class for SASL client requests. + * + */ +public abstract class DefaultSaslClientRequest implements CallbackHandler +{ + + /** + * LDAP controls. + */ + private RequestControl[] controls; + + /** + * Creates SASL client properties from the supplied configuration. + * + * @param config SASL config + * @return client properties + */ + public static Map createProperties(final SaslConfig config) { + final Map props = new HashMap<>(); + // add raw properties first, other properties will override if a conflict exists + if (!config.getProperties().isEmpty()) { + props.putAll(config.getProperties()); + } + if (config.getQualityOfProtection() != null) { + if (config.getQualityOfProtection().length == 0) { + throw new IllegalArgumentException("QOP cannot be empty"); + } + props.put( + Sasl.QOP, + Stream.of(config.getQualityOfProtection()).peek(q -> { + if (q == null) { + throw new IllegalArgumentException("QOP cannot be null"); + } + }).map(QualityOfProtection::string).collect(Collectors.joining(","))); + } + if (config.getSecurityStrength() != null) { + if (config.getSecurityStrength().length == 0) { + throw new IllegalArgumentException("Security strength cannot be empty"); + } + props.put( + Sasl.STRENGTH, + Stream.of(config.getSecurityStrength()).peek(s -> { + if (s == null) { + throw new IllegalArgumentException("Security strength cannot be null"); + } + }).map(s -> LdapUtils.toLowerCaseAscii(s.name())).collect(Collectors.joining(","))); + } + if (config.getMutualAuthentication() != null) { + props.put(Sasl.SERVER_AUTH, config.getMutualAuthentication().toString()); + } + return Collections.unmodifiableMap(props); + } + + public RequestControl[] getControls() { + return controls; + } + + public void setControls(final RequestControl... cntrls) { + controls = cntrls; + } + + /** + * Returns the SASL mechanism. + * + * @return SASL mechanism + */ + public abstract Mechanism getMechanism(); + + /** + * Returns the SASL authorization. + * + * @return SASL authorization + */ + public String getAuthorizationID() { + return null; + } + + /** + * Returns the SASL properties. + * + * @return SASL properties + */ + public Map getSaslProperties() { + return null; + } + + /** + * Returns the SASL client to use for this request. + * + * @return SASL client + */ + public SaslClient getSaslClient() { + return new DefaultSaslClient(); + } + + /** + * Creates a new bind request for this client. + * + * @param saslCredentials to bind with + * @return SASL bind request + */ + public SaslBindRequest createBindRequest(final byte[] saslCredentials) { + final SaslBindRequest req = new SaslBindRequest(getMechanism().mechanism(), saslCredentials); + req.setControls(getControls()); + return req; + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + "controls=" + Arrays.toString(controls); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/DigestMD5BindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/DigestMD5BindRequest.java new file mode 100644 index 0000000..0e29314 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/DigestMD5BindRequest.java @@ -0,0 +1,142 @@ + +package org.xbib.net.ldap.sasl; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.IntStream; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.RealmCallback; +import javax.security.sasl.RealmChoiceCallback; + +/** + * LDAP DIGEST-MD5 bind request. + * + */ +public class DigestMD5BindRequest extends DefaultSaslClientRequest { + + /** + * DIGEST-MD5 SASL mechanism. + */ + public static final Mechanism MECHANISM = Mechanism.DIGEST_MD5; + + /** + * Authentication ID. + */ + private final String authenticationID; + + /** + * Authorization ID. + */ + private final String authorizationID; + + /** + * Realm. + */ + private final String saslRealm; + + /** + * SASL client properties. + */ + private final Map saslProperties; + + /** + * Password. + */ + private final String password; + + + /** + * Creates a new DIGEST-MD5 bind request. + * + * @param authID to bind as + * @param authzID authorization ID + * @param pass password + * @param realm SASL realm + * @param props SASL client properties + */ + public DigestMD5BindRequest( + final String authID, + final String authzID, + final String pass, + final String realm, + final Map props) { + if (authID == null) { + throw new IllegalArgumentException("Authentication ID cannot be null"); + } + authenticationID = authID; + authorizationID = authzID; + if (pass == null) { + throw new IllegalArgumentException("Password cannot be null"); + } + password = pass; + saslRealm = realm; + saslProperties = Collections.unmodifiableMap(props); + } + + + @Override + public void handle(final Callback[] callbacks) + throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(authenticationID); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(password.toCharArray()); + } else if (callback instanceof RealmCallback rc) { + if (saslRealm == null) { + final String defaultRealm = rc.getDefaultText(); + if (defaultRealm == null) { + throw new IllegalStateException("Default realm required, but none provided"); + } else { + rc.setText(defaultRealm); + } + } else { + rc.setText(saslRealm); + } + } else if (callback instanceof RealmChoiceCallback rcc) { + if (saslRealm == null) { + throw new IllegalStateException( + "Realm required, choose one of the following: " + Arrays.toString(rcc.getChoices())); + } else if (rcc.getChoices() != null) { + final int selectedIndex = IntStream.range( + 0, rcc.getChoices().length).filter(i -> rcc.getChoices()[i].equals(saslRealm)).findFirst().getAsInt(); + rcc.setSelectedIndex(selectedIndex); + } + } else { + throw new UnsupportedCallbackException(callback, "Unsupported callback: " + callback); + } + } + } + + + @Override + public Mechanism getMechanism() { + return MECHANISM; + } + + + @Override + public String getAuthorizationID() { + return authorizationID; + } + + + @Override + public Map getSaslProperties() { + return saslProperties; + } + + + @Override + public String toString() { + return super.toString() + ", " + + "authenticationID=" + authenticationID + ", " + + "authorizationID=" + authorizationID + ", " + + "saslRealm=" + saslRealm + ", " + + "saslProperties=" + saslProperties; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/ExternalBindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/ExternalBindRequest.java new file mode 100644 index 0000000..145bac6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/ExternalBindRequest.java @@ -0,0 +1,32 @@ + +package org.xbib.net.ldap.sasl; + +/** + * LDAP external bind request. + * + */ +public class ExternalBindRequest extends SaslBindRequest { + + /** + * External SASL mechanism. + */ + public static final Mechanism MECHANISM = Mechanism.EXTERNAL; + + + /** + * Creates a new external bind request. + */ + public ExternalBindRequest() { + this(null); + } + + + /** + * Creates a new external bind request. + * + * @param authzID to bind as + */ + public ExternalBindRequest(final String authzID) { + super(MECHANISM.mechanism(), authzID != null ? authzID : ""); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/GssApiBindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/GssApiBindRequest.java new file mode 100644 index 0000000..43d722c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/GssApiBindRequest.java @@ -0,0 +1,261 @@ + +package org.xbib.net.ldap.sasl; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.RealmCallback; +import org.xbib.net.ldap.transport.DefaultSaslClient; +import org.xbib.net.ldap.transport.GssApiSaslClient; + +/** + * LDAP GSSAPI bind request. + * + */ +public class GssApiBindRequest extends DefaultSaslClientRequest { + + /** + * GSSAPI SASL mechanism. + */ + private static final Mechanism MECHANISM = Mechanism.GSSAPI; + + /** + * SASL property to control the JAAS configuration name. + */ + private static final String JAAS_OPTIONS_PROPERTY_PREFIX = "org.xbib.net.ldap.sasl.gssapi.jaas."; + + /** + * Property for the JAAS entry name from a configuration file. + */ + public static final String JAAS_NAME_PROPERTY = JAAS_OPTIONS_PROPERTY_PREFIX + "name"; + /** + * Property for JAAS refreshConfig. + */ + public static final String JAAS_REFRESH_CONFIG_PROPERTY = JAAS_OPTIONS_PROPERTY_PREFIX + "refreshConfig"; + /** + * Default name of the JAAS configuration. + */ + private static final String DEFAULT_GSSAPI_JAAS_NAME = "gssapi"; + /** + * Property for the login module class name for GSSAPI. + */ + private static final String JAAS_LOGIN_MODULE_PROPERTY = JAAS_OPTIONS_PROPERTY_PREFIX + "loginModule"; + + /** + * Default login module for GSSAPI. + */ + private static final String DEFAULT_GSSAPI_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; + + /** + * Authentication ID. + */ + private final String authenticationID; + + /** + * Authorization ID. + */ + private final String authorizationID; + + /** + * Realm. + */ + private final String saslRealm; + + /** + * SASL client properties. + */ + private final Map saslProperties; + + /** + * Name of the JAAS configuration. + */ + private final String jaasName; + + /** + * Whether to refresh the JAAS configuration prior to use. + * See {@link javax.security.auth.login.Configuration#refresh()}. + */ + private final boolean jaasRefreshConfig; + + /** + * Class name of the JAAS login module to use for GSSAPI. + */ + private final String jaasLoginModule; + + /** + * Options set on the JAAS login module. + */ + private final Map jaasOptions; + + /** + * Password. + */ + private final String password; + + /** + * Boolean that ensures the {@link GssApiSaslClient} is only returned on the first request. + */ + private final AtomicBoolean invokeOnce = new AtomicBoolean(); + + + /** + * Creates a new GSSAPI bind request. + * + * @param authID to bind as + * @param authzID authorization ID + * @param pass password to bind with + * @param realm SASL realm + * @param props SASL client properties + */ + public GssApiBindRequest( + final String authID, + final String authzID, + final String pass, + final String realm, + final Map props) { + authenticationID = authID; + authorizationID = authzID; + password = pass; + saslRealm = realm; + saslProperties = props.entrySet().stream() + .filter(e -> !e.getKey().startsWith(JAAS_OPTIONS_PROPERTY_PREFIX)) + .collect( + Collectors.collectingAndThen( + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue), Collections::unmodifiableMap)); + jaasLoginModule = (String) props.getOrDefault(JAAS_LOGIN_MODULE_PROPERTY, DEFAULT_GSSAPI_LOGIN_MODULE); + jaasOptions = props.entrySet().stream() + .filter(e -> + e.getKey().startsWith(JAAS_OPTIONS_PROPERTY_PREFIX) && + !e.getKey().equals(JAAS_NAME_PROPERTY) && + !e.getKey().equals(JAAS_LOGIN_MODULE_PROPERTY)) + .collect( + Collectors.collectingAndThen( + Collectors.toMap( + e -> e.getKey().substring(JAAS_OPTIONS_PROPERTY_PREFIX.length()), + Map.Entry::getValue), + Collections::unmodifiableMap)); + if (props.get(JAAS_NAME_PROPERTY) == null) { + if (props.get(JAAS_LOGIN_MODULE_PROPERTY) == null && jaasOptions.isEmpty()) { + jaasName = DEFAULT_GSSAPI_JAAS_NAME; + } else { + jaasName = null; + } + } else { + jaasName = (String) props.get(JAAS_NAME_PROPERTY); + } + jaasRefreshConfig = Boolean.parseBoolean((String) props.getOrDefault(JAAS_REFRESH_CONFIG_PROPERTY, "false")); + } + + + @Override + public SaslClient getSaslClient() { + if (invokeOnce.compareAndSet(false, true)) { + return new GssApiSaslClient(); + } else { + return new DefaultSaslClient(); + } + } + + + @Override + public void handle(final Callback[] callbacks) + throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(authenticationID); + } else if (callback instanceof PasswordCallback) { + if (password == null) { + throw new UnsupportedCallbackException(callback, "Password required for PasswordCallback"); + } else { + ((PasswordCallback) callback).setPassword(password.toCharArray()); + } + } else if (callback instanceof RealmCallback rc) { + if (saslRealm == null) { + throw new IllegalStateException("Realm required, but none provided"); + } else { + rc.setText(saslRealm); + } + } else { + throw new UnsupportedCallbackException(callback, "Unsupported callback: " + callback); + } + } + } + + + @Override + public Mechanism getMechanism() { + return MECHANISM; + } + + + @Override + public String getAuthorizationID() { + return authorizationID; + } + + + @Override + public Map getSaslProperties() { + return saslProperties; + } + + + /** + * Returns the entry name in a JAAS configuration file. + * + * @return JAAS configuration name + */ + public String getJaasName() { + return jaasName; + } + + + /** + * Returns whether to refresh the JAAS configuration prior to use. See {@link + * javax.security.auth.login.Configuration#refresh()}. + * + * @return whether to refresh the JAAS config + */ + public boolean getJaasRefreshConfig() { + return jaasRefreshConfig; + } + + + /** + * Returns the class name of the JAAS login module. + * + * @return JAAS login module class name + */ + public String getJaasLoginModule() { + return jaasLoginModule; + } + + + /** + * Returns the JAAS options for the login module. + * + * @return JAAS options + */ + public Map getJaasOptions() { + return jaasOptions; + } + + + @Override + public String toString() { + return super.toString() + ", " + + "authenticationID=" + authenticationID + ", " + + "authorizationID=" + authorizationID + ", " + + "realm=" + saslRealm + ", " + + "saslProperties=" + saslProperties + ", " + + "jaasName=" + jaasName + ", " + + "jaasRefreshConfig=" + jaasRefreshConfig + ", " + + "jaasLoginModule=" + jaasLoginModule + ", " + + "jaasOptions=" + jaasOptions; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/Mechanism.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/Mechanism.java new file mode 100644 index 0000000..fb39f30 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/Mechanism.java @@ -0,0 +1,97 @@ + +package org.xbib.net.ldap.sasl; + +/** + * Enum to define SASL mechanisms. + * + */ +public enum Mechanism { + + /** + * External authentication type. + */ + EXTERNAL("EXTERNAL"), + + /** + * Digest MD5 authentication type. + */ + DIGEST_MD5("DIGEST-MD5"), + + /** + * Cram MD5 authentication type. + */ + CRAM_MD5("CRAM-MD5"), + + /** + * Kerberos authentication type. + */ + GSSAPI("GSSAPI"), + + /** + * SCRAM SHA1. + */ + SCRAM_SHA_1("SCRAM-SHA-1", "SHA-1", "HmacSHA1"), + + /** + * SCRAM SHA256. + */ + SCRAM_SHA_256("SCRAM-SHA-256", "SHA-256", "HmacSHA256"), + + /** + * SCRAM SHA512. + */ + SCRAM_SHA_512("SCRAM-SHA-512", "SHA-512", "HmacSHA512"); + + + /** + * SASL mechanism name. + */ + private final String mechanismName; + + /** + * Digest algorithm name. + */ + private final String[] properties; + + + /** + * Creates a new mechanism. + * + * @param mechanism SASL mechanism name + */ + Mechanism(final String mechanism) { + this(mechanism, (String[]) null); + } + + + /** + * Creates a new mechanism. + * + * @param mechanism SASL mechanism name + * @param props mechanism properties + */ + Mechanism(final String mechanism, final String... props) { + mechanismName = mechanism; + properties = props; + } + + + /** + * Returns the name of this mechanism. + * + * @return mechanism name + */ + public String mechanism() { + return mechanismName; + } + + + /** + * Returns any properties associated with this mechanism. + * + * @return mechanism properties or null + */ + public String[] properties() { + return properties; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/QualityOfProtection.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/QualityOfProtection.java new file mode 100644 index 0000000..0722e02 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/QualityOfProtection.java @@ -0,0 +1,63 @@ + +package org.xbib.net.ldap.sasl; + +/** + * Enum to define SASL quality of protection. + * + */ +public enum QualityOfProtection { + + /** + * Authentication only. + */ + AUTH("auth"), + + /** + * Authentication with integrity protection. + */ + AUTH_INT("auth-int"), + + /** + * Authentication with integrity and privacy protection. + */ + AUTH_CONF("auth-conf"); + + /** + * Quality of protection strings. + */ + private final String qop; + + + /** + * Creates a new quality of protection. + * + * @param s quality of protection strings + */ + QualityOfProtection(final String s) { + qop = s; + } + + /** + * Returns the quality of protection for the supplied protection string. + * + * @param s to find quality of protection for + * @return quality of protection + */ + public static QualityOfProtection fromString(final String s) { + for (QualityOfProtection p : QualityOfProtection.values()) { + if (p.string().equalsIgnoreCase(s)) { + return p; + } + } + return null; + } + + /** + * Returns the protection string. + * + * @return protection string + */ + public String string() { + return qop; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslBindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslBindRequest.java new file mode 100644 index 0000000..39ff97b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslBindRequest.java @@ -0,0 +1,171 @@ + +package org.xbib.net.ldap.sasl; + +import org.xbib.net.ldap.AbstractRequestMessage; +import org.xbib.net.ldap.BindRequest; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.ApplicationDERTag; +import org.xbib.asn1.ConstructedDEREncoder; +import org.xbib.asn1.ContextDERTag; +import org.xbib.asn1.DEREncoder; +import org.xbib.asn1.IntegerType; +import org.xbib.asn1.OctetStringType; + +/** + * LDAP SASL bind request defined as: + * + *
+ * SaslCredentials ::= SEQUENCE {
+ * mechanism               LDAPString,
+ * credentials             OCTET STRING OPTIONAL }
+ * 
+ * + */ +public class SaslBindRequest extends AbstractRequestMessage implements BindRequest { + + /** + * SASL mechanism. + */ + private String saslMechanism; + + /** + * SASL credentials. + */ + private byte[] saslCredentials; + + + /** + * Default constructor. + */ + private SaslBindRequest() { + } + + + /** + * Creates a new SASL bind request. + * + * @param mechanism type of SASL request + */ + public SaslBindRequest(final String mechanism) { + this(mechanism, (byte[]) null); + } + + + /** + * Creates a new SASL bind request. + * + * @param mechanism type of SASL request + * @param credentials to bind as + */ + public SaslBindRequest(final String mechanism, final String credentials) { + this(mechanism, LdapUtils.utf8Encode(credentials)); + } + + + /** + * Creates a new SASL bind request. + * + * @param mechanism type of SASL request + * @param credentials to bind as + */ + public SaslBindRequest(final String mechanism, final byte[] credentials) { + saslMechanism = mechanism; + saslCredentials = credentials; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + @Override + protected DEREncoder[] getRequestEncoders(final int id) { + final ConstructedDEREncoder saslMechanismEncoder; + // CheckStyle:MagicNumber OFF + if (saslCredentials == null) { + saslMechanismEncoder = new ConstructedDEREncoder( + new ContextDERTag(3, true), + new OctetStringType(saslMechanism)); + } else { + saslMechanismEncoder = new ConstructedDEREncoder( + new ContextDERTag(3, true), + new OctetStringType(saslMechanism), + new OctetStringType(saslCredentials)); + } + // CheckStyle:MagicNumber ON + return new DEREncoder[]{ + new IntegerType(id), + new ConstructedDEREncoder( + new ApplicationDERTag(PROTOCOL_OP, true), + new IntegerType(VERSION), + new OctetStringType(""), + saslMechanismEncoder), + }; + } + + @Override + public String toString() { + return super.toString() + ", " + "saslMechanism=" + saslMechanism; + } + + /** + * SASL bind request builder. + */ + public static class Builder extends + AbstractRequestMessage.AbstractBuilder { + + + /** + * Default constructor. + */ + protected Builder() { + super(new SaslBindRequest()); + } + + + @Override + protected Builder self() { + return this; + } + + + /** + * Sets the SASL mechanism. + * + * @param mechanism SASL mechanism + * @return this builder + */ + public Builder mechanism(final String mechanism) { + object.saslMechanism = mechanism; + return self(); + } + + + /** + * Sets the SASL credentials. + * + * @param credentials SASL credentials + * @return this builder + */ + public Builder credentials(final byte[] credentials) { + object.saslCredentials = credentials; + return self(); + } + + + /** + * Sets the SASL credentials. + * + * @param credentials SASL credentials + * @return this builder + */ + public Builder credentials(final String credentials) { + object.saslCredentials = LdapUtils.utf8Encode(credentials); + return self(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslClient.java new file mode 100644 index 0000000..643ba73 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslClient.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.sasl; + +import org.xbib.net.ldap.BindResponse; +import org.xbib.net.ldap.transport.TransportConnection; + +/** + * SASL client that negotiates the details of the bind operation. + * + * @param type of request + */ +public interface SaslClient { + + + /** + * Performs a SASL bind. + * + * @param conn to perform the bind on + * @param request SASL request to perform + * @return final result of the bind process + * @throws Exception if an error occurs + */ + BindResponse bind(TransportConnection conn, T request) + throws Exception; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslClientRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslClientRequest.java new file mode 100644 index 0000000..90344ad --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslClientRequest.java @@ -0,0 +1,17 @@ + +package org.xbib.net.ldap.sasl; + +/** + * Maker interface for SASL mechanisms that use a custom client. + * + */ +public interface SaslClientRequest { + + + /** + * Returns the SASL client used by this request. + * + * @return SASL client + */ + SaslClient getSaslClient(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslConfig.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslConfig.java new file mode 100644 index 0000000..7d61aee --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SaslConfig.java @@ -0,0 +1,269 @@ + +package org.xbib.net.ldap.sasl; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.AbstractConfig; + +/** + * Contains basic configuration data for SASL authentication. + * + */ +public class SaslConfig extends AbstractConfig { + + /** + * sasl properties. + */ + private final Map properties = new HashMap<>(); + /** + * sasl mechanism. + */ + private Mechanism mechanism; + /** + * sasl authorization id. + */ + private String authorizationId; + /** + * perform mutual authentication. + */ + private Boolean mutualAuthentication; + /** + * sasl quality of protection. + */ + private QualityOfProtection[] qualityOfProtection; + /** + * sasl security strength. + */ + private SecurityStrength[] securityStrength; + /** + * sasl realm. + */ + private String saslRealm; + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the sasl mechanism. + * + * @return mechanism + */ + public Mechanism getMechanism() { + return mechanism; + } + + /** + * Sets the sasl mechanism. + * + * @param m mechanism + */ + public void setMechanism(final Mechanism m) { + mechanism = m; + } + + /** + * Returns the sasl authorization id. + * + * @return authorization id + */ + public String getAuthorizationId() { + return authorizationId; + } + + /** + * Sets the sasl authorization id. + * + * @param id authorization id + */ + public void setAuthorizationId(final String id) { + authorizationId = id; + } + + /** + * Returns whether mutual authentication should occur. + * + * @return whether mutual authentication should occur + */ + public Boolean getMutualAuthentication() { + return mutualAuthentication; + } + + /** + * Sets whether mutual authentication should occur. + * + * @param b whether mutual authentication should occur + */ + public void setMutualAuthentication(final Boolean b) { + mutualAuthentication = b; + } + + /** + * Returns the sasl quality of protection. + * + * @return quality of protection + */ + public QualityOfProtection[] getQualityOfProtection() { + return qualityOfProtection; + } + + /** + * Sets the sasl quality of protection. + * + * @param qop quality of protection + */ + public void setQualityOfProtection(final QualityOfProtection... qop) { + checkArrayContainsNull(qop); + qualityOfProtection = qop; + } + + /** + * Returns the sasl security strength. + * + * @return security strength + */ + public SecurityStrength[] getSecurityStrength() { + return securityStrength; + } + + /** + * Sets the sasl security strength. + * + * @param ss security strength + */ + public void setSecurityStrength(final SecurityStrength... ss) { + checkArrayContainsNull(ss); + securityStrength = ss; + } + + /** + * Returns the sasl realm. + * + * @return realm + */ + public String getRealm() { + return saslRealm; + } + + /** + * Sets the sasl realm. + * + * @param realm to set + */ + public void setRealm(final String realm) { + saslRealm = realm; + } + + /** + * Returns sasl properties. + * + * @return properties + */ + public Map getProperties() { + return properties; + } + + /** + * Sets sasl properties. + * + * @param props to set + */ + public void setProperties(final Map props) { + properties.putAll(props); + } + + /** + * Returns a sasl property. + * + * @param name of the property + * @return property + */ + public Object getProperty(final String name) { + return properties.get(name); + } + + /** + * Sets a sasl property. + * + * @param name of the property + * @param value of the property + */ + public void setProperty(final String name, final Object value) { + properties.put(name, value); + } + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + + "mechanism=" + mechanism + ", " + + "authorizationId=" + authorizationId + ", " + + "mutualAuthentication=" + mutualAuthentication + ", " + + "qualityOfProtection=" + Arrays.toString(qualityOfProtection) + ", " + + "securityStrength=" + Arrays.toString(securityStrength) + ", " + + "realm=" + saslRealm + ", " + + "properties=" + properties + "]"; + } + + public static class Builder { + + + private final SaslConfig object = new SaslConfig(); + + + protected Builder() { + } + + + public Builder mechanism(final Mechanism mechanism) { + object.setMechanism(mechanism); + return this; + } + + + public Builder authorizationId(final String id) { + object.setAuthorizationId(id); + return this; + } + + + public Builder mutualAuthentication(final Boolean b) { + object.setMutualAuthentication(b); + return this; + } + + + public Builder qualityOfProtection(final QualityOfProtection... protections) { + object.setQualityOfProtection(protections); + return this; + } + + + public Builder securityStrength(final SecurityStrength... strengths) { + object.setSecurityStrength(strengths); + return this; + } + + + public Builder realm(final String realm) { + object.setRealm(realm); + return this; + } + + + public Builder property(final String name, final Object value) { + object.setProperty(name, value); + return this; + } + + + public SaslConfig build() { + return object; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/ScramBindRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/ScramBindRequest.java new file mode 100644 index 0000000..0df2453 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/ScramBindRequest.java @@ -0,0 +1,89 @@ + +package org.xbib.net.ldap.sasl; + +import org.xbib.net.ldap.transport.ScramSaslClient; + +/** + * LDAP SCRAM (Salted Challenge Response Authentication Mechanism) bind request. + * + */ +public class ScramBindRequest implements SaslClientRequest { + + /** + * Mechanism. + */ + private final Mechanism scramMechanism; + + /** + * Username. + */ + private final String username; + + /** + * Password. + */ + private final String password; + + /** + * Scram nonce. + */ + private final byte[] scramNonce; + + + /** + * Creates a new scram bind request. + * + * @param mech SCRAM SASL mechanism + * @param user to bind as + * @param pass to bind with + */ + public ScramBindRequest(final Mechanism mech, final String user, final String pass) { + this(mech, user, pass, null); + } + + + /** + * Creates a new scram bind request. + * + * @param mech SCRAM SASL mechanism + * @param user to bind as + * @param pass to bind with + * @param nonce to use with the SCRAM protocol + */ + public ScramBindRequest(final Mechanism mech, final String user, final String pass, final byte[] nonce) { + if (mech != Mechanism.SCRAM_SHA_1 && mech != Mechanism.SCRAM_SHA_256 && mech != Mechanism.SCRAM_SHA_512) { + throw new IllegalArgumentException("Invalid SCRAM mechanism: " + mech); + } + scramMechanism = mech; + username = user; + password = pass; + scramNonce = nonce; + } + + + public Mechanism getMechanism() { + return scramMechanism; + } + + + public String getUsername() { + return username; + } + + + public String getPassword() { + return password; + } + + + public byte[] getNonce() { + return scramNonce; + } + + + @Override + public ScramSaslClient getSaslClient() { + return new ScramSaslClient(); + } +} + diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SecurityStrength.java b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SecurityStrength.java new file mode 100644 index 0000000..3e327b9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/sasl/SecurityStrength.java @@ -0,0 +1,24 @@ + +package org.xbib.net.ldap.sasl; + +/** + * Enum to define SASL security strength. + * + */ +public enum SecurityStrength { + + /** + * High security strength. + */ + HIGH, + + /** + * Medium security strength. + */ + MEDIUM, + + /** + * Low security strength. + */ + LOW +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractDefaultDefinitionFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractDefaultDefinitionFunction.java new file mode 100644 index 0000000..1d94e0c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractDefaultDefinitionFunction.java @@ -0,0 +1,287 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for default definition functions. + * + * @param type of schema element + */ +public abstract class AbstractDefaultDefinitionFunction implements DefinitionFunction { + + + /** + * Validates that the supplied definition is generally of the correct form. Must start with an open parenthesis and + * end with a close parenthesis. + * + * @param definition to validate + * @return buffer without opening and closing parenthesis + * @throws SchemaParseException if the buffer is invalid + */ + protected CharBuffer validate(final String definition) + throws SchemaParseException { + if (definition == null || definition.isEmpty()) { + throw new SchemaParseException("Definition cannot be null or empty"); + } + CharBuffer buffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(definition.getBytes())); + if (buffer.get() != '(') { + throw new SchemaParseException("Definition '" + definition + "' must start with '('"); + } + if (buffer.get(buffer.limit() - 1) != ')') { + throw new SchemaParseException("Definition '" + definition + "' must end with ')'"); + } + if (!buffer.hasRemaining()) { + throw new SchemaParseException("Definition '" + definition + "' does not contain an expression"); + } + buffer = buffer.limit(buffer.limit() - 1).slice(); + if (!buffer.hasRemaining()) { + throw new SchemaParseException("Definition '" + definition + "' does not contain an expression"); + } + return buffer; + } + + + /** + * Reads the buffer until a space is encountered. + * + * @param cb to read from + * @return oid + */ + protected String readOID(final CharBuffer cb) { + return readUntilSpace(cb); + } + + + /** + * Reads the supplied buffer for $ delimited data between an open and closed parenthesis. Returns an array of + * integers containing each rule ID that was read. If the buffer doesn't start with an open parenthesis, an array of + * a single oid is returned. Advances the buffer to the position after the string. + * + * @param cb to read from + * @return oids + */ + protected String[] readOIDs(final CharBuffer cb) { + if (!cb.hasRemaining()) { + throw new IllegalArgumentException("Cannot read oids from empty buffer"); + } + char c = cb.get(); + if (c != '(') { + return new String[]{readOID(cb.position(cb.position() - 1))}; + } + if (!cb.hasRemaining()) { + throw new IllegalArgumentException("Cannot read oids with empty content"); + } + + final int startPos = cb.position(); + final int limit = cb.limit(); + c = readUntil(cb, ')'); + if (c != ')') { + throw new IllegalArgumentException("oids must end with a close paren"); + } + final int endPos = cb.position() - 1; + final CharBuffer slice = cb.limit(endPos).position(startPos).slice(); + cb.limit(limit).position(endPos + 1); + final String[] oids = SchemaUtils.parseOIDs(slice.toString().trim()); + if (oids.length == 0) { + throw new IllegalArgumentException("oids cannot be empty"); + } + return oids; + } + + + /** + * Reads the buffer until a space is encountered. Converts the read string into an integer. + * + * @param cb to read from + * @return rule id + */ + protected int readRuleID(final CharBuffer cb) { + final String id = readUntilSpace(cb); + return Integer.parseInt(id); + } + + + /** + * Reads the supplied buffer for space delimited data between an open and closed parenthesis. Returns an array of + * integers containing each rule ID that was read. Advances the buffer to the position after the string. + * + * @param cb to read from + * @return rule ids + */ + protected int[] readRuleIDs(final CharBuffer cb) { + if (!cb.hasRemaining()) { + throw new IllegalArgumentException("Cannot read ruleids from empty buffer"); + } + char c = cb.get(); + if (c != '(') { + return new int[]{readRuleID(cb.position(cb.position() - 1))}; + } + if (!cb.hasRemaining()) { + throw new IllegalArgumentException("Cannot read ruleids with empty content"); + } + + final int startPos = cb.position(); + final int limit = cb.limit(); + c = readUntil(cb, ')'); + if (c != ')') { + throw new IllegalArgumentException("ruleids must end with a close paren"); + } + final int endPos = cb.position() - 1; + final CharBuffer slice = cb.limit(endPos).position(startPos).slice(); + cb.limit(limit).position(endPos + 1); + final int[] ids = SchemaUtils.parseNumbers(slice.toString().trim()); + if (ids.length == 0) { + throw new IllegalArgumentException("ruleids cannot be empty"); + } + return ids; + } + + + /** + * Reads the supplied buffer for content between two single quotes. Returns a string for the portion of the buffer + * that was read. Advances the buffer to the position after the string. + * + * @param cb to read from + * @return string read from the buffer + */ + protected String readQDString(final CharBuffer cb) { + if (!cb.hasRemaining()) { + throw new IllegalArgumentException("Cannot read qdstring from empty buffer"); + } + char c = cb.get(); + if (c != '\'') { + throw new IllegalArgumentException("qdstring must start with a single quote"); + } + if (!cb.hasRemaining()) { + throw new IllegalArgumentException("Cannot read qdstring with empty content"); + } + + final int startPos = cb.position(); + final int limit = cb.limit(); + c = readUntil(cb, '\''); + if (c != '\'') { + throw new IllegalArgumentException("qdstring must end with a single quote"); + } + final int endPos = cb.position() - 1; + final CharBuffer slice = cb.limit(endPos).position(startPos).slice(); + cb.limit(limit).position(endPos + 1); + return slice.toString(); + } + + + /** + * Reads the supplied buffer for single quoted data between an open and closed parenthesis. Returns an array of + * strings containing each qdstring that was read. If the buffer contains only data between single quotes, an array of + * a single qdstring is returned. Advances the buffer to the position after the string. + * + * @param cb to read from + * @return string read from the buffer + */ + protected String[] readQDStrings(final CharBuffer cb) { + if (!cb.hasRemaining()) { + throw new IllegalArgumentException("Cannot read qdstrings from empty buffer"); + } + char c = cb.get(); + if (c == '\'') { + return new String[]{readQDString(cb.position(cb.position() - 1))}; + } else if (c != '(') { + throw new IllegalArgumentException("qdstrings must start with a single quote or an open paren"); + } + if (!cb.hasRemaining()) { + throw new IllegalArgumentException("Cannot read qdstrings with empty content"); + } + + final List values = new ArrayList<>(); + final int limit = cb.limit(); + while (cb.hasRemaining()) { + c = cb.get(); + if (c == ')') { + break; + } else if (c == '\'') { + final int startValue = cb.position(); + c = readUntil(cb, '\''); + if (c != '\'') { + throw new IllegalArgumentException("qdstring must end with a single quote"); + } + final int endPos = cb.position() - 1; + final CharBuffer slice = cb.limit(endPos).position(startValue).slice(); + cb.limit(limit).position(endPos + 1); + values.add(slice.toString()); + } + } + if (c != ')') { + throw new IllegalArgumentException("qdstrings must end with a close paren"); + } + if (values.isEmpty()) { + throw new IllegalArgumentException("qdstrings cannot be empty"); + } + return values.toArray(new String[0]); + } + + + /** + * Reads the supplied buffer until a space is found. Returns a string for the portion of the buffer that was read. + * Advances the buffer to the position after the string. + * + * @param cb to read from + * @return string read from the buffer or empty string if the buffer has no remaining characters + */ + protected String readUntilSpace(final CharBuffer cb) { + if (!cb.hasRemaining()) { + return ""; + } + final int startPos = cb.position(); + final int limit = cb.limit(); + readUntil(cb, ' '); + final int endPos = cb.position() - 1; + final CharBuffer slice = cb.limit(endPos).position(startPos).slice(); + cb.limit(limit).position(endPos); + return slice.toString(); + } + + + /** + * Advances the buffer position to the first character that is not a space or the end of the buffer is reached. No-op + * if the buffer has no remaining characters. + * + * @param cb to read from + */ + protected void skipSpaces(final CharBuffer cb) { + if (!cb.hasRemaining()) { + return; + } + while (cb.hasRemaining()) { + if (cb.get() != ' ') { + break; + } + } + if (!cb.hasRemaining()) { + return; + } + cb.position(cb.position() - 1); + } + + + /** + * Advances the buffer position until the supplied character is found or the end of the buffer is reached. + * + * @param cb to read from + * @param c to stop advancing at + * @return the last character read + */ + private char readUntil(final CharBuffer cb, final char c) { + char bufferChar = 0; + while (cb.hasRemaining()) { + bufferChar = cb.get(); + if (bufferChar == c) { + break; + } + } + return bufferChar; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractNamedSchemaElement.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractNamedSchemaElement.java new file mode 100644 index 0000000..1e7c333 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractNamedSchemaElement.java @@ -0,0 +1,90 @@ + +package org.xbib.net.ldap.schema; + +/** + * Base schema bean for named schema elements. + * + */ +public abstract class AbstractNamedSchemaElement extends AbstractSchemaElement { + + /** + * Names. + */ + private String[] names; + + /** + * Obsolete. + */ + private boolean obsolete; + + + /** + * Returns the first name defined or null if no names are defined. + * + * @return first name in the list + */ + public String getName() { + if (names != null && names.length > 0) { + return names[0]; + } + return null; + } + + + /** + * Returns the names. + * + * @return names + */ + public String[] getNames() { + return names; + } + + + /** + * Sets the names. + * + * @param s names + */ + public void setNames(final String[] s) { + names = s; + } + + + /** + * Returns whether the supplied string matches, ignoring case, any of the names for this schema element. + * + * @param s to match + * @return whether the supplied string matches a name + */ + public boolean hasName(final String s) { + if (names != null) { + for (String name : names) { + if (name.equalsIgnoreCase(s)) { + return true; + } + } + } + return false; + } + + + /** + * Returns whether this attribute type definition is obsolete. + * + * @return whether this attribute type definition is obsolete + */ + public boolean isObsolete() { + return obsolete; + } + + + /** + * Sets whether this attribute type definition is obsolete. + * + * @param b whether this attribute type definition is obsolete + */ + public void setObsolete(final boolean b) { + obsolete = b; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractRegexDefinitionFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractRegexDefinitionFunction.java new file mode 100644 index 0000000..9ad6998 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractRegexDefinitionFunction.java @@ -0,0 +1,64 @@ + +package org.xbib.net.ldap.schema; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Base class for regex definition functions. + * + * @param type of schema element + */ +public abstract class AbstractRegexDefinitionFunction implements DefinitionFunction { + + /** + * Regex to match zero or more spaces. + */ + protected static final String WSP_REGEX = "[ ]*"; + + /** + * Regex to match one or more spaces. + */ + protected static final String ONE_WSP_REGEX = "[ ]+"; + + /** + * Regex to match one or more non spaces. + */ + protected static final String NO_WSP_REGEX = "[^ ]+"; + + /** + * Pattern to match extensions. + */ + private static final Pattern EXTENSIONS_PATTERN = Pattern.compile( + "(?:(X-[^ ]+)[ ]*(?:'([^']+)'|\\(([^\\)]+)\\))?)+"); + + + /** + * Parses extensions from the supplied definition. + * + * @param definition that was parsed + * @return extensions + */ + protected Extensions parseExtensions(final String definition) { + final Matcher m = EXTENSIONS_PATTERN.matcher(definition); + final Extensions exts = new Extensions(); + while (m.find()) { + final String name = m.group(1).trim(); + final List values = new ArrayList<>(1); + + // CheckStyle:MagicNumber OFF + if (m.group(2) != null) { + values.add(m.group(2).trim()); + } else if (m.group(3) != null) { + values.addAll(Arrays.asList(SchemaUtils.parseDescriptors(m.group(3).trim()))); + } + // CheckStyle:MagicNumber ON + exts.addExtension(name, values); + } + + return exts; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractSchemaElement.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractSchemaElement.java new file mode 100644 index 0000000..b0bc690 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AbstractSchemaElement.java @@ -0,0 +1,84 @@ + +package org.xbib.net.ldap.schema; + +/** + * Base class for schema elements. + * + */ +public abstract class AbstractSchemaElement implements SchemaElement { + + /** + * Description. + */ + private String description; + + /** + * Extensions. + */ + private Extensions extensions; + + /** + * Returns whether the supplied schema element has an extension name with a value of 'true'. + * + * @param type of schema element + * @param schemaElement to inspect + * @param extensionName to read boolean from + * @return whether syntax has this boolean extension + */ + public static boolean containsBooleanExtension( + final T schemaElement, + final String extensionName) { + if (schemaElement != null) { + final Extensions exts = schemaElement.getExtensions(); + return exts != null && Boolean.parseBoolean(exts.getValue(extensionName)); + } + return false; + } + + /** + * Returns the description. + * + * @return description + */ + public String getDescription() { + return description; + } + + /** + * Sets the description. + * + * @param s description + */ + public void setDescription(final String s) { + description = s; + } + + /** + * Returns the extensions. + * + * @return extensions + */ + public Extensions getExtensions() { + return extensions; + } + + /** + * Sets the extensions. + * + * @param e extensions + */ + public void setExtensions(final Extensions e) { + extensions = e; + } + + // CheckStyle:EqualsHashCode OFF + @Override + public boolean equals(final Object o) { + return super.equals(o); + } + // CheckStyle:EqualsHashCode ON + + + @Override + public abstract int hashCode(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/AttributeType.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AttributeType.java new file mode 100644 index 0000000..a403378 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AttributeType.java @@ -0,0 +1,625 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for an attribute type schema element. + * + *
+ * AttributeTypeDescription = LPAREN WSP
+ * numericoid                    ; object identifier
+ * [ SP "NAME" SP qdescrs ]      ; short names (descriptors)
+ * [ SP "DESC" SP qdstring ]     ; description
+ * [ SP "OBSOLETE" ]             ; not active
+ * [ SP "SUP" SP oid ]           ; supertype
+ * [ SP "EQUALITY" SP oid ]      ; equality matching rule
+ * [ SP "ORDERING" SP oid ]      ; ordering matching rule
+ * [ SP "SUBSTR" SP oid ]        ; substrings matching rule
+ * [ SP "SYNTAX" SP noidlen ]    ; value syntax
+ * [ SP "SINGLE-VALUE" ]         ; single-value
+ * [ SP "COLLECTIVE" ]           ; collective
+ * [ SP "NO-USER-MODIFICATION" ] ; not user modifiable
+ * [ SP "USAGE" SP usage ]       ; usage
+ * extensions WSP RPAREN         ; extensions
+ * 
+ * + */ +public class AttributeType extends AbstractNamedSchemaElement { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1103; + + /** + * OID. + */ + private final String oid; + + /** + * Superior type. + */ + private String superiorType; + + /** + * Equality matching rule. + */ + private String equalityMatchingRule; + + /** + * Ordering matching rule. + */ + private String orderingMatchingRule; + + /** + * Substring matching rule. + */ + private String substringMatchingRule; + + /** + * Syntax OID. + */ + private String syntaxOID; + + /** + * Single valued. + */ + private boolean singleValued; + + /** + * Collective. + */ + private boolean collective; + + /** + * No user modification. + */ + private boolean noUserModification; + + /** + * Usage. + */ + private AttributeUsage usage; + + + /** + * Creates a new attribute type. + * + * @param s oid + */ + public AttributeType(final String s) { + oid = s; + } + + + /** + * Creates a new attribute type. + * + * @param oid oid + * @param names names + * @param description description + * @param obsolete obsolete + * @param superiorType superior type + * @param equalityMatchingRule equality matching rule + * @param orderingMatchingRule ordering matching rule + * @param substringMatchingRule substring matching rule + * @param syntaxOID syntax OID + * @param singleValued single valued + * @param collective collective + * @param noUserModification no user modification + * @param usage usage + * @param extensions extensions + */ + // CheckStyle:ParameterNumber|HiddenField OFF + public AttributeType( + final String oid, + final String[] names, + final String description, + final boolean obsolete, + final String superiorType, + final String equalityMatchingRule, + final String orderingMatchingRule, + final String substringMatchingRule, + final String syntaxOID, + final boolean singleValued, + final boolean collective, + final boolean noUserModification, + final AttributeUsage usage, + final Extensions extensions) { + this(oid); + setNames(names); + setDescription(description); + setObsolete(obsolete); + setSuperiorType(superiorType); + setEqualityMatchingRule(equalityMatchingRule); + setOrderingMatchingRule(orderingMatchingRule); + setSubstringMatchingRule(substringMatchingRule); + setSyntaxOID(syntaxOID); + setSingleValued(singleValued); + setCollective(collective); + setNoUserModification(noUserModification); + setUsage(usage); + setExtensions(extensions); + } + // CheckStyle:ParameterNumber|HiddenField ON + + /** + * Parses the supplied definition string and creates an initialized attribute type. + * + * @param definition to parse + * @return attribute type + * @throws SchemaParseException if the supplied definition is invalid + */ + public static AttributeType parse(final String definition) + throws SchemaParseException { + return SchemaParser.parse(AttributeType.class, definition); + } + + /** + * Returns the oid. + * + * @return oid + */ + public String getOID() { + return oid; + } + + /** + * Returns the superior type. + * + * @return superior type + */ + public String getSuperiorType() { + return superiorType; + } + + /** + * Sets the superior type. + * + * @param s superior type + */ + public void setSuperiorType(final String s) { + superiorType = s; + } + + /** + * Returns the equality matching rule. + * + * @return equality matching rule + */ + public String getEqualityMatchingRule() { + return equalityMatchingRule; + } + + /** + * Sets the equality matching rule. + * + * @param s equality matching rule + */ + public void setEqualityMatchingRule(final String s) { + equalityMatchingRule = s; + } + + /** + * Returns the ordering matching rule. + * + * @return ordering matching rule + */ + public String getOrderingMatchingRule() { + return orderingMatchingRule; + } + + /** + * Sets the ordering matching rule. + * + * @param s ordering matching rule + */ + public void setOrderingMatchingRule(final String s) { + orderingMatchingRule = s; + } + + /** + * Returns the substring matching rule. + * + * @return substring matching rule + */ + public String getSubstringMatchingRule() { + return substringMatchingRule; + } + + /** + * Sets the substring matching rule. + * + * @param s substring matching rule + */ + public void setSubstringMatchingRule(final String s) { + substringMatchingRule = s; + } + + /** + * Returns the syntax oid. + * + * @return syntax oid + */ + public String getSyntaxOID() { + return syntaxOID; + } + + /** + * Sets the syntax oid. + * + * @param s syntax oid + */ + public void setSyntaxOID(final String s) { + syntaxOID = s; + } + + /** + * Returns the syntax oid. + * + * @param withBoundCount whether the bound count should be included + * @return syntax oid + */ + public String getSyntaxOID(final boolean withBoundCount) { + if (!withBoundCount && syntaxOID != null) { + if (syntaxOID.contains("{") && syntaxOID.endsWith("}")) { + return syntaxOID.substring(0, syntaxOID.indexOf('{')); + } + } + return syntaxOID; + } + + /** + * Returns the syntax oid bound count. + * + * @return syntax oid bound count + */ + public int getSyntaxOIDBoundCount() { + if (syntaxOID != null) { + if (syntaxOID.contains("{") && syntaxOID.endsWith("}")) { + final String count = syntaxOID.substring(syntaxOID.indexOf('{') + 1, syntaxOID.length() - 1); + return Integer.parseInt(count); + } + } + return -1; + } + + /** + * Returns whether this attribute type is single valued. + * + * @return whether this attribute type is single valued + */ + public boolean isSingleValued() { + return singleValued; + } + + /** + * Sets whether this attribute type is single valued. + * + * @param b whether this attribute type is single valued + */ + public void setSingleValued(final boolean b) { + singleValued = b; + } + + /** + * Returns whether this attribute type is collective. + * + * @return whether this attribute type is collective + */ + public boolean isCollective() { + return collective; + } + + /** + * Sets whether this attribute type is collective. + * + * @param b whether this attribute type is collective + */ + public void setCollective(final boolean b) { + collective = b; + } + + /** + * Returns whether this attribute type allows user modification. + * + * @return whether this attribute type allows user modification + */ + public boolean isNoUserModification() { + return noUserModification; + } + + /** + * Sets whether this attribute type allows user modification. + * + * @param b whether this attribute type allows user modification + */ + public void setNoUserModification(final boolean b) { + noUserModification = b; + } + + /** + * Returns the usage. + * + * @return usage + */ + public AttributeUsage getUsage() { + return usage != null ? usage : AttributeUsage.USER_APPLICATIONS; + } + + /** + * Sets the usage. + * + * @param u attribute usage + */ + public void setUsage(final AttributeUsage u) { + usage = u; + } + + @Override + public String format() { + final StringBuilder sb = new StringBuilder("( "); + sb.append(oid).append(" "); + if (getNames() != null && getNames().length > 0) { + sb.append("NAME "); + sb.append(SchemaUtils.formatDescriptors(getNames())); + } + if (getDescription() != null) { + sb.append("DESC "); + sb.append(SchemaUtils.formatDescriptors(getDescription())); + } + if (isObsolete()) { + sb.append("OBSOLETE "); + } + if (superiorType != null) { + sb.append("SUP ").append(superiorType).append(" "); + } + if (equalityMatchingRule != null) { + sb.append("EQUALITY ").append(equalityMatchingRule).append(" "); + } + if (orderingMatchingRule != null) { + sb.append("ORDERING ").append(orderingMatchingRule).append(" "); + } + if (substringMatchingRule != null) { + sb.append("SUBSTR ").append(substringMatchingRule).append(" "); + } + if (syntaxOID != null) { + sb.append("SYNTAX ").append(syntaxOID).append(" "); + } + if (singleValued) { + sb.append("SINGLE-VALUE "); + } + if (collective) { + sb.append("COLLECTIVE "); + } + if (noUserModification) { + sb.append("NO-USER-MODIFICATION "); + } + if (usage != null) { + sb.append("USAGE ").append(usage.getName()).append(" "); + } + if (getExtensions() != null) { + sb.append(getExtensions().format()); + } + sb.append(")"); + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof AttributeType) { + final AttributeType v = (AttributeType) o; + return LdapUtils.areEqual(oid, v.oid) && + LdapUtils.areEqual(getNames(), v.getNames()) && + LdapUtils.areEqual(getDescription(), v.getDescription()) && + LdapUtils.areEqual(isObsolete(), v.isObsolete()) && + LdapUtils.areEqual(superiorType, v.superiorType) && + LdapUtils.areEqual(equalityMatchingRule, v.equalityMatchingRule) && + LdapUtils.areEqual(orderingMatchingRule, v.orderingMatchingRule) && + LdapUtils.areEqual(substringMatchingRule, v.substringMatchingRule) && + LdapUtils.areEqual(syntaxOID, v.syntaxOID) && + LdapUtils.areEqual(singleValued, v.singleValued) && + LdapUtils.areEqual(collective, v.collective) && + LdapUtils.areEqual(noUserModification, v.noUserModification) && + LdapUtils.areEqual(usage, v.usage) && + LdapUtils.areEqual(getExtensions(), v.getExtensions()); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + oid, + getNames(), + getDescription(), + isObsolete(), + superiorType, + equalityMatchingRule, + orderingMatchingRule, + substringMatchingRule, + syntaxOID, + singleValued, + collective, + noUserModification, + usage, + getExtensions()); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "oid=" + oid + ", " + + "names=" + Arrays.toString(getNames()) + ", " + + "description=" + getDescription() + ", " + + "obsolete=" + isObsolete() + ", " + + "superiorType=" + superiorType + ", " + + "equalityMatchingRule=" + equalityMatchingRule + ", " + + "orderingMatchingRule=" + orderingMatchingRule + ", " + + "substringMatchingRule=" + substringMatchingRule + ", " + + "syntaxOID=" + syntaxOID + ", " + + "singleValued=" + singleValued + ", " + + "collective=" + collective + ", " + + "noUserModification=" + noUserModification + ", " + + "usage=" + usage + ", " + + "extensions=" + getExtensions() + "]"; + } + + + /** + * Parses an attribute type definition using a char buffer. + */ + public static class DefaultDefinitionFunction extends AbstractDefaultDefinitionFunction { + + + @Override + public AttributeType parse(final String definition) + throws SchemaParseException { + final CharBuffer buffer = validate(definition); + skipSpaces(buffer); + final AttributeType atd = new AttributeType(readUntilSpace(buffer)); + final Extensions exts = new Extensions(); + while (buffer.hasRemaining()) { + skipSpaces(buffer); + final String token = readUntilSpace(buffer); + skipSpaces(buffer); + switch (token) { + case "NAME": + atd.setNames(readQDStrings(buffer)); + break; + case "DESC": + atd.setDescription(readQDString(buffer)); + break; + case "OBSOLETE": + atd.setObsolete(true); + break; + case "SUP": + atd.setSuperiorType(readOID(buffer)); + break; + case "EQUALITY": + atd.setEqualityMatchingRule(readOID(buffer)); + break; + case "ORDERING": + atd.setOrderingMatchingRule(readOID(buffer)); + break; + case "SUBSTR": + atd.setSubstringMatchingRule(readOID(buffer)); + break; + case "SYNTAX": + atd.setSyntaxOID(readUntilSpace(buffer)); + break; + case "SINGLE-VALUE": + atd.setSingleValued(true); + break; + case "COLLECTIVE": + atd.setCollective(true); + break; + case "NO-USER-MODIFICATION": + atd.setNoUserModification(true); + break; + case "USAGE": + atd.setUsage(AttributeUsage.parse(readUntilSpace(buffer))); + break; + case "": + break; + default: + if (!token.startsWith("X-")) { + throw new SchemaParseException( + "Definition '" + definition + "' contains invalid extension '" + token + "'"); + } + skipSpaces(buffer); + exts.addExtension(token, List.of(readQDStrings(buffer))); + break; + } + } + if (!exts.isEmpty()) { + atd.setExtensions(exts); + } + return atd; + } + } + + + /** + * Parses an attribute type definition using a regular expression. + */ + public static class RegexDefinitionFunction extends AbstractRegexDefinitionFunction { + + /** + * Pattern to match definitions. + */ + private static final Pattern DEFINITION_PATTERN = Pattern.compile( + WSP_REGEX + "\\(" + + WSP_REGEX + "(" + NO_WSP_REGEX + ")" + + WSP_REGEX + "(?:NAME" + ONE_WSP_REGEX + "(?:'([^']+)'|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:DESC" + ONE_WSP_REGEX + "'([^']*)')?" + + WSP_REGEX + "(OBSOLETE)?" + + WSP_REGEX + "(?:SUP" + ONE_WSP_REGEX + "(" + NO_WSP_REGEX + "))?" + + WSP_REGEX + "(?:EQUALITY" + ONE_WSP_REGEX + "(" + NO_WSP_REGEX + "))?" + + WSP_REGEX + "(?:ORDERING" + ONE_WSP_REGEX + "(" + NO_WSP_REGEX + "))?" + + WSP_REGEX + "(?:SUBSTR" + ONE_WSP_REGEX + "(" + NO_WSP_REGEX + "))?" + + WSP_REGEX + "(?:SYNTAX" + ONE_WSP_REGEX + "(" + NO_WSP_REGEX + "))?" + + WSP_REGEX + "(SINGLE-VALUE)?" + + WSP_REGEX + "(COLLECTIVE)?" + + WSP_REGEX + "(NO-USER-MODIFICATION)?" + + WSP_REGEX + "(?:USAGE" + ONE_WSP_REGEX + "(\\p{Alpha}+))?" + + WSP_REGEX + "(?:(X-[^ ]+.*))?" + + WSP_REGEX + "\\)" + WSP_REGEX); + + + @Override + public AttributeType parse(final String definition) + throws SchemaParseException { + final Matcher m = DEFINITION_PATTERN.matcher(definition); + if (!m.matches()) { + throw new SchemaParseException("Invalid attribute type definition: " + definition); + } + + final AttributeType atd = new AttributeType(m.group(1).trim()); + + // CheckStyle:MagicNumber OFF + // parse names + if (m.group(2) != null) { + atd.setNames(SchemaUtils.parseDescriptors(m.group(2).trim())); + } else if (m.group(3) != null) { + atd.setNames(SchemaUtils.parseDescriptors(m.group(3).trim())); + } + + atd.setDescription(m.group(4) != null ? m.group(4).trim() : null); + atd.setObsolete(m.group(5) != null); + atd.setSuperiorType(m.group(6) != null ? m.group(6).trim() : null); + atd.setEqualityMatchingRule(m.group(7) != null ? m.group(7).trim() : null); + atd.setOrderingMatchingRule(m.group(8) != null ? m.group(8).trim() : null); + atd.setSubstringMatchingRule(m.group(9) != null ? m.group(9).trim() : null); + atd.setSyntaxOID(m.group(10) != null ? m.group(10).trim() : null); + atd.setSingleValued(m.group(11) != null); + atd.setCollective(m.group(12) != null); + atd.setNoUserModification(m.group(13) != null); + if (m.group(14) != null) { + atd.setUsage(AttributeUsage.parse(m.group(14).trim())); + } + + // parse extensions + if (m.group(15) != null) { + atd.setExtensions(parseExtensions(m.group(15).trim())); + } + return atd; + // CheckStyle:MagicNumber ON + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/AttributeUsage.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AttributeUsage.java new file mode 100644 index 0000000..7679c0f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/AttributeUsage.java @@ -0,0 +1,92 @@ + +package org.xbib.net.ldap.schema; + +/** + * Enum for an attribute usage schema element. + * + *
+ * AttributeUsage =
+ * "userApplications"     /
+ * "directoryOperation"   /
+ * "distributedOperation" / ; DSA-shared
+ * "dSAOperation"           ; DSA-specific, value depends on server
+ * 
+ * + */ +public enum AttributeUsage { + + /** + * user applications. + */ + USER_APPLICATIONS("userApplications", false), + + /** + * directory operation. + */ + DIRECTORY_OPERATION("directoryOperation", true), + + /** + * distributed operation. + */ + DISTRIBUTED_OPERATION("distributedOperation", true), + + /** + * dSA operation. + */ + DSA_OPERATION("dSAOperation", true); + + /** + * Name of this attribute usage. + */ + private final String name; + + /** + * Whether this attribute usage is operational. + */ + private final boolean operational; + + + /** + * Creates a new attribute usage. + * + * @param s name of this usage + * @param b whether this usage is operational + */ + AttributeUsage(final String s, final boolean b) { + name = s; + operational = b; + } + + /** + * Returns the attribute usage for the supplied string name. + * + * @param s case-insensitive name to find attribute usage for + * @return attribute usage or null if name cannot be found + */ + public static AttributeUsage parse(final String s) { + for (AttributeUsage usage : AttributeUsage.values()) { + if (usage.getName().equalsIgnoreCase(s)) { + return usage; + } + } + return null; + } + + /** + * Returns the name. + * + * @return attribute usage name + */ + public String getName() { + return name; + } + + /** + * Whether this attribute usage is operational. + * + * @return whether this attribute usage is operational + */ + public boolean isOperational() { + return operational; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/DITContentRule.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/DITContentRule.java new file mode 100644 index 0000000..80e8324 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/DITContentRule.java @@ -0,0 +1,431 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for a DIT content rule schema element. + * + *
+ * DITContentRuleDescription = LPAREN WSP
+ * numericoid                 ; object identifier
+ * [ SP "NAME" SP qdescrs ]   ; short names (descriptors)
+ * [ SP "DESC" SP qdstring ]  ; description
+ * [ SP "OBSOLETE" ]          ; not active
+ * [ SP "AUX" SP oids ]       ; auxiliary object classes
+ * [ SP "MUST" SP oids ]      ; attribute types
+ * [ SP "MAY" SP oids ]       ; attribute types
+ * [ SP "NOT" SP oids ]       ; attribute types
+ * extensions WSP RPAREN      ; extensions
+ * 
+ * + */ +public class DITContentRule extends AbstractNamedSchemaElement { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1151; + + /** + * OID. + */ + private final String oid; + + /** + * Auxiliary classes. + */ + private String[] auxiliaryClasses; + + /** + * Required attributes. + */ + private String[] requiredAttributes; + + /** + * Optional attributes. + */ + private String[] optionalAttributes; + + /** + * Restricted attributes. + */ + private String[] restrictedAttributes; + + + /** + * Creates a new DIT content rule. + * + * @param s oid + */ + public DITContentRule(final String s) { + oid = s; + } + + + /** + * Creates a new DIT content rule. + * + * @param oid oid + * @param names names + * @param description description + * @param obsolete obsolete + * @param auxiliaryClasses auxiliary classes + * @param requiredAttributes required attributes + * @param optionalAttributes optional attributes + * @param restrictedAttributes restricted attributes + * @param extensions extensions + */ + // CheckStyle:ParameterNumber|HiddenField OFF + public DITContentRule( + final String oid, + final String[] names, + final String description, + final boolean obsolete, + final String[] auxiliaryClasses, + final String[] requiredAttributes, + final String[] optionalAttributes, + final String[] restrictedAttributes, + final Extensions extensions) { + this(oid); + setNames(names); + setDescription(description); + setObsolete(obsolete); + setAuxiliaryClasses(auxiliaryClasses); + setRequiredAttributes(requiredAttributes); + setOptionalAttributes(optionalAttributes); + setRestrictedAttributes(restrictedAttributes); + setExtensions(extensions); + } + // CheckStyle:ParameterNumber|HiddenField ON + + /** + * Parses the supplied definition string and creates an initialized DIT content rule. + * + * @param definition to parse + * @return DIT content rule + * @throws SchemaParseException if the supplied definition is invalid + */ + public static DITContentRule parse(final String definition) + throws SchemaParseException { + return SchemaParser.parse(DITContentRule.class, definition); + } + + /** + * Returns the oid. + * + * @return oid + */ + public String getOID() { + return oid; + } + + /** + * Returns the auxiliary classes. + * + * @return auxiliary classes + */ + public String[] getAuxiliaryClasses() { + return auxiliaryClasses; + } + + /** + * Sets the auxiliary classes. + * + * @param s auxiliary classes + */ + public void setAuxiliaryClasses(final String[] s) { + auxiliaryClasses = s; + } + + /** + * Returns the required attributes. + * + * @return required attributes + */ + public String[] getRequiredAttributes() { + return requiredAttributes; + } + + /** + * Sets the required attributes. + * + * @param s required attributes + */ + public void setRequiredAttributes(final String[] s) { + requiredAttributes = s; + } + + /** + * Returns the optional attributes. + * + * @return optional attributes + */ + public String[] getOptionalAttributes() { + return optionalAttributes; + } + + /** + * Sets the optional attributes. + * + * @param s optional attributes + */ + public void setOptionalAttributes(final String[] s) { + optionalAttributes = s; + } + + /** + * Returns the restricted attributes. + * + * @return restricted attributes + */ + public String[] getRestrictedAttributes() { + return restrictedAttributes; + } + + /** + * Sets the restricted attributes. + * + * @param s restricted attributes + */ + public void setRestrictedAttributes(final String[] s) { + restrictedAttributes = s; + } + + @Override + public String format() { + final StringBuilder sb = new StringBuilder("( "); + sb.append(oid).append(" "); + if (getNames() != null && getNames().length > 0) { + sb.append("NAME "); + sb.append(SchemaUtils.formatDescriptors(getNames())); + } + if (getDescription() != null) { + sb.append("DESC "); + sb.append(SchemaUtils.formatDescriptors(getDescription())); + } + if (isObsolete()) { + sb.append("OBSOLETE "); + } + if (auxiliaryClasses != null && auxiliaryClasses.length > 0) { + sb.append("AUX "); + sb.append(SchemaUtils.formatOids(auxiliaryClasses)); + } + if (requiredAttributes != null && requiredAttributes.length > 0) { + sb.append("MUST "); + sb.append(SchemaUtils.formatOids(requiredAttributes)); + } + if (optionalAttributes != null && optionalAttributes.length > 0) { + sb.append("MAY "); + sb.append(SchemaUtils.formatOids(optionalAttributes)); + } + if (restrictedAttributes != null && restrictedAttributes.length > 0) { + sb.append("NOT "); + sb.append(SchemaUtils.formatOids(restrictedAttributes)); + } + if (getExtensions() != null) { + sb.append(getExtensions().format()); + } + sb.append(")"); + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof DITContentRule) { + final DITContentRule v = (DITContentRule) o; + return LdapUtils.areEqual(oid, v.oid) && + LdapUtils.areEqual(getNames(), v.getNames()) && + LdapUtils.areEqual(getDescription(), v.getDescription()) && + LdapUtils.areEqual(isObsolete(), v.isObsolete()) && + LdapUtils.areEqual(auxiliaryClasses, v.auxiliaryClasses) && + LdapUtils.areEqual(requiredAttributes, v.requiredAttributes) && + LdapUtils.areEqual(optionalAttributes, v.optionalAttributes) && + LdapUtils.areEqual(restrictedAttributes, v.restrictedAttributes) && + LdapUtils.areEqual(getExtensions(), v.getExtensions()); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + oid, + getNames(), + getDescription(), + isObsolete(), + auxiliaryClasses, + requiredAttributes, + optionalAttributes, + restrictedAttributes, + getExtensions()); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "oid=" + oid + ", " + + "names=" + Arrays.toString(getNames()) + ", " + + "description=" + getDescription() + ", " + + "obsolete=" + isObsolete() + ", " + + "auxiliaryClasses=" + Arrays.toString(auxiliaryClasses) + ", " + + "requiredAttributes=" + Arrays.toString(requiredAttributes) + ", " + + "optionalAttributes=" + Arrays.toString(optionalAttributes) + ", " + + "restrictedAttributes=" + Arrays.toString(restrictedAttributes) + ", " + + "extensions=" + getExtensions() + "]"; + } + + + /** + * Parses a DIT content rule definition using a char buffer. + */ + public static class DefaultDefinitionFunction extends AbstractDefaultDefinitionFunction { + + + @Override + public DITContentRule parse(final String definition) + throws SchemaParseException { + final CharBuffer buffer = validate(definition); + skipSpaces(buffer); + final DITContentRule dcr = new DITContentRule(readUntilSpace(buffer)); + final Extensions exts = new Extensions(); + while (buffer.hasRemaining()) { + skipSpaces(buffer); + final String token = readUntilSpace(buffer); + skipSpaces(buffer); + switch (token) { + case "NAME": + dcr.setNames(readQDStrings(buffer)); + break; + case "DESC": + dcr.setDescription(readQDString(buffer)); + break; + case "OBSOLETE": + dcr.setObsolete(true); + break; + case "AUX": + dcr.setAuxiliaryClasses(readOIDs(buffer)); + break; + case "MUST": + dcr.setRequiredAttributes(readOIDs(buffer)); + break; + case "MAY": + dcr.setOptionalAttributes(readOIDs(buffer)); + break; + case "NOT": + dcr.setRestrictedAttributes(readOIDs(buffer)); + break; + case "": + break; + default: + if (!token.startsWith("X-")) { + throw new SchemaParseException( + "Definition '" + definition + "' contains invalid extension '" + token + "'"); + } + skipSpaces(buffer); + exts.addExtension(token, List.of(readQDStrings(buffer))); + break; + } + } + if (!exts.isEmpty()) { + dcr.setExtensions(exts); + } + return dcr; + } + } + + + /** + * Parses a DIT content rule definition using a regular expression. + */ + public static class RegexDefinitionFunction extends AbstractRegexDefinitionFunction { + + /** + * Pattern to match definitions. + */ + private static final Pattern DEFINITION_PATTERN = Pattern.compile( + WSP_REGEX + "\\(" + + WSP_REGEX + "(" + NO_WSP_REGEX + ")" + + WSP_REGEX + "(?:NAME" + ONE_WSP_REGEX + "(?:'([^']+)'|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:DESC" + ONE_WSP_REGEX + "'([^']*)')?" + + WSP_REGEX + "(OBSOLETE)?" + + WSP_REGEX + "(?:AUX" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:MUST" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:MAY" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:NOT" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:(X-[^ ]+.*))?" + + WSP_REGEX + "\\)" + WSP_REGEX); + + + @Override + public DITContentRule parse(final String definition) + throws SchemaParseException { + final Matcher m = DEFINITION_PATTERN.matcher(definition); + if (!m.matches()) { + throw new SchemaParseException("Invalid DIT content rule definition: " + definition); + } + + final DITContentRule dcrd = new DITContentRule(m.group(1).trim()); + + // CheckStyle:MagicNumber OFF + // parse names + if (m.group(2) != null) { + dcrd.setNames(SchemaUtils.parseDescriptors(m.group(2).trim())); + } else if (m.group(3) != null) { + dcrd.setNames(SchemaUtils.parseDescriptors(m.group(3).trim())); + } + + dcrd.setDescription(m.group(4) != null ? m.group(4).trim() : null); + dcrd.setObsolete(m.group(5) != null); + + // parse auxiliary classes + if (m.group(6) != null) { + dcrd.setAuxiliaryClasses(SchemaUtils.parseOIDs(m.group(6).trim())); + } else if (m.group(7) != null) { + dcrd.setAuxiliaryClasses(SchemaUtils.parseOIDs(m.group(7).trim())); + } + + // parse required attributes + if (m.group(9) != null) { + dcrd.setRequiredAttributes(SchemaUtils.parseOIDs(m.group(9).trim())); + } else if (m.group(10) != null) { + dcrd.setRequiredAttributes(SchemaUtils.parseOIDs(m.group(10).trim())); + } + + // parse optional attributes + if (m.group(11) != null) { + dcrd.setOptionalAttributes(SchemaUtils.parseOIDs(m.group(11).trim())); + } else if (m.group(12) != null) { + dcrd.setOptionalAttributes(SchemaUtils.parseOIDs(m.group(12).trim())); + } + + // parse restricted attributes + if (m.group(11) != null) { + dcrd.setRestrictedAttributes(SchemaUtils.parseOIDs(m.group(11).trim())); + } else if (m.group(12) != null) { + dcrd.setRestrictedAttributes(SchemaUtils.parseOIDs(m.group(12).trim())); + } + + // parse extensions + if (m.group(13) != null) { + dcrd.setExtensions(parseExtensions(m.group(13).trim())); + } + return dcrd; + // CheckStyle:MagicNumber ON + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/DITStructureRule.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/DITStructureRule.java new file mode 100644 index 0000000..b53f137 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/DITStructureRule.java @@ -0,0 +1,333 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for a DIT content rule schema element. + * + *
+ * DITStructureRuleDescription = LPAREN WSP
+ * ruleid                     ; rule identifier
+ * [ SP "NAME" SP qdescrs ]   ; short names (descriptors)
+ * [ SP "DESC" SP qdstring ]  ; description
+ * [ SP "OBSOLETE" ]          ; not active
+ * SP "FORM" SP oid           ; NameForm
+ * [ SP "SUP" ruleids ]       ; superior rules
+ * extensions WSP RPAREN      ; extensions
+ * 
+ * + */ +public class DITStructureRule extends AbstractNamedSchemaElement { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1153; + + /** + * ID. + */ + private final int id; + + /** + * Name form. + */ + private String nameForm; + + /** + * Superior rules. + */ + private int[] superiorRules; + + + /** + * Creates a new DIT structure rule. + * + * @param i id + */ + public DITStructureRule(final int i) { + id = i; + } + + + /** + * Creates a new DIT structure rule. + * + * @param id id + * @param names names + * @param description description + * @param obsolete obsolete + * @param nameForm name form + * @param superiorRules superior rules + * @param extensions extensions + */ + // CheckStyle:ParameterNumber|HiddenField OFF + public DITStructureRule( + final int id, + final String[] names, + final String description, + final boolean obsolete, + final String nameForm, + final int[] superiorRules, + final Extensions extensions) { + this(id); + setNames(names); + setDescription(description); + setObsolete(obsolete); + setNameForm(nameForm); + setSuperiorRules(superiorRules); + setExtensions(extensions); + } + // CheckStyle:ParameterNumber|HiddenField ON + + /** + * Parses the supplied definition string and creates an initialized DIT structure rule. + * + * @param definition to parse + * @return DIT structure rule + * @throws SchemaParseException if the supplied definition is invalid + */ + public static DITStructureRule parse(final String definition) + throws SchemaParseException { + return SchemaParser.parse(DITStructureRule.class, definition); + } + + /** + * Returns the id. + * + * @return id + */ + public int getID() { + return id; + } + + /** + * Returns the name form. + * + * @return name form + */ + public String getNameForm() { + return nameForm; + } + + /** + * Sets the name form. + * + * @param s name form + */ + public void setNameForm(final String s) { + nameForm = s; + } + + /** + * Returns the superior rules. + * + * @return superior rules + */ + public int[] getSuperiorRules() { + return superiorRules; + } + + /** + * Sets the superior rules. + * + * @param i superior rules + */ + public void setSuperiorRules(final int[] i) { + superiorRules = i; + } + + @Override + public String format() { + final StringBuilder sb = new StringBuilder("( "); + sb.append(id).append(" "); + if (getNames() != null && getNames().length > 0) { + sb.append("NAME "); + sb.append(SchemaUtils.formatDescriptors(getNames())); + } + if (getDescription() != null) { + sb.append("DESC "); + sb.append(SchemaUtils.formatDescriptors(getDescription())); + } + if (isObsolete()) { + sb.append("OBSOLETE "); + } + if (nameForm != null) { + sb.append("FORM ").append(nameForm).append(" "); + } + if (superiorRules != null && superiorRules.length > 0) { + sb.append("SUP "); + sb.append(SchemaUtils.formatNumbers(superiorRules)); + } + if (getExtensions() != null) { + sb.append(getExtensions().format()); + } + sb.append(")"); + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof DITStructureRule) { + final DITStructureRule v = (DITStructureRule) o; + return LdapUtils.areEqual(id, v.id) && + LdapUtils.areEqual(getNames(), v.getNames()) && + LdapUtils.areEqual(getDescription(), v.getDescription()) && + LdapUtils.areEqual(isObsolete(), v.isObsolete()) && + LdapUtils.areEqual(nameForm, v.nameForm) && + LdapUtils.areEqual(superiorRules, v.superiorRules) && + LdapUtils.areEqual(getExtensions(), v.getExtensions()); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + id, + getNames(), + getDescription(), + isObsolete(), + nameForm, + superiorRules, + getExtensions()); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "id=" + id + ", " + + "names=" + Arrays.toString(getNames()) + ", " + + "description=" + getDescription() + ", " + + "obsolete=" + isObsolete() + ", " + + "nameForm=" + nameForm + ", " + + "superiorRules=" + Arrays.toString(superiorRules) + ", " + + "extensions=" + getExtensions() + "]"; + } + + + /** + * Parses a DIT structure rule definition using a char buffer. + */ + public static class DefaultDefinitionFunction extends AbstractDefaultDefinitionFunction { + + + @Override + public DITStructureRule parse(final String definition) + throws SchemaParseException { + final CharBuffer buffer = validate(definition); + skipSpaces(buffer); + final DITStructureRule dsr = new DITStructureRule(readRuleID(buffer)); + final Extensions exts = new Extensions(); + while (buffer.hasRemaining()) { + skipSpaces(buffer); + final String token = readUntilSpace(buffer); + skipSpaces(buffer); + switch (token) { + case "NAME": + dsr.setNames(readQDStrings(buffer)); + break; + case "DESC": + dsr.setDescription(readQDString(buffer)); + break; + case "OBSOLETE": + dsr.setObsolete(true); + break; + case "FORM": + dsr.setNameForm(readUntilSpace(buffer)); + break; + case "SUP": + dsr.setSuperiorRules(readRuleIDs(buffer)); + break; + case "": + break; + default: + if (!token.startsWith("X-")) { + throw new SchemaParseException( + "Definition '" + definition + "' contains invalid extension '" + token + "'"); + } + skipSpaces(buffer); + exts.addExtension(token, List.of(readQDStrings(buffer))); + break; + } + } + if (!exts.isEmpty()) { + dsr.setExtensions(exts); + } + return dsr; + } + } + + + /** + * Parses a DIT structure rule definition using a regular expression. + */ + public static class RegexDefinitionFunction extends AbstractRegexDefinitionFunction { + + /** + * Pattern to match definitions. + */ + private static final Pattern DEFINITION_PATTERN = Pattern.compile( + WSP_REGEX + "\\(" + + WSP_REGEX + "(\\p{Digit}+)" + + WSP_REGEX + "(?:NAME" + ONE_WSP_REGEX + "(?:'([^']+)'|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:DESC" + ONE_WSP_REGEX + "'([^']*)')?" + + WSP_REGEX + "(OBSOLETE)?" + + WSP_REGEX + "(?:FORM" + ONE_WSP_REGEX + "(" + NO_WSP_REGEX + "))?" + + WSP_REGEX + "(?:SUP" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:(X-[^ ]+.*))?" + + WSP_REGEX + "\\)" + WSP_REGEX); + + + @Override + public DITStructureRule parse(final String definition) + throws SchemaParseException { + final Matcher m = DEFINITION_PATTERN.matcher(definition); + if (!m.matches()) { + throw new SchemaParseException("Invalid DIT structure rule definition: " + definition); + } + + final DITStructureRule dsrd = new DITStructureRule(Integer.parseInt(m.group(1).trim())); + + // CheckStyle:MagicNumber OFF + // parse names + if (m.group(2) != null) { + dsrd.setNames(SchemaUtils.parseDescriptors(m.group(2).trim())); + } else if (m.group(3) != null) { + dsrd.setNames(SchemaUtils.parseDescriptors(m.group(3).trim())); + } + + dsrd.setDescription(m.group(4) != null ? m.group(4).trim() : null); + dsrd.setObsolete(m.group(5) != null); + dsrd.setNameForm(m.group(6) != null ? m.group(6).trim() : null); + + // parse superior rules + if (m.group(7) != null) { + dsrd.setSuperiorRules(SchemaUtils.parseNumbers(m.group(7).trim())); + } else if (m.group(8) != null) { + dsrd.setSuperiorRules(SchemaUtils.parseNumbers(m.group(8).trim())); + } + + // parse extensions + if (m.group(9) != null) { + dsrd.setExtensions(parseExtensions(m.group(9).trim())); + } + return dsrd; + // CheckStyle:MagicNumber ON + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/DefinitionFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/DefinitionFunction.java new file mode 100644 index 0000000..d471224 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/DefinitionFunction.java @@ -0,0 +1,20 @@ + +package org.xbib.net.ldap.schema; + +/** + * Marker interface for a schema definition function. + * + * @param type of schema element + */ +public interface DefinitionFunction { + + + /** + * Parses the supplied string representation of a schema element. + * + * @param definition to parse + * @return parsed schema element + * @throws SchemaParseException if the supplied schema definition is invalid + */ + T parse(String definition) throws SchemaParseException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/Extensions.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/Extensions.java new file mode 100644 index 0000000..5da6b45 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/Extensions.java @@ -0,0 +1,174 @@ + +package org.xbib.net.ldap.schema; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for an extension found in a schema element. + * + */ +public class Extensions { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1171; + + /** + * Extensions. + */ + private final Map> extensions = new LinkedHashMap<>(); + + + /** + * Creates a new extensions. + */ + public Extensions() { + } + + + /** + * Creates a new extensions. + * + * @param name of a single extension + * @param values for that extension + */ + public Extensions(final String name, final List values) { + addExtension(name, values); + } + + + /** + * Returns the name. + * + * @return name + */ + public Set getNames() { + return extensions.keySet(); + } + + + /** + * Returns the values for the extension with the supplied name. + * + * @param name of the extension + * @return values + */ + public List getValues(final String name) { + return extensions.get(name); + } + + + /** + * Returns a single string value for the extension with the supplied name. See {@link #getValues(String)}. + * + * @param name of the extension + * @return single string extension value + */ + public String getValue(final String name) { + final List values = getValues(name); + if (values == null || values.isEmpty()) { + return null; + } + return values.iterator().next(); + } + + + /** + * Returns all the values in this extensions. + * + * @return map of name to values for every extension + */ + public Map> getAllValues() { + return Collections.unmodifiableMap(extensions); + } + + + /** + * Adds an extension. + * + * @param name of the extension + */ + public void addExtension(final String name) { + extensions.put(name, new ArrayList<>(0)); + } + + + /** + * Adds an extension. + * + * @param name of the extension + * @param values in the extension + */ + public void addExtension(final String name, final List values) { + extensions.put(name, values); + } + + + /** + * Returns the number of extensions in the underlying map. + * + * @return number of extensions + */ + public int size() { + return extensions.size(); + } + + + /** + * Returns whether the number of extensions is zero. + * + * @return whether the number of extensions is zero + */ + public boolean isEmpty() { + return extensions.isEmpty(); + } + + + /** + * Returns this extension as formatted string per RFC 4512. + * + * @return formatted string + */ + public String format() { + final StringBuilder sb = new StringBuilder(); + for (Map.Entry> entry : extensions.entrySet()) { + sb.append(entry.getKey()).append(" "); + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + sb.append(SchemaUtils.formatDescriptors(entry.getValue().toArray(new String[0]))); + } + } + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof Extensions) { + final Extensions v = (Extensions) o; + return LdapUtils.areEqual(extensions, v.extensions); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, extensions); + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "extensions=" + extensions + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/MatchingRule.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/MatchingRule.java new file mode 100644 index 0000000..ac0cf66 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/MatchingRule.java @@ -0,0 +1,288 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for a matching rule schema element. + * + *
+ * MatchingRuleDescription = LPAREN WSP
+ * numericoid                 ; object identifier
+ * [ SP "NAME" SP qdescrs ]   ; short names (descriptors)
+ * [ SP "DESC" SP qdstring ]  ; description
+ * [ SP "OBSOLETE" ]          ; not active
+ * SP "SYNTAX" SP numericoid  ; assertion syntax
+ * extensions WSP RPAREN      ; extensions
+ * 
+ * + */ +public class MatchingRule extends AbstractNamedSchemaElement { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1117; + + /** + * OID. + */ + private final String oid; + + /** + * Syntax OID. + */ + private String syntaxOID; + + + /** + * Creates a new matching rule. + * + * @param s oid + */ + public MatchingRule(final String s) { + oid = s; + } + + + /** + * Creates a new matching rule. + * + * @param oid oid + * @param names names + * @param description description + * @param obsolete obsolete + * @param syntaxOID syntax OID + * @param extensions extensions + */ + // CheckStyle:ParameterNumber|HiddenField OFF + public MatchingRule( + final String oid, + final String[] names, + final String description, + final boolean obsolete, + final String syntaxOID, + final Extensions extensions) { + this(oid); + setNames(names); + setDescription(description); + setObsolete(obsolete); + setSyntaxOID(syntaxOID); + setExtensions(extensions); + } + // CheckStyle:ParameterNumber|HiddenField ON + + /** + * Parses the supplied definition string and creates an initialized matching rule. + * + * @param definition to parse + * @return matching rule + * @throws SchemaParseException if the supplied definition is invalid + */ + public static MatchingRule parse(final String definition) + throws SchemaParseException { + return SchemaParser.parse(MatchingRule.class, definition); + } + + /** + * Returns the oid. + * + * @return oid + */ + public String getOID() { + return oid; + } + + /** + * Returns the syntax oid. + * + * @return syntax oid + */ + public String getSyntaxOID() { + return syntaxOID; + } + + /** + * Sets the syntax oid. + * + * @param s syntax oid + */ + public void setSyntaxOID(final String s) { + syntaxOID = s; + } + + @Override + public String format() { + final StringBuilder sb = new StringBuilder("( "); + sb.append(oid).append(" "); + if (getNames() != null && getNames().length > 0) { + sb.append("NAME "); + sb.append(SchemaUtils.formatDescriptors(getNames())); + } + if (getDescription() != null) { + sb.append("DESC "); + sb.append(SchemaUtils.formatDescriptors(getDescription())); + } + if (isObsolete()) { + sb.append("OBSOLETE "); + } + if (syntaxOID != null) { + sb.append("SYNTAX ").append(syntaxOID).append(" "); + } + if (getExtensions() != null) { + sb.append(getExtensions().format()); + } + sb.append(")"); + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof MatchingRule v) { + return LdapUtils.areEqual(oid, v.oid) && + LdapUtils.areEqual(getNames(), v.getNames()) && + LdapUtils.areEqual(getDescription(), v.getDescription()) && + LdapUtils.areEqual(isObsolete(), v.isObsolete()) && + LdapUtils.areEqual(syntaxOID, v.syntaxOID) && + LdapUtils.areEqual(getExtensions(), v.getExtensions()); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + oid, + getNames(), + getDescription(), + isObsolete(), + syntaxOID, + getExtensions()); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "oid=" + oid + ", " + + "names=" + Arrays.toString(getNames()) + ", " + + "description=" + getDescription() + ", " + + "obsolete=" + isObsolete() + ", " + + "syntaxOID=" + syntaxOID + ", " + + "extensions=" + getExtensions() + "]"; + } + + + /** + * Parses a matching rule definition using a char buffer. + */ + public static class DefaultDefinitionFunction extends AbstractDefaultDefinitionFunction { + + + @Override + public MatchingRule parse(final String definition) + throws SchemaParseException { + final CharBuffer buffer = validate(definition); + skipSpaces(buffer); + final MatchingRule mr = new MatchingRule(readUntilSpace(buffer)); + final Extensions exts = new Extensions(); + while (buffer.hasRemaining()) { + skipSpaces(buffer); + final String token = readUntilSpace(buffer); + skipSpaces(buffer); + switch (token) { + case "NAME": + mr.setNames(readQDStrings(buffer)); + break; + case "DESC": + mr.setDescription(readQDString(buffer)); + break; + case "OBSOLETE": + mr.setObsolete(true); + break; + case "SYNTAX": + mr.setSyntaxOID(readUntilSpace(buffer)); + break; + case "": + break; + default: + if (!token.startsWith("X-")) { + throw new SchemaParseException( + "Definition '" + definition + "' contains invalid extension '" + token + "'"); + } + skipSpaces(buffer); + exts.addExtension(token, List.of(readQDStrings(buffer))); + break; + } + } + if (!exts.isEmpty()) { + mr.setExtensions(exts); + } + return mr; + } + } + + + /** + * Parses a matching rule definition using a regular expression. + */ + public static class RegexDefinitionFunction extends AbstractRegexDefinitionFunction { + + /** + * Pattern to match definitions. + */ + private static final Pattern DEFINITION_PATTERN = Pattern.compile( + WSP_REGEX + "\\(" + + WSP_REGEX + "(" + NO_WSP_REGEX + ")" + + WSP_REGEX + "(?:NAME" + ONE_WSP_REGEX + "(?:'([^']+)'|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:DESC" + ONE_WSP_REGEX + "'([^']*)')?" + + WSP_REGEX + "(OBSOLETE)?" + + WSP_REGEX + "(?:SYNTAX" + ONE_WSP_REGEX + "(" + NO_WSP_REGEX + "))?" + + WSP_REGEX + "(?:(X-[^ ]+.*))?" + + WSP_REGEX + "\\)" + WSP_REGEX); + + + @Override + public MatchingRule parse(final String definition) + throws SchemaParseException { + final Matcher m = DEFINITION_PATTERN.matcher(definition); + if (!m.matches()) { + throw new SchemaParseException("Invalid matching rule definition: " + definition); + } + + final MatchingRule mrd = new MatchingRule(m.group(1).trim()); + + // CheckStyle:MagicNumber OFF + // parse names + if (m.group(2) != null) { + mrd.setNames(SchemaUtils.parseDescriptors(m.group(2).trim())); + } else if (m.group(3) != null) { + mrd.setNames(SchemaUtils.parseDescriptors(m.group(3).trim())); + } + + mrd.setDescription(m.group(4) != null ? m.group(4).trim() : null); + mrd.setObsolete(m.group(5) != null); + mrd.setSyntaxOID(m.group(6) != null ? m.group(6).trim() : null); + + // parse extensions + if (m.group(7) != null) { + mrd.setExtensions(parseExtensions(m.group(7).trim())); + } + return mrd; + // CheckStyle:MagicNumber ON + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/MatchingRuleUse.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/MatchingRuleUse.java new file mode 100644 index 0000000..52b3df8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/MatchingRuleUse.java @@ -0,0 +1,296 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for a matching rule use schema element. + * + *
+ * MatchingRuleUseDescription = LPAREN WSP
+ * numericoid                 ; object identifier
+ * [ SP "NAME" SP qdescrs ]   ; short names (descriptors)
+ * [ SP "DESC" SP qdstring ]  ; description
+ * [ SP "OBSOLETE" ]          ; not active
+ * SP "APPLIES" SP oids       ; attribute types
+ * extensions WSP RPAREN      ; extensions
+ * 
+ * + */ +public class MatchingRuleUse extends AbstractNamedSchemaElement { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1123; + + /** + * OID. + */ + private final String oid; + + /** + * Superior classes. + */ + private String[] appliesAttributeTypes; + + + /** + * Creates a new matching rule use. + * + * @param s oid + */ + public MatchingRuleUse(final String s) { + oid = s; + } + + + /** + * Creates a new matching rule use. + * + * @param oid oid + * @param names names + * @param description description + * @param obsolete obsolete + * @param appliesAttributeTypes applies attribute types + * @param extensions extensions + */ + // CheckStyle:ParameterNumber|HiddenField OFF + public MatchingRuleUse( + final String oid, + final String[] names, + final String description, + final boolean obsolete, + final String[] appliesAttributeTypes, + final Extensions extensions) { + this(oid); + setNames(names); + setDescription(description); + setObsolete(obsolete); + setAppliesAttributeTypes(appliesAttributeTypes); + setExtensions(extensions); + } + // CheckStyle:ParameterNumber|HiddenField ON + + /** + * Parses the supplied definition string and creates an initialized matching rule use. + * + * @param definition to parse + * @return matching rule use + * @throws SchemaParseException if the supplied definition is invalid + */ + public static MatchingRuleUse parse(final String definition) + throws SchemaParseException { + return SchemaParser.parse(MatchingRuleUse.class, definition); + } + + /** + * Returns the oid. + * + * @return oid + */ + public String getOID() { + return oid; + } + + /** + * Returns the applies attribute types. + * + * @return attribute types + */ + public String[] getAppliesAttributeTypes() { + return appliesAttributeTypes; + } + + /** + * Sets the applies attribute types. + * + * @param s attribute types + */ + public void setAppliesAttributeTypes(final String[] s) { + appliesAttributeTypes = s; + } + + @Override + public String format() { + final StringBuilder sb = new StringBuilder("( "); + sb.append(oid).append(" "); + if (getNames() != null && getNames().length > 0) { + sb.append("NAME "); + sb.append(SchemaUtils.formatDescriptors(getNames())); + } + if (getDescription() != null) { + sb.append("DESC "); + sb.append(SchemaUtils.formatDescriptors(getDescription())); + } + if (isObsolete()) { + sb.append("OBSOLETE "); + } + if (appliesAttributeTypes != null && appliesAttributeTypes.length > 0) { + sb.append("APPLIES "); + sb.append(SchemaUtils.formatOids(appliesAttributeTypes)); + } + if (getExtensions() != null) { + sb.append(getExtensions().format()); + } + sb.append(")"); + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof MatchingRuleUse) { + final MatchingRuleUse v = (MatchingRuleUse) o; + return LdapUtils.areEqual(oid, v.oid) && + LdapUtils.areEqual(getNames(), v.getNames()) && + LdapUtils.areEqual(getDescription(), v.getDescription()) && + LdapUtils.areEqual(isObsolete(), v.isObsolete()) && + LdapUtils.areEqual(appliesAttributeTypes, v.appliesAttributeTypes) && + LdapUtils.areEqual(getExtensions(), v.getExtensions()); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + oid, + getNames(), + getDescription(), + isObsolete(), + appliesAttributeTypes, + getExtensions()); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "oid=" + oid + ", " + + "names=" + Arrays.toString(getNames()) + ", " + + "description=" + getDescription() + ", " + + "obsolete=" + isObsolete() + ", " + + "appliesAttributeTypes=" + Arrays.toString(appliesAttributeTypes) + ", " + + "extensions=" + getExtensions() + "]"; + } + + + /** + * Parses a matching rule use definition using a char buffer. + */ + public static class DefaultDefinitionFunction extends AbstractDefaultDefinitionFunction { + + + @Override + public MatchingRuleUse parse(final String definition) + throws SchemaParseException { + final CharBuffer buffer = validate(definition); + skipSpaces(buffer); + final MatchingRuleUse mru = new MatchingRuleUse(readUntilSpace(buffer)); + final Extensions exts = new Extensions(); + while (buffer.hasRemaining()) { + skipSpaces(buffer); + final String token = readUntilSpace(buffer); + skipSpaces(buffer); + switch (token) { + case "NAME": + mru.setNames(readQDStrings(buffer)); + break; + case "DESC": + mru.setDescription(readQDString(buffer)); + break; + case "OBSOLETE": + mru.setObsolete(true); + break; + case "APPLIES": + mru.setAppliesAttributeTypes(readOIDs(buffer)); + break; + case "": + break; + default: + if (!token.startsWith("X-")) { + throw new SchemaParseException( + "Definition '" + definition + "' contains invalid extension '" + token + "'"); + } + skipSpaces(buffer); + exts.addExtension(token, List.of(readQDStrings(buffer))); + break; + } + } + if (!exts.isEmpty()) { + mru.setExtensions(exts); + } + return mru; + } + } + + + /** + * Parses a matching rule use definition using a regular expression. + */ + public static class RegexDefinitionFunction extends AbstractRegexDefinitionFunction { + + /** + * Pattern to match definitions. + */ + private static final Pattern DEFINITION_PATTERN = Pattern.compile( + WSP_REGEX + "\\(" + + WSP_REGEX + "(" + NO_WSP_REGEX + ")" + + WSP_REGEX + "(?:NAME" + ONE_WSP_REGEX + "(?:'([^']+)'|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:DESC" + ONE_WSP_REGEX + "'([^']*)')?" + + WSP_REGEX + "(OBSOLETE)?" + + WSP_REGEX + "(?:APPLIES" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:(X-[^ ]+.*))?" + + WSP_REGEX + "\\)" + WSP_REGEX); + + + @Override + public MatchingRuleUse parse(final String definition) + throws SchemaParseException { + final Matcher m = DEFINITION_PATTERN.matcher(definition); + if (!m.matches()) { + throw new SchemaParseException("Invalid matching rule use definition: " + definition); + } + + final MatchingRuleUse mrud = new MatchingRuleUse(m.group(1).trim()); + + // CheckStyle:MagicNumber OFF + // parse names + if (m.group(2) != null) { + mrud.setNames(SchemaUtils.parseDescriptors(m.group(2).trim())); + } else if (m.group(3) != null) { + mrud.setNames(SchemaUtils.parseDescriptors(m.group(3).trim())); + } + + mrud.setDescription(m.group(4) != null ? m.group(4).trim() : null); + mrud.setObsolete(m.group(5) != null); + + // parse applies attribute types + if (m.group(6) != null) { + mrud.setAppliesAttributeTypes(SchemaUtils.parseOIDs(m.group(6).trim())); + } else if (m.group(7) != null) { + mrud.setAppliesAttributeTypes(SchemaUtils.parseOIDs(m.group(7).trim())); + } + + // parse extensions + if (m.group(8) != null) { + mrud.setExtensions(parseExtensions(m.group(8).trim())); + } + return mrud; + // CheckStyle:MagicNumber ON + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/NameForm.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/NameForm.java new file mode 100644 index 0000000..02e288a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/NameForm.java @@ -0,0 +1,379 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for a name form schema element. + * + *
+ * NameFormDescription = LPAREN WSP
+ * numericoid                 ; object identifier
+ * [ SP "NAME" SP qdescrs ]   ; short names (descriptors)
+ * [ SP "DESC" SP qdstring ]  ; description
+ * [ SP "OBSOLETE" ]          ; not active
+ * SP "OC" SP oid             ; structural object class
+ * SP "MUST" SP oids          ; attribute types
+ * [ SP "MAY" SP oids ]       ; attribute types
+ * extensions WSP RPAREN      ; extensions
+ * 
+ * + */ +public class NameForm extends AbstractNamedSchemaElement { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1163; + + /** + * OID. + */ + private final String oid; + + /** + * Structural object class. + */ + private String structuralClass; + + /** + * Required attributes. + */ + private String[] requiredAttributes; + + /** + * Optional attributes. + */ + private String[] optionalAttributes; + + + /** + * Creates a new name form. + * + * @param s oid + */ + public NameForm(final String s) { + oid = s; + } + + + /** + * Creates a new name form. + * + * @param oid oid + * @param names names + * @param description description + * @param obsolete obsolete + * @param structuralClass structural object class + * @param requiredAttributes required attributes + * @param optionalAttributes optional attributes + * @param extensions extensions + */ + // CheckStyle:ParameterNumber|HiddenField OFF + public NameForm( + final String oid, + final String[] names, + final String description, + final boolean obsolete, + final String structuralClass, + final String[] requiredAttributes, + final String[] optionalAttributes, + final Extensions extensions) { + this(oid); + setNames(names); + setDescription(description); + setObsolete(obsolete); + setStructuralClass(structuralClass); + setRequiredAttributes(requiredAttributes); + setOptionalAttributes(optionalAttributes); + setExtensions(extensions); + } + // CheckStyle:ParameterNumber|HiddenField ON + + /** + * Parses the supplied definition string and creates an initialized name form. + * + * @param definition to parse + * @return name form + * @throws SchemaParseException if the supplied definition is invalid + */ + public static NameForm parse(final String definition) + throws SchemaParseException { + return SchemaParser.parse(NameForm.class, definition); + } + + /** + * Returns the oid. + * + * @return oid + */ + public String getOID() { + return oid; + } + + /** + * Returns the structural object class. + * + * @return structural object class + */ + public String getStructuralClass() { + return structuralClass; + } + + /** + * Sets the structural object class. + * + * @param s structural object class + */ + public void setStructuralClass(final String s) { + structuralClass = s; + } + + /** + * Returns the required attributes. + * + * @return required attributes + */ + public String[] getRequiredAttributes() { + return requiredAttributes; + } + + /** + * Sets the required attributes. + * + * @param s required attributes + */ + public void setRequiredAttributes(final String[] s) { + requiredAttributes = s; + } + + /** + * Returns the optional attributes. + * + * @return optional attributes + */ + public String[] getOptionalAttributes() { + return optionalAttributes; + } + + /** + * Sets the optional attributes. + * + * @param s optional attributes + */ + public void setOptionalAttributes(final String[] s) { + optionalAttributes = s; + } + + @Override + public String format() { + final StringBuilder sb = new StringBuilder("( "); + sb.append(oid).append(" "); + if (getNames() != null && getNames().length > 0) { + sb.append("NAME "); + sb.append(SchemaUtils.formatDescriptors(getNames())); + } + if (getDescription() != null) { + sb.append("DESC "); + sb.append(SchemaUtils.formatDescriptors(getDescription())); + } + if (isObsolete()) { + sb.append("OBSOLETE "); + } + if (structuralClass != null) { + sb.append("OC ").append(structuralClass).append(" "); + } + if (requiredAttributes != null && requiredAttributes.length > 0) { + sb.append("MUST "); + sb.append(SchemaUtils.formatOids(requiredAttributes)); + } + if (optionalAttributes != null && optionalAttributes.length > 0) { + sb.append("MAY "); + sb.append(SchemaUtils.formatOids(optionalAttributes)); + } + if (getExtensions() != null) { + sb.append(getExtensions().format()); + } + sb.append(")"); + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof NameForm) { + final NameForm v = (NameForm) o; + return LdapUtils.areEqual(oid, v.oid) && + LdapUtils.areEqual(getNames(), v.getNames()) && + LdapUtils.areEqual(getDescription(), v.getDescription()) && + LdapUtils.areEqual(isObsolete(), v.isObsolete()) && + LdapUtils.areEqual(structuralClass, v.structuralClass) && + LdapUtils.areEqual(requiredAttributes, v.requiredAttributes) && + LdapUtils.areEqual(optionalAttributes, v.optionalAttributes) && + LdapUtils.areEqual(getExtensions(), v.getExtensions()); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + oid, + getNames(), + getDescription(), + isObsolete(), + structuralClass, + requiredAttributes, + optionalAttributes, + getExtensions()); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "oid=" + oid + ", " + + "names=" + Arrays.toString(getNames()) + ", " + + "description=" + getDescription() + ", " + + "obsolete=" + isObsolete() + ", " + + "structuralClass=" + structuralClass + ", " + + "requiredAttributes=" + Arrays.toString(requiredAttributes) + ", " + + "optionalAttributes=" + Arrays.toString(optionalAttributes) + ", " + + "extensions=" + getExtensions() + "]"; + } + + + /** + * Parses a name form definition using a char buffer. + */ + public static class DefaultDefinitionFunction extends AbstractDefaultDefinitionFunction { + + + @Override + public NameForm parse(final String definition) + throws SchemaParseException { + final CharBuffer buffer = validate(definition); + skipSpaces(buffer); + final NameForm nf = new NameForm(readUntilSpace(buffer)); + final Extensions exts = new Extensions(); + while (buffer.hasRemaining()) { + skipSpaces(buffer); + final String token = readUntilSpace(buffer); + skipSpaces(buffer); + switch (token) { + case "NAME": + nf.setNames(readQDStrings(buffer)); + break; + case "DESC": + nf.setDescription(readQDString(buffer)); + break; + case "OBSOLETE": + nf.setObsolete(true); + break; + case "OC": + nf.setStructuralClass(readOID(buffer)); + break; + case "MUST": + nf.setRequiredAttributes(readOIDs(buffer)); + break; + case "MAY": + nf.setOptionalAttributes(readOIDs(buffer)); + break; + case "": + break; + default: + if (!token.startsWith("X-")) { + throw new SchemaParseException( + "Definition '" + definition + "' contains invalid extension '" + token + "'"); + } + skipSpaces(buffer); + exts.addExtension(token, List.of(readQDStrings(buffer))); + break; + } + } + if (!exts.isEmpty()) { + nf.setExtensions(exts); + } + return nf; + } + } + + + /** + * Parses a name form definition using a regular expression. + */ + public static class RegexDefinitionFunction extends AbstractRegexDefinitionFunction { + + /** + * Pattern to match definitions. + */ + private static final Pattern DEFINITION_PATTERN = Pattern.compile( + WSP_REGEX + "\\(" + + WSP_REGEX + "(" + NO_WSP_REGEX + ")" + + WSP_REGEX + "(?:NAME" + ONE_WSP_REGEX + "(?:'([^']+)'|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:DESC" + ONE_WSP_REGEX + "'([^']*)')?" + + WSP_REGEX + "(OBSOLETE)?" + + WSP_REGEX + "(?:OC" + ONE_WSP_REGEX + "(" + NO_WSP_REGEX + "))?" + + WSP_REGEX + "(?:MUST" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:MAY" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:(X-[^ ]+.*))?" + + WSP_REGEX + "\\)" + WSP_REGEX); + + + @Override + public NameForm parse(final String definition) + throws SchemaParseException { + final Matcher m = DEFINITION_PATTERN.matcher(definition); + if (!m.matches()) { + throw new SchemaParseException("Invalid name form definition: " + definition); + } + + final NameForm nfd = new NameForm(m.group(1).trim()); + + // CheckStyle:MagicNumber OFF + // parse names + if (m.group(2) != null) { + nfd.setNames(SchemaUtils.parseDescriptors(m.group(2).trim())); + } else if (m.group(3) != null) { + nfd.setNames(SchemaUtils.parseDescriptors(m.group(3).trim())); + } + + nfd.setDescription(m.group(4) != null ? m.group(4).trim() : null); + nfd.setObsolete(m.group(5) != null); + nfd.setStructuralClass(m.group(6) != null ? m.group(6).trim() : null); + + // parse required attributes + if (m.group(7) != null) { + nfd.setRequiredAttributes(SchemaUtils.parseOIDs(m.group(7).trim())); + } else if (m.group(8) != null) { + nfd.setRequiredAttributes(SchemaUtils.parseOIDs(m.group(8).trim())); + } + + // parse optional attributes + if (m.group(9) != null) { + nfd.setOptionalAttributes(SchemaUtils.parseOIDs(m.group(9).trim())); + } else if (m.group(10) != null) { + nfd.setOptionalAttributes(SchemaUtils.parseOIDs(m.group(10).trim())); + } + + // parse extensions + if (m.group(11) != null) { + nfd.setExtensions(parseExtensions(m.group(11).trim())); + } + return nfd; + // CheckStyle:MagicNumber ON + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/ObjectClass.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/ObjectClass.java new file mode 100644 index 0000000..1b69d45 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/ObjectClass.java @@ -0,0 +1,433 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for an object class schema element. + * + *
+ * ObjectClassDescription = LPAREN WSP
+ * numericoid                 ; object identifier
+ * [ SP "NAME" SP qdescrs ]   ; short names (descriptors)
+ * [ SP "DESC" SP qdstring ]  ; description
+ * [ SP "OBSOLETE" ]          ; not active
+ * [ SP "SUP" SP oids ]       ; superior object classes
+ * [ SP kind ]                ; kind of class
+ * [ SP "MUST" SP oids ]      ; attribute types
+ * [ SP "MAY" SP oids ]       ; attribute types
+ * extensions WSP RPAREN
+ * 
+ * + */ +public class ObjectClass extends AbstractNamedSchemaElement { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1109; + + /** + * OID. + */ + private final String oid; + + /** + * Superior classes. + */ + private String[] superiorClasses; + + /** + * Object class type. + */ + private ObjectClassType objectClassType; + + /** + * Required attributes. + */ + private String[] requiredAttributes; + + /** + * Optional attributes. + */ + private String[] optionalAttributes; + + + /** + * Creates a new object class. + * + * @param s oid + */ + public ObjectClass(final String s) { + oid = s; + } + + + /** + * Creates a new object class. + * + * @param oid oid + * @param names names + * @param description description + * @param obsolete obsolete + * @param superiorClasses superior classes + * @param objectClassType object class type + * @param requiredAttributes required attributes + * @param optionalAttributes optional attributes + * @param extensions extensions + */ + // CheckStyle:ParameterNumber|HiddenField OFF + public ObjectClass( + final String oid, + final String[] names, + final String description, + final boolean obsolete, + final String[] superiorClasses, + final ObjectClassType objectClassType, + final String[] requiredAttributes, + final String[] optionalAttributes, + final Extensions extensions) { + this(oid); + setNames(names); + setDescription(description); + setObsolete(obsolete); + setSuperiorClasses(superiorClasses); + setObjectClassType(objectClassType); + setRequiredAttributes(requiredAttributes); + setOptionalAttributes(optionalAttributes); + setExtensions(extensions); + } + // CheckStyle:ParameterNumber|HiddenField ON + + /** + * Parses the supplied definition string and creates an initialized object class. + * + * @param definition to parse + * @return object class + * @throws SchemaParseException if the supplied definition is invalid + */ + public static ObjectClass parse(final String definition) + throws SchemaParseException { + return SchemaParser.parse(ObjectClass.class, definition); + } + + /** + * Returns the oid. + * + * @return oid + */ + public String getOID() { + return oid; + } + + /** + * Returns the superior classes. + * + * @return superior classes + */ + public String[] getSuperiorClasses() { + return superiorClasses; + } + + /** + * Sets the superior classes. + * + * @param s superior classes + */ + public void setSuperiorClasses(final String[] s) { + superiorClasses = s; + } + + /** + * Returns the object class type. + * + * @return object class type + */ + public ObjectClassType getObjectClassType() { + return objectClassType; + } + + /** + * Sets the object class type. + * + * @param type object class type + */ + public void setObjectClassType(final ObjectClassType type) { + objectClassType = type; + } + + /** + * Returns the required attributes. + * + * @return required attributes + */ + public String[] getRequiredAttributes() { + return requiredAttributes; + } + + /** + * Sets the required attributes. + * + * @param s required attributes + */ + public void setRequiredAttributes(final String[] s) { + requiredAttributes = s; + } + + /** + * Returns the optional attributes. + * + * @return optional attributes + */ + public String[] getOptionalAttributes() { + return optionalAttributes; + } + + /** + * Sets the optional attributes. + * + * @param s optional attributes + */ + public void setOptionalAttributes(final String[] s) { + optionalAttributes = s; + } + + @Override + public String format() { + final StringBuilder sb = new StringBuilder("( "); + sb.append(oid).append(" "); + if (getNames() != null && getNames().length > 0) { + sb.append("NAME "); + sb.append(SchemaUtils.formatDescriptors(getNames())); + } + if (getDescription() != null) { + sb.append("DESC "); + sb.append(SchemaUtils.formatDescriptors(getDescription())); + } + if (isObsolete()) { + sb.append("OBSOLETE "); + } + if (superiorClasses != null && superiorClasses.length > 0) { + sb.append("SUP "); + sb.append(SchemaUtils.formatOids(superiorClasses)); + } + if (objectClassType != null) { + sb.append(objectClassType.name()).append(" "); + } + if (requiredAttributes != null && requiredAttributes.length > 0) { + sb.append("MUST "); + sb.append(SchemaUtils.formatOids(requiredAttributes)); + } + if (optionalAttributes != null && optionalAttributes.length > 0) { + sb.append("MAY "); + sb.append(SchemaUtils.formatOids(optionalAttributes)); + } + if (getExtensions() != null) { + sb.append(getExtensions().format()); + } + sb.append(")"); + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof ObjectClass) { + final ObjectClass v = (ObjectClass) o; + return LdapUtils.areEqual(oid, v.oid) && + LdapUtils.areEqual(getNames(), v.getNames()) && + LdapUtils.areEqual(getDescription(), v.getDescription()) && + LdapUtils.areEqual(isObsolete(), v.isObsolete()) && + LdapUtils.areEqual(superiorClasses, v.superiorClasses) && + LdapUtils.areEqual(objectClassType, v.objectClassType) && + LdapUtils.areEqual(requiredAttributes, v.requiredAttributes) && + LdapUtils.areEqual(optionalAttributes, v.optionalAttributes) && + LdapUtils.areEqual(getExtensions(), v.getExtensions()); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + oid, + getNames(), + getDescription(), + isObsolete(), + superiorClasses, + objectClassType, + requiredAttributes, + optionalAttributes, + getExtensions()); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "oid=" + oid + ", " + + "names=" + Arrays.toString(getNames()) + ", " + + "description=" + getDescription() + ", " + + "obsolete=" + isObsolete() + ", " + + "superiorClasses=" + Arrays.toString(superiorClasses) + ", " + + "objectClassType=" + objectClassType + ", " + + "requiredAttributes=" + Arrays.toString(requiredAttributes) + ", " + + "optionalAttributes=" + Arrays.toString(optionalAttributes) + ", " + + "extensions=" + getExtensions() + "]"; + } + + + /** + * Parses an object class definition using a char buffer. + */ + public static class DefaultDefinitionFunction extends AbstractDefaultDefinitionFunction { + + + @Override + public ObjectClass parse(final String definition) + throws SchemaParseException { + final CharBuffer buffer = validate(definition); + skipSpaces(buffer); + final ObjectClass oc = new ObjectClass(readUntilSpace(buffer)); + final Extensions exts = new Extensions(); + while (buffer.hasRemaining()) { + skipSpaces(buffer); + final String token = readUntilSpace(buffer); + skipSpaces(buffer); + switch (token) { + case "NAME": + oc.setNames(readQDStrings(buffer)); + break; + case "DESC": + oc.setDescription(readQDString(buffer)); + break; + case "OBSOLETE": + oc.setObsolete(true); + break; + case "SUP": + oc.setSuperiorClasses(readOIDs(buffer)); + break; + case "ABSTRACT": + oc.setObjectClassType(ObjectClassType.ABSTRACT); + break; + case "STRUCTURAL": + oc.setObjectClassType(ObjectClassType.STRUCTURAL); + break; + case "AUXILIARY": + oc.setObjectClassType(ObjectClassType.AUXILIARY); + break; + case "MUST": + oc.setRequiredAttributes(readOIDs(buffer)); + break; + case "MAY": + oc.setOptionalAttributes(readOIDs(buffer)); + break; + case "": + break; + default: + if (!token.startsWith("X-")) { + throw new SchemaParseException( + "Definition '" + definition + "' contains invalid extension '" + token + "'"); + } + skipSpaces(buffer); + exts.addExtension(token, List.of(readQDStrings(buffer))); + break; + } + } + if (!exts.isEmpty()) { + oc.setExtensions(exts); + } + return oc; + } + } + + + /** + * Parses an object class definition using a regular expression. + */ + public static class RegexDefinitionFunction extends AbstractRegexDefinitionFunction { + + /** + * Pattern to match definitions. + */ + private static final Pattern DEFINITION_PATTERN = Pattern.compile( + WSP_REGEX + "\\(" + + WSP_REGEX + "(" + NO_WSP_REGEX + ")" + + WSP_REGEX + "(?:NAME" + ONE_WSP_REGEX + "(?:'([^']+)'|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:DESC" + ONE_WSP_REGEX + "'([^']*)')?" + + WSP_REGEX + "(OBSOLETE)?" + + WSP_REGEX + "(?:SUP" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(\\p{Alpha}+)?" + + WSP_REGEX + "(?:MUST" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:MAY" + ONE_WSP_REGEX + "(?:(" + NO_WSP_REGEX + ")|\\(([^\\)]+)\\)))?" + + WSP_REGEX + "(?:(X-[^ ]+.*))?" + + WSP_REGEX + "\\)" + WSP_REGEX); + + + @Override + public ObjectClass parse(final String definition) + throws SchemaParseException { + final Matcher m = DEFINITION_PATTERN.matcher(definition); + if (!m.matches()) { + throw new SchemaParseException("Invalid object class definition: " + definition); + } + + final ObjectClass ocd = new ObjectClass(m.group(1).trim()); + + // CheckStyle:MagicNumber OFF + // parse names + if (m.group(2) != null) { + ocd.setNames(SchemaUtils.parseDescriptors(m.group(2).trim())); + } else if (m.group(3) != null) { + ocd.setNames(SchemaUtils.parseDescriptors(m.group(3).trim())); + } + + ocd.setDescription(m.group(4) != null ? m.group(4).trim() : null); + ocd.setObsolete(m.group(5) != null); + + // parse superior classes + if (m.group(6) != null) { + ocd.setSuperiorClasses(SchemaUtils.parseOIDs(m.group(6).trim())); + } else if (m.group(7) != null) { + ocd.setSuperiorClasses(SchemaUtils.parseOIDs(m.group(7).trim())); + } + + if (m.group(8) != null) { + ocd.setObjectClassType(ObjectClassType.valueOf(m.group(8).trim())); + } + + // parse required attributes + if (m.group(9) != null) { + ocd.setRequiredAttributes(SchemaUtils.parseOIDs(m.group(9).trim())); + } else if (m.group(10) != null) { + ocd.setRequiredAttributes(SchemaUtils.parseOIDs(m.group(10).trim())); + } + + // parse optional attributes + if (m.group(11) != null) { + ocd.setOptionalAttributes(SchemaUtils.parseOIDs(m.group(11).trim())); + } else if (m.group(12) != null) { + ocd.setOptionalAttributes(SchemaUtils.parseOIDs(m.group(12).trim())); + } + + // parse extensions + if (m.group(13) != null) { + ocd.setExtensions(parseExtensions(m.group(13).trim())); + } + return ocd; + // CheckStyle:MagicNumber ON + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/ObjectClassType.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/ObjectClassType.java new file mode 100644 index 0000000..b92d63a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/ObjectClassType.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema; + +/** + * Enum for an object class schema element. + * + *
+ * ObjectClassType = "ABSTRACT" / "STRUCTURAL" / "AUXILIARY"
+ * 
+ * + */ +public enum ObjectClassType { + + /** + * abstract. + */ + ABSTRACT, + + /** + * structural. + */ + STRUCTURAL, + + /** + * auxiliary. + */ + AUXILIARY +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/Schema.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/Schema.java new file mode 100644 index 0000000..960649d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/Schema.java @@ -0,0 +1,458 @@ + +package org.xbib.net.ldap.schema; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean that contains the schema definitions in RFC 4512. + * + */ +public class Schema { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1181; + + /** + * Attribute types. + */ + private Collection attributeTypes = Collections.emptySet(); + + /** + * DIT content rules. + */ + private Collection ditContentRules = Collections.emptySet(); + + /** + * DIT structure rules. + */ + private Collection ditStructureRules = Collections.emptySet(); + + /** + * Syntaxes. + */ + private Collection syntaxes = Collections.emptySet(); + + /** + * Matching rules. + */ + private Collection matchingRules = Collections.emptySet(); + + /** + * Matching rule uses. + */ + private Collection matchingRuleUses = Collections.emptySet(); + + /** + * Name forms. + */ + private Collection nameForms = Collections.emptySet(); + + /** + * Object classes. + */ + private Collection objectClasses = Collections.emptySet(); + + + /** + * Default constructor. + */ + public Schema() { + } + + + /** + * Creates a new schema. + * + * @param attributeTypes attribute types + * @param ditContentRules DIT content rules + * @param ditStructureRules DIT structure rules + * @param syntaxes syntaxes + * @param matchingRules matching rules + * @param matchingRuleUses matching rule uses + * @param nameForms name forms + * @param objectClasses object classses + */ + // CheckStyle:ParameterNumber|HiddenField OFF + public Schema( + final Collection attributeTypes, + final Collection ditContentRules, + final Collection ditStructureRules, + final Collection syntaxes, + final Collection matchingRules, + final Collection matchingRuleUses, + final Collection nameForms, + final Collection objectClasses) { + setAttributeTypes(attributeTypes); + setDitContentRules(ditContentRules); + setDitStructureRules(ditStructureRules); + setSyntaxes(syntaxes); + setMatchingRules(matchingRules); + setMatchingRuleUses(matchingRuleUses); + setNameForms(nameForms); + setObjectClasses(objectClasses); + } + // CheckStyle:ParameterNumber|HiddenField ON + + + /** + * Returns the attribute types. + * + * @return attribute types + */ + public Collection getAttributeTypes() { + return attributeTypes; + } + + /** + * Sets the attribute types. + * + * @param c attribute types + */ + public void setAttributeTypes(final Collection c) { + attributeTypes = c; + } + + /** + * Returns the attribute type with the supplied OID or name. + * + * @param name OID or name + * @return attribute type or null if name does not exist + */ + public AttributeType getAttributeType(final String name) { + for (AttributeType at : attributeTypes) { + if (at.getOID().equals(name) || at.hasName(name)) { + return at; + } + } + return null; + } + + /** + * Returns the attribute names in this schema that represent binary data. This includes attributes with a syntax OID + * of '1.3.6.1.4.1.1466.115.121.1.5' and any syntax with the 'X-NOT-HUMAN-READABLE' extension. + * + * @return binary attribute names + */ + public String[] getBinaryAttributeNames() { + final List binaryAttrs = new ArrayList<>(); + for (AttributeType type : attributeTypes) { + boolean isBinary = false; + final String syntaxOid = type.getSyntaxOID(false); + if ("1.3.6.1.4.1.1466.115.121.1.5".equals(syntaxOid)) { + isBinary = true; + } else { + final Syntax syntax = getSyntax(syntaxOid); + if (Syntax.containsBooleanExtension(syntax, "X-NOT-HUMAN-READABLE")) { + isBinary = true; + } + } + if (isBinary) { + Collections.addAll(binaryAttrs, type.getNames()); + } + } + return binaryAttrs.toArray(new String[0]); + } + + + /** + * Returns the DIT content rules. + * + * @return DIT content rules + */ + public Collection getDitContentRules() { + return ditContentRules; + } + + /** + * Sets the DIT content rules. + * + * @param c DIT content rules + */ + public void setDitContentRules(final Collection c) { + ditContentRules = c; + } + + /** + * Returns the DIT content rule with the supplied OID or name. + * + * @param name OID or name + * @return DIT content rule or null if name does not exist + */ + public DITContentRule getDITContentRule(final String name) { + for (DITContentRule rule : ditContentRules) { + if (rule.getOID().equals(name) || rule.hasName(name)) { + return rule; + } + } + return null; + } + + /** + * Returns the DIT structure rules. + * + * @return DIT structure rules + */ + public Collection getDitStructureRules() { + return ditStructureRules; + } + + /** + * Sets the DIT structure rules. + * + * @param c DIT structure rules + */ + public void setDitStructureRules(final Collection c) { + ditStructureRules = c; + } + + /** + * Returns the DIT structure rule with the supplied ID. + * + * @param id rule ID + * @return DIT structure rule or null if id does not exist + */ + public DITStructureRule getDITStructureRule(final int id) { + for (DITStructureRule rule : ditStructureRules) { + if (rule.getID() == id) { + return rule; + } + } + return null; + } + + /** + * Returns the DIT structure rule with the supplied name. + * + * @param name rule name + * @return DIT structure rule or null if name does not exist + */ + public DITStructureRule getDITStructureRule(final String name) { + for (DITStructureRule rule : ditStructureRules) { + if (rule.hasName(name)) { + return rule; + } + } + return null; + } + + /** + * Returns the syntaxes. + * + * @return syntaxes + */ + public Collection getSyntaxes() { + return syntaxes; + } + + /** + * Sets the syntaxes. + * + * @param c syntaxes + */ + public void setSyntaxes(final Collection c) { + syntaxes = c; + } + + /** + * Returns the syntax with the supplied OID. + * + * @param oid OID + * @return syntax or null if OID does not exist + */ + public Syntax getSyntax(final String oid) { + for (Syntax syntax : syntaxes) { + if (syntax.getOID().equals(oid)) { + return syntax; + } + } + return null; + } + + /** + * Returns the matching rules. + * + * @return matching rules + */ + public Collection getMatchingRules() { + return matchingRules; + } + + /** + * Sets the matching rules. + * + * @param c matching rules + */ + public void setMatchingRules(final Collection c) { + matchingRules = c; + } + + /** + * Returns the matching rule with the supplied OID or name. + * + * @param name OID or name + * @return matching rule or null if name does not exist + */ + public MatchingRule getMatchingRule(final String name) { + for (MatchingRule rule : matchingRules) { + if (rule.getOID().equals(name) || rule.hasName(name)) { + return rule; + } + } + return null; + } + + /** + * Returns the matching rule uses. + * + * @return matching rule uses + */ + public Collection getMatchingRuleUses() { + return matchingRuleUses; + } + + /** + * Sets the matching rule uses. + * + * @param c matching rule uses + */ + public void setMatchingRuleUses(final Collection c) { + matchingRuleUses = c; + } + + /** + * Returns the matching rule use with the supplied OID or name. + * + * @param name OID or name + * @return matching rule use or null if name does not exist + */ + public MatchingRuleUse getMatchingRuleUse(final String name) { + for (MatchingRuleUse rule : matchingRuleUses) { + if (rule.getOID().equals(name) || rule.hasName(name)) { + return rule; + } + } + return null; + } + + /** + * Returns the name forms. + * + * @return name forms + */ + public Collection getNameForms() { + return nameForms; + } + + /** + * Sets the name forms. + * + * @param c name forms + */ + public void setNameForms(final Collection c) { + nameForms = c; + } + + /** + * Returns the name form with the supplied OID or name. + * + * @param name OID or name + * @return name form or null if name does not exist + */ + public NameForm getNameForm(final String name) { + for (NameForm form : nameForms) { + if (form.getOID().equals(name) || form.hasName(name)) { + return form; + } + } + return null; + } + + /** + * Returns the object classes. + * + * @return object classes + */ + public Collection getObjectClasses() { + return objectClasses; + } + + /** + * Sets the object classes. + * + * @param c object classes + */ + public void setObjectClasses(final Collection c) { + objectClasses = c; + } + + /** + * Returns the object class with the supplied OID or name. + * + * @param name OID or name + * @return object class or null if name does not exist + */ + public ObjectClass getObjectClass(final String name) { + for (ObjectClass oc : objectClasses) { + if (oc.getOID().equals(name) || oc.hasName(name)) { + return oc; + } + } + return null; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof Schema) { + final Schema v = (Schema) o; + return LdapUtils.areEqual(attributeTypes, v.attributeTypes) && + LdapUtils.areEqual(ditContentRules, v.ditContentRules) && + LdapUtils.areEqual(ditStructureRules, v.ditStructureRules) && + LdapUtils.areEqual(syntaxes, v.syntaxes) && + LdapUtils.areEqual(matchingRules, v.matchingRules) && + LdapUtils.areEqual(matchingRuleUses, v.matchingRuleUses) && + LdapUtils.areEqual(nameForms, v.nameForms) && + LdapUtils.areEqual(objectClasses, v.objectClasses); + } + return false; + } + + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + attributeTypes, + ditContentRules, + ditStructureRules, + syntaxes, + matchingRules, + matchingRuleUses, + nameForms, + objectClasses); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "attributeTypes=" + attributeTypes + ", " + + "ditContentRules=" + ditContentRules + ", " + + "ditStructureRules=" + ditStructureRules + ", " + + "syntaxes=" + syntaxes + ", " + + "matchingRules=" + matchingRules + ", " + + "matchingRuleUses=" + matchingRuleUses + ", " + + "nameForms=" + nameForms + ", " + + "objectClasses=" + objectClasses + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaElement.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaElement.java new file mode 100644 index 0000000..a3f35e8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaElement.java @@ -0,0 +1,17 @@ + +package org.xbib.net.ldap.schema; + +/** + * Interface for schema elements. + * + */ +public interface SchemaElement { + + + /** + * Returns this schema element as formatted string per RFC 4512. + * + * @return formatted string + */ + String format(); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaFactory.java new file mode 100644 index 0000000..78b4ca3 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaFactory.java @@ -0,0 +1,214 @@ + +package org.xbib.net.ldap.schema; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.LdapAttribute; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ReturnAttributes; +import org.xbib.net.ldap.SearchOperation; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.SearchScope; +import org.xbib.net.ldap.io.LdifReader; +import org.xbib.net.ldap.schema.transcode.AttributeTypeValueTranscoder; +import org.xbib.net.ldap.schema.transcode.DITContentRuleValueTranscoder; +import org.xbib.net.ldap.schema.transcode.DITStructureRuleValueTranscoder; +import org.xbib.net.ldap.schema.transcode.MatchingRuleUseValueTranscoder; +import org.xbib.net.ldap.schema.transcode.MatchingRuleValueTranscoder; +import org.xbib.net.ldap.schema.transcode.NameFormValueTranscoder; +import org.xbib.net.ldap.schema.transcode.ObjectClassValueTranscoder; +import org.xbib.net.ldap.schema.transcode.SyntaxValueTranscoder; + +/** + * Factory to create {@link Schema} objects from an LDAP entry. + * + */ +public final class SchemaFactory { + + /** + * Attribute on the root DSE indicating the location of the subschema entry. + */ + private static final String SUBSCHEMA_SUBENTRY_ATTR_NAME = "subschemaSubentry"; + + /** + * Attribute types attribute name on the subschema entry. + */ + private static final String ATTRIBUTE_TYPES_ATTR_NAME = "attributeTypes"; + + /** + * DIT content rules attribute name on the subschema entry. + */ + private static final String DIT_CONTENT_RULES_ATTR_NAME = "dITContentRules"; + + /** + * DIT structure rules attribute name on the subschema entry. + */ + private static final String DIT_STRUCTURE_RULES_ATTR_NAME = "dITStructureRules"; + + /** + * LDAP syntaxes attribute name on the subschema entry. + */ + private static final String LDAP_SYNTAXES_ATTR_NAME = "ldapSyntaxes"; + + /** + * Matching rules attribute name on the subschema entry. + */ + private static final String MATCHING_RULES_ATTR_NAME = "matchingRules"; + + /** + * Matching rule use attribute name on the subschema entry. + */ + private static final String MATCHING_RULE_USE_ATTR_NAME = "matchingRuleUse"; + + /** + * Name forms attribute name on the subschema entry. + */ + private static final String NAME_FORMS_ATTR_NAME = "nameForms"; + + /** + * Object classes attribute name on the subschema entry. + */ + private static final String OBJECT_CLASS_ATTR_NAME = "objectClasses"; + + + /** + * Default constructor. + */ + private SchemaFactory() { + } + + + /** + * Creates a new schema. The input stream should contain the LDIF for the subschema entry. + * + * @param is containing the schema ldif + * @return schema created from the ldif + * @throws IOException if an error occurs reading the input stream + */ + public static Schema createSchema(final InputStream is) + throws IOException { + final LdifReader reader = new LdifReader(new InputStreamReader(is)); + return createSchema(reader.read().getEntry()); + } + + + /** + * Creates a new schema. The subschema subentry is searched for on the root DSE, followed by searching for the + * subschema entry itself. + * + * @param factory to obtain an LDAP connection from + * @return schema created from the connection factory + * @throws LdapException if the search fails + */ + public static Schema createSchema(final ConnectionFactory factory) + throws LdapException { + final LdapEntry rootDSE = getLdapEntry( + factory, + "", + "(objectClass=*)", + SUBSCHEMA_SUBENTRY_ATTR_NAME); + final String entryDn = rootDSE.getAttribute(SUBSCHEMA_SUBENTRY_ATTR_NAME).getStringValue(); + return createSchema(getLdapEntry(factory, entryDn, "(objectClass=subSchema)", ReturnAttributes.ALL.value())); + } + + + /** + * Creates a new schema. The entryDn is searched to obtain the schema. + * + * @param factory to obtain an LDAP connection from + * @param entryDn the subschema entry + * @return schema created from the connection factory + * @throws LdapException if the search fails + */ + public static Schema createSchema(final ConnectionFactory factory, final String entryDn) + throws LdapException { + return createSchema(getLdapEntry(factory, entryDn, "(objectClass=subSchema)", ReturnAttributes.ALL.value())); + } + + + /** + * Creates a new schema. The schema entry is parsed to obtain the schema. + * + * @param schemaEntry containing the schema + * @return schema created from the entry + */ + public static Schema createSchema(final LdapEntry schemaEntry) { + if (schemaEntry == null) { + throw new IllegalArgumentException("Schema entry cannot be null"); + } + + final Schema schema = new Schema(); + + final LdapAttribute atAttr = schemaEntry.getAttribute(ATTRIBUTE_TYPES_ATTR_NAME); + if (atAttr != null) { + schema.setAttributeTypes(atAttr.getValues(new AttributeTypeValueTranscoder().decoder())); + } + + final LdapAttribute dcrAttr = schemaEntry.getAttribute(DIT_CONTENT_RULES_ATTR_NAME); + if (dcrAttr != null) { + schema.setDitContentRules(dcrAttr.getValues(new DITContentRuleValueTranscoder().decoder())); + } + + final LdapAttribute dsrAttr = schemaEntry.getAttribute(DIT_STRUCTURE_RULES_ATTR_NAME); + if (dsrAttr != null) { + schema.setDitStructureRules(dsrAttr.getValues(new DITStructureRuleValueTranscoder().decoder())); + } + + final LdapAttribute sAttr = schemaEntry.getAttribute(LDAP_SYNTAXES_ATTR_NAME); + if (sAttr != null) { + schema.setSyntaxes(sAttr.getValues(new SyntaxValueTranscoder().decoder())); + } + + final LdapAttribute mrAttr = schemaEntry.getAttribute(MATCHING_RULES_ATTR_NAME); + if (mrAttr != null) { + schema.setMatchingRules(mrAttr.getValues(new MatchingRuleValueTranscoder().decoder())); + } + + final LdapAttribute mruAttr = schemaEntry.getAttribute(MATCHING_RULE_USE_ATTR_NAME); + if (mruAttr != null) { + schema.setMatchingRuleUses(mruAttr.getValues(new MatchingRuleUseValueTranscoder().decoder())); + } + + final LdapAttribute nfAttr = schemaEntry.getAttribute(NAME_FORMS_ATTR_NAME); + if (nfAttr != null) { + schema.setNameForms(nfAttr.getValues(new NameFormValueTranscoder().decoder())); + } + + final LdapAttribute ocAttr = schemaEntry.getAttribute(OBJECT_CLASS_ATTR_NAME); + if (ocAttr != null) { + schema.setObjectClasses(ocAttr.getValues(new ObjectClassValueTranscoder().decoder())); + } + + return schema; + } + + + /** + * Searches for the supplied dn and returns its ldap entry. + * + * @param factory to obtain an LDAP connection from + * @param dn to search for + * @param filter search filter + * @param retAttrs attributes to return + * @return ldap entry + * @throws LdapException if the search fails + */ + private static LdapEntry getLdapEntry( + final ConnectionFactory factory, + final String dn, + final String filter, + final String... retAttrs) + throws LdapException { + final SearchOperation search = new SearchOperation(factory); + final SearchResponse result = search.execute( + SearchRequest.builder().dn(dn).scope(SearchScope.OBJECT).filter(filter).returnAttributes(retAttrs).build()); + if (!result.isSuccess()) { + throw new LdapException("Unsuccessful search for schema: " + result); + } + return result.getEntry(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaFunction.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaFunction.java new file mode 100644 index 0000000..28a52ac --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaFunction.java @@ -0,0 +1,21 @@ + +package org.xbib.net.ldap.schema; + +/** + * Marker interface for a schema function. + * + */ +public interface SchemaFunction { + + + /** + * Parses the supplied string representation of a schema element. + * + * @param type of schema element + * @param type class type of schema element + * @param definition to parse + * @return parsed schema element + * @throws SchemaParseException if the supplied schema definition is invalid + */ + T parse(Class type, String definition) throws SchemaParseException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaParseException.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaParseException.java new file mode 100644 index 0000000..87c2a9f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaParseException.java @@ -0,0 +1,47 @@ + +package org.xbib.net.ldap.schema; + +import org.xbib.net.ldap.LdapException; + +/** + * Exception that indicates a schema element string could not be parsed. + * + */ +public class SchemaParseException extends LdapException { + + /** + * serialVersionUID. + */ + private static final long serialVersionUID = -5214120370570326233L; + + + /** + * Creates a new schema parse exception. + * + * @param msg describing this exception + */ + public SchemaParseException(final String msg) { + super(msg); + } + + + /** + * Creates a new schema parse exception. + * + * @param e underlying exception + */ + public SchemaParseException(final Throwable e) { + super(e); + } + + + /** + * Creates a new schema parse exception. + * + * @param msg describing this exception + * @param e underlying exception + */ + public SchemaParseException(final String msg, final Throwable e) { + super(msg, e); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaParser.java new file mode 100644 index 0000000..573e940 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaParser.java @@ -0,0 +1,234 @@ + +package org.xbib.net.ldap.schema; + +import java.lang.reflect.Constructor; +import org.xbib.net.ldap.LdapUtils; + +/** + * Encapsulates a {@link SchemaFunction} and exposes a convenience static method for parsing schema definitions. The + * schema function used by this class can be set using the system property {@link #SCHEMA_FUNCTION_PROPERTY}. + * + */ +public final class SchemaParser { + + /** + * Schema function system property. + */ + private static final String SCHEMA_FUNCTION_PROPERTY = "org.xbib.net.ldap.schema.function"; + /** + * Custom schema parser constructor. + */ + private static final Constructor SCHEMA_FUNCTION_CONSTRUCTOR; + /** + * Default schema function. + */ + private static final SchemaFunction SCHEMA_FUNCTION = getSchemaFunction(); + + static { + // Initialize a custom attribute type function if a system property is found + SCHEMA_FUNCTION_CONSTRUCTOR = LdapUtils.createConstructorFromProperty(SCHEMA_FUNCTION_PROPERTY); + } + + + /** + * Default constructor. + */ + private SchemaParser() { + } + + + /** + * The {@link #SCHEMA_FUNCTION_PROPERTY} property is checked and that class is loaded if provided. Otherwise, the + * {@link DefaultSchemaFunction} is returned. + * + * @return default filter function + */ + public static SchemaFunction getSchemaFunction() { + if (SCHEMA_FUNCTION_CONSTRUCTOR != null) { + try { + return (SchemaFunction) SCHEMA_FUNCTION_CONSTRUCTOR.newInstance(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + return new DefaultSchemaFunction(); + } + + + /** + * Parses the supplied string representation of a schema element. + * + * @param type of schema element + * @param type of schema element + * @param definition to parse + * @return parsed schema element + * @throws SchemaParseException if definition is invalid + */ + public static T parse(final Class type, final String definition) + throws SchemaParseException { + return SCHEMA_FUNCTION.parse(type, definition); + } + + + /** + * CharBuffer based implementation for schema functions. + */ + public static class DefaultSchemaFunction implements SchemaFunction { + + /** + * Default syntax function. + */ + private static final Syntax.DefaultDefinitionFunction SYNTAX_FUNCTION = new Syntax.DefaultDefinitionFunction(); + + /** + * Default attribute type function. + */ + private static final AttributeType.DefaultDefinitionFunction ATTRIBUTE_TYPE_FUNCTION = + new AttributeType.DefaultDefinitionFunction(); + + /** + * Default DIT structure rule function. + */ + private static final DITStructureRule.DefaultDefinitionFunction DIT_STRUCTURE_RULE_FUNCTION = + new DITStructureRule.DefaultDefinitionFunction(); + + /** + * Default matching rule use function. + */ + private static final MatchingRuleUse.DefaultDefinitionFunction MATCHING_RULE_USE_FUNCTION = + new MatchingRuleUse.DefaultDefinitionFunction(); + + /** + * Default object class function. + */ + private static final ObjectClass.DefaultDefinitionFunction OBJECT_CLASS_FUNCTION = + new ObjectClass.DefaultDefinitionFunction(); + + /** + * Default name form function. + */ + private static final NameForm.DefaultDefinitionFunction NAME_FORM_FUNCTION = + new NameForm.DefaultDefinitionFunction(); + + /** + * Default DIT content rule function. + */ + private static final DITContentRule.DefaultDefinitionFunction DIT_CONTENT_RULE_FUNCTION = + new DITContentRule.DefaultDefinitionFunction(); + + /** + * Default DIT matching rule function. + */ + private static final MatchingRule.DefaultDefinitionFunction MATCHING_RULE_FUNCTION = + new MatchingRule.DefaultDefinitionFunction(); + + + @Override + @SuppressWarnings("unchecked") + public T parse(final Class type, final String definition) + throws SchemaParseException { + final T element; + if (Syntax.class == type) { + element = (T) SYNTAX_FUNCTION.parse(definition); + } else if (AttributeType.class == type) { + element = (T) ATTRIBUTE_TYPE_FUNCTION.parse(definition); + } else if (DITStructureRule.class == type) { + element = (T) DIT_STRUCTURE_RULE_FUNCTION.parse(definition); + } else if (MatchingRuleUse.class == type) { + element = (T) MATCHING_RULE_USE_FUNCTION.parse(definition); + } else if (ObjectClass.class == type) { + element = (T) OBJECT_CLASS_FUNCTION.parse(definition); + } else if (NameForm.class == type) { + element = (T) NAME_FORM_FUNCTION.parse(definition); + } else if (DITContentRule.class == type) { + element = (T) DIT_CONTENT_RULE_FUNCTION.parse(definition); + } else if (MatchingRule.class == type) { + element = (T) MATCHING_RULE_FUNCTION.parse(definition); + } else { + throw new IllegalStateException("Unknown schema element " + type); + } + return element; + } + } + + + /** + * Regular expression based implementation for schema functions. + */ + public static class RegexSchemaFunction implements SchemaFunction { + + /** + * Regex syntax function. + */ + private static final Syntax.RegexDefinitionFunction SYNTAX_FUNCTION = new Syntax.RegexDefinitionFunction(); + + /** + * Regex attribute type function. + */ + private static final AttributeType.RegexDefinitionFunction ATTRIBUTE_TYPE_FUNCTION = + new AttributeType.RegexDefinitionFunction(); + + /** + * Regex DIT structure rule function. + */ + private static final DITStructureRule.RegexDefinitionFunction DIT_STRUCTURE_RULE_FUNCTION = + new DITStructureRule.RegexDefinitionFunction(); + + /** + * Regex matching rule use function. + */ + private static final MatchingRuleUse.RegexDefinitionFunction MATCHING_RULE_USE_FUNCTION = + new MatchingRuleUse.RegexDefinitionFunction(); + + /** + * Regex object class function. + */ + private static final ObjectClass.RegexDefinitionFunction OBJECT_CLASS_FUNCTION = + new ObjectClass.RegexDefinitionFunction(); + + /** + * Regex name form function. + */ + private static final NameForm.RegexDefinitionFunction NAME_FORM_FUNCTION = new NameForm.RegexDefinitionFunction(); + + /** + * Regex DIT content rule function. + */ + private static final DITContentRule.RegexDefinitionFunction DIT_CONTENT_RULE_FUNCTION = + new DITContentRule.RegexDefinitionFunction(); + + /** + * Regex DIT matching rule function. + */ + private static final MatchingRule.RegexDefinitionFunction MATCHING_RULE_FUNCTION = + new MatchingRule.RegexDefinitionFunction(); + + + @Override + @SuppressWarnings("unchecked") + public T parse(final Class type, final String definition) + throws SchemaParseException { + final T element; + if (Syntax.class == type) { + element = (T) SYNTAX_FUNCTION.parse(definition); + } else if (AttributeType.class == type) { + element = (T) ATTRIBUTE_TYPE_FUNCTION.parse(definition); + } else if (DITStructureRule.class == type) { + element = (T) DIT_STRUCTURE_RULE_FUNCTION.parse(definition); + } else if (MatchingRuleUse.class == type) { + element = (T) MATCHING_RULE_USE_FUNCTION.parse(definition); + } else if (ObjectClass.class == type) { + element = (T) OBJECT_CLASS_FUNCTION.parse(definition); + } else if (NameForm.class == type) { + element = (T) NAME_FORM_FUNCTION.parse(definition); + } else if (DITContentRule.class == type) { + element = (T) DIT_CONTENT_RULE_FUNCTION.parse(definition); + } else if (MatchingRule.class == type) { + element = (T) MATCHING_RULE_FUNCTION.parse(definition); + } else { + throw new IllegalStateException("Unknown schema element " + type); + } + return element; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaUtils.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaUtils.java new file mode 100644 index 0000000..30ab1e7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/SchemaUtils.java @@ -0,0 +1,143 @@ + +package org.xbib.net.ldap.schema; + +/** + * Provides utility methods for this package. + * + */ +public final class SchemaUtils { + + + /** + * Default constructor. + */ + private SchemaUtils() { + } + + + /** + * Parses the supplied descriptors string and returns its contents as a string array. If the string contains a single + * quote it is assumed to be a multivalue descriptor of the form "'value1' 'value2' 'value3'". Otherwise, it is + * treated as a single value descriptor. + * + * @param descrs string to parse + * @return array of descriptors + */ + public static String[] parseDescriptors(final String descrs) { + if (descrs.contains("'")) { + final String[] quotedDescr = descrs.split(" "); + final String[] s = new String[quotedDescr.length]; + for (int i = 0; i < s.length; i++) { + s[i] = quotedDescr[i].substring(1, quotedDescr[i].length() - 1).trim(); + } + return s; + } else { + return new String[]{descrs}; + } + } + + + /** + * Parses the supplied OID string and returns its contents as a string array. If the string contains a dollar sign it + * is assumed to be a multivalue OID of the form "value1 $ value2 $ value3". Otherwise, it is treated as a single + * value OID. + * + * @param oids string to parse + * @return array of oids + */ + public static String[] parseOIDs(final String oids) { + if (oids.contains("$")) { + final String[] s = oids.split("\\$"); + for (int i = 0; i < s.length; i++) { + s[i] = s[i].trim(); + } + return s; + } else { + return new String[]{oids}; + } + } + + + /** + * Parses the supplied number string and returns its contents as a string array. + * + * @param numbers string to parse + * @return array of numbers + */ + public static int[] parseNumbers(final String numbers) { + final String[] s = numbers.split(" "); + final int[] i = new int[s.length]; + for (int j = 0; j < i.length; j++) { + i[j] = Integer.parseInt(s[j].trim()); + } + return i; + } + + + /** + * Returns a formatted string to describe the supplied descriptors. + * + * @param descrs to format + * @return formatted string + */ + public static String formatDescriptors(final String... descrs) { + final StringBuilder sb = new StringBuilder(); + if (descrs.length == 1) { + sb.append("'").append(descrs[0].replace("'", "\\27")).append("' "); + } else { + sb.append("( "); + for (String descr : descrs) { + sb.append("'").append(descr.replace("'", "\\27")).append("' "); + } + sb.append(") "); + } + return sb.toString(); + } + + + /** + * Returns a formatted string to describe the supplied OIDs. + * + * @param oids to format + * @return formatted string + */ + public static String formatOids(final String... oids) { + final StringBuilder sb = new StringBuilder(); + if (oids.length == 1) { + sb.append(oids[0]).append(" "); + } else { + sb.append("( "); + for (int i = 0; i < oids.length; i++) { + sb.append(oids[i]); + if (i < oids.length - 1) { + sb.append(" $ "); + } else { + sb.append(" "); + } + } + sb.append(") "); + } + return sb.toString(); + } + + + /** + * Returns a formatted string to describe the supplied numbers. + * + * @param numbers to format + * @return formatted string + */ + public static String formatNumbers(final int... numbers) { + final StringBuilder sb = new StringBuilder(); + if (numbers.length == 1) { + sb.append(numbers[0]).append(" "); + } else { + sb.append("( "); + for (int number : numbers) { + sb.append(number).append(" "); + } + sb.append(") "); + } + return sb.toString(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/Syntax.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/Syntax.java new file mode 100644 index 0000000..3d812ec --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/Syntax.java @@ -0,0 +1,203 @@ + +package org.xbib.net.ldap.schema; + +import java.nio.CharBuffer; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ldap.LdapUtils; + +/** + * Bean for an attribute syntax schema element. + * + *
+ * SyntaxDescription = LPAREN WSP
+ * numericoid                 ; object identifier
+ * [ SP "DESC" SP qdstring ]  ; description
+ * extensions WSP RPAREN      ; extensions
+ * 
+ * + */ +public class Syntax extends AbstractSchemaElement { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1129; + + /** + * OID. + */ + private final String oid; + + + /** + * Creates a new attribute syntax. + * + * @param s oid + */ + public Syntax(final String s) { + oid = s; + } + + + /** + * Creates a new attribute syntax. + * + * @param oid oid + * @param description description + * @param extensions extensions + */ + // CheckStyle:HiddenField OFF + public Syntax(final String oid, final String description, final Extensions extensions) { + this(oid); + setDescription(description); + setExtensions(extensions); + } + // CheckStyle:HiddenField ON + + /** + * Parses the supplied definition string and creates an initialized attribute syntax. + * + * @param definition to parse + * @return attribute syntax + * @throws SchemaParseException if the supplied definition is invalid + */ + public static Syntax parse(final String definition) + throws SchemaParseException { + return SchemaParser.parse(Syntax.class, definition); + } + + /** + * Returns the oid. + * + * @return oid + */ + public String getOID() { + return oid; + } + + @Override + public String format() { + final StringBuilder sb = new StringBuilder("( "); + sb.append(oid).append(" "); + if (getDescription() != null) { + sb.append("DESC "); + sb.append(SchemaUtils.formatDescriptors(getDescription())); + } + if (getExtensions() != null) { + sb.append(getExtensions().format()); + } + sb.append(")"); + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof Syntax v) { + return LdapUtils.areEqual(oid, v.oid) && + LdapUtils.areEqual(getDescription(), v.getDescription()) && + LdapUtils.areEqual(getExtensions(), v.getExtensions()); + } + return false; + } + + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, oid, getDescription(), getExtensions()); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "oid=" + oid + ", " + + "description=" + getDescription() + ", " + + "extensions=" + getExtensions() + "]"; + } + + + /** + * Parses a syntax definition using a char buffer. + */ + public static class DefaultDefinitionFunction extends AbstractDefaultDefinitionFunction { + + @Override + public Syntax parse(final String definition) + throws SchemaParseException { + final CharBuffer buffer = validate(definition); + skipSpaces(buffer); + final Syntax s = new Syntax(readUntilSpace(buffer)); + final Extensions exts = new Extensions(); + while (buffer.hasRemaining()) { + skipSpaces(buffer); + final String token = readUntilSpace(buffer); + skipSpaces(buffer); + switch (token) { + case "DESC": + s.setDescription(readQDString(buffer)); + break; + case "": + break; + default: + if (!token.startsWith("X-")) { + throw new SchemaParseException( + "Definition '" + definition + "' contains invalid extension '" + token + "'"); + } + skipSpaces(buffer); + exts.addExtension(token, List.of(readQDStrings(buffer))); + break; + } + } + if (!exts.isEmpty()) { + s.setExtensions(exts); + } + return s; + } + } + + + /** + * Parses a syntax definition using a regular expression. + */ + public static class RegexDefinitionFunction extends AbstractRegexDefinitionFunction { + + /** + * Pattern to match definitions. + */ + private static final Pattern DEFINITION_PATTERN = Pattern.compile( + WSP_REGEX + "\\(" + + WSP_REGEX + "(" + NO_WSP_REGEX + ")" + + WSP_REGEX + "(?:DESC '([^']+)')?" + + WSP_REGEX + "(?:(X-[^ ]+.*))?" + + WSP_REGEX + "\\)" + WSP_REGEX); + + + @Override + public Syntax parse(final String definition) + throws SchemaParseException { + final Matcher m = DEFINITION_PATTERN.matcher(definition); + if (!m.matches()) { + throw new SchemaParseException("Invalid attribute syntax definition: " + definition); + } + + final Syntax asd = new Syntax(m.group(1).trim()); + + // CheckStyle:MagicNumber OFF + asd.setDescription(m.group(2) != null ? m.group(2).trim() : null); + + // parse extensions + if (m.group(3) != null) { + asd.setExtensions(parseExtensions(m.group(3).trim())); + } + return asd; + // CheckStyle:MagicNumber ON + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/AbstractSchemaElementValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/AbstractSchemaElementValueTranscoder.java new file mode 100644 index 0000000..1b653d1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/AbstractSchemaElementValueTranscoder.java @@ -0,0 +1,20 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.SchemaElement; +import org.xbib.net.ldap.transcode.AbstractStringValueTranscoder; + +/** + * Base class for schema element value transcoders. + * + * @param type of schema element + */ +public abstract class AbstractSchemaElementValueTranscoder + extends AbstractStringValueTranscoder { + + + @Override + public String encodeStringValue(final T value) { + return value.format(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/AttributeTypeValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/AttributeTypeValueTranscoder.java new file mode 100644 index 0000000..e50b706 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/AttributeTypeValueTranscoder.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.AttributeType; +import org.xbib.net.ldap.schema.SchemaParseException; + +/** + * Decodes and encodes an attribute type for use in an ldap attribute value. + * + */ +public class AttributeTypeValueTranscoder extends AbstractSchemaElementValueTranscoder { + + + @Override + public AttributeType decodeStringValue(final String value) { + try { + return AttributeType.parse(value); + } catch (SchemaParseException e) { + throw new IllegalArgumentException("Could not transcode attribute type", e); + } + } + + + @Override + public Class getType() { + return AttributeType.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/DITContentRuleValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/DITContentRuleValueTranscoder.java new file mode 100644 index 0000000..fed3ad5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/DITContentRuleValueTranscoder.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.DITContentRule; +import org.xbib.net.ldap.schema.SchemaParseException; + +/** + * Decodes and encodes a DIT content rule for use in an ldap attribute value. + * + */ +public class DITContentRuleValueTranscoder extends AbstractSchemaElementValueTranscoder { + + + @Override + public DITContentRule decodeStringValue(final String value) { + try { + return DITContentRule.parse(value); + } catch (SchemaParseException e) { + throw new IllegalArgumentException("Could not transcode DIT content rule", e); + } + } + + + @Override + public Class getType() { + return DITContentRule.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/DITStructureRuleValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/DITStructureRuleValueTranscoder.java new file mode 100644 index 0000000..3a0fb7d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/DITStructureRuleValueTranscoder.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.DITStructureRule; +import org.xbib.net.ldap.schema.SchemaParseException; + +/** + * Decodes and encodes a DIT structure rule for use in an ldap attribute value. + * + */ +public class DITStructureRuleValueTranscoder extends AbstractSchemaElementValueTranscoder { + + + @Override + public DITStructureRule decodeStringValue(final String value) { + try { + return DITStructureRule.parse(value); + } catch (SchemaParseException e) { + throw new IllegalArgumentException("Could not transcode DIT structure rule", e); + } + } + + + @Override + public Class getType() { + return DITStructureRule.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/MatchingRuleUseValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/MatchingRuleUseValueTranscoder.java new file mode 100644 index 0000000..4cc699c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/MatchingRuleUseValueTranscoder.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.MatchingRuleUse; +import org.xbib.net.ldap.schema.SchemaParseException; + +/** + * Decodes and encodes a matching rule use for use in an ldap attribute value. + * + */ +public class MatchingRuleUseValueTranscoder extends AbstractSchemaElementValueTranscoder { + + + @Override + public MatchingRuleUse decodeStringValue(final String value) { + try { + return MatchingRuleUse.parse(value); + } catch (SchemaParseException e) { + throw new IllegalArgumentException("Could not transcode matching rule use", e); + } + } + + + @Override + public Class getType() { + return MatchingRuleUse.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/MatchingRuleValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/MatchingRuleValueTranscoder.java new file mode 100644 index 0000000..3319147 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/MatchingRuleValueTranscoder.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.MatchingRule; +import org.xbib.net.ldap.schema.SchemaParseException; + +/** + * Decodes and encodes a matching rule for use in an ldap attribute value. + * + */ +public class MatchingRuleValueTranscoder extends AbstractSchemaElementValueTranscoder { + + + @Override + public MatchingRule decodeStringValue(final String value) { + try { + return MatchingRule.parse(value); + } catch (SchemaParseException e) { + throw new IllegalArgumentException("Could not transcode matching rule", e); + } + } + + + @Override + public Class getType() { + return MatchingRule.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/NameFormValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/NameFormValueTranscoder.java new file mode 100644 index 0000000..ea9bdbd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/NameFormValueTranscoder.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.NameForm; +import org.xbib.net.ldap.schema.SchemaParseException; + +/** + * Decodes and encodes a name form for use in an ldap attribute value. + * + */ +public class NameFormValueTranscoder extends AbstractSchemaElementValueTranscoder { + + + @Override + public NameForm decodeStringValue(final String value) { + try { + return NameForm.parse(value); + } catch (SchemaParseException e) { + throw new IllegalArgumentException("Could not transcode name form", e); + } + } + + + @Override + public Class getType() { + return NameForm.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/ObjectClassValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/ObjectClassValueTranscoder.java new file mode 100644 index 0000000..9f60bfe --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/ObjectClassValueTranscoder.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.ObjectClass; +import org.xbib.net.ldap.schema.SchemaParseException; + +/** + * Decodes and encodes an object class for use in an ldap attribute value. + * + */ +public class ObjectClassValueTranscoder extends AbstractSchemaElementValueTranscoder { + + + @Override + public ObjectClass decodeStringValue(final String value) { + try { + return ObjectClass.parse(value); + } catch (SchemaParseException e) { + throw new IllegalArgumentException("Could not transcode object class", e); + } + } + + + @Override + public Class getType() { + return ObjectClass.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/SyntaxValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/SyntaxValueTranscoder.java new file mode 100644 index 0000000..f2d0dc6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/schema/transcode/SyntaxValueTranscoder.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.schema.transcode; + +import org.xbib.net.ldap.schema.SchemaParseException; +import org.xbib.net.ldap.schema.Syntax; + +/** + * Decodes and encodes an attribute syntax for use in an ldap attribute value. + * + */ +public class SyntaxValueTranscoder extends AbstractSchemaElementValueTranscoder { + + + @Override + public Syntax decodeStringValue(final String value) { + try { + return Syntax.parse(value); + } catch (SchemaParseException e) { + throw new IllegalArgumentException("Could not transcode attribute syntax", e); + } + } + + + @Override + public Class getType() { + return Syntax.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AbstractCredentialReader.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AbstractCredentialReader.java new file mode 100644 index 0000000..6ca2b4a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AbstractCredentialReader.java @@ -0,0 +1,45 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import org.xbib.net.ldap.io.ResourceUtils; + +/** + * Base class for all credential readers. It provides support for loading files from resources on the classpath or a + * filepath. If a path is prefixed with the string "classpath:" it is interpreted as a classpath specification. If a + * path is prefixed with the string "file:" it is interpreted as a file path. Any other input throws + * IllegalArgumentException. + * + * @param Type of credential read by this instance. + */ +public abstract class AbstractCredentialReader implements CredentialReader { + + + @Override + public T read(final String path, final String... params) + throws IOException, GeneralSecurityException { + try (InputStream is = ResourceUtils.getResource(path)) { + final T credential = read(is, params); + return credential; + } + } + + + /** + * Gets a buffered input stream from the given input stream. If the given instance is already buffered, it is simply + * returned. + * + * @param is input stream from which to create buffered instance. + * @return buffered input stream. If the given instance is already buffered, it is simply returned. + */ + protected InputStream getBufferedInputStream(final InputStream is) { + if (is.markSupported()) { + return is; + } else { + return new BufferedInputStream(is); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AbstractSSLContextInitializer.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AbstractSSLContextInitializer.java new file mode 100644 index 0000000..14df261 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AbstractSSLContextInitializer.java @@ -0,0 +1,81 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.GeneralSecurityException; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides common implementation for SSL context initializer. + * + */ +public abstract class AbstractSSLContextInitializer implements SSLContextInitializer { + /** + * Trust managers. + */ + protected TrustManager[] trustManagers; + + + @Override + public TrustManager[] getTrustManagers() + throws GeneralSecurityException { + final TrustManager[] tm = createTrustManagers(); + TrustManager[] aggregate = null; + if (tm == null) { + if (trustManagers != null) { + aggregate = aggregateTrustManagers(trustManagers); + } + } else { + aggregate = aggregateTrustManagers(LdapUtils.concatArrays(tm, trustManagers)); + } + return aggregate; + } + + + @Override + public void setTrustManagers(final TrustManager... managers) { + trustManagers = managers; + } + + + /** + * Creates any trust managers specific to this context initializer. + * + * @return trust managers + * @throws GeneralSecurityException if an errors occurs while loading the TrustManagers + */ + protected abstract TrustManager[] createTrustManagers() + throws GeneralSecurityException; + + + @Override + public SSLContext initSSLContext(final String protocol) + throws GeneralSecurityException { + final KeyManager[] km = getKeyManagers(); + final TrustManager[] tm = getTrustManagers(); + final SSLContext ctx = SSLContext.getInstance(protocol); + ctx.init(km, tm, null); + return ctx; + } + + + /** + * Creates an {@link AggregateTrustManager} containing the supplied trust managers. + * + * @param managers to aggregate + * @return array containing a single aggregate trust manager + */ + protected TrustManager[] aggregateTrustManagers(final TrustManager... managers) { + X509TrustManager[] x509Managers = null; + if (managers != null) { + x509Managers = new X509TrustManager[managers.length]; + for (int i = 0; i < managers.length; i++) { + x509Managers[i] = (X509TrustManager) managers[i]; + } + } + return new TrustManager[]{new AggregateTrustManager(x509Managers)}; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AggregateTrustManager.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AggregateTrustManager.java new file mode 100644 index 0000000..3e82390 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AggregateTrustManager.java @@ -0,0 +1,198 @@ + +package org.xbib.net.ldap.ssl; + +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Trust manager that delegates to multiple trust managers. + * + */ +public class AggregateTrustManager extends X509ExtendedTrustManager { + + /** + * Trust managers to invoke. + */ + private final X509ExtendedTrustManager[] trustManagers; + /** + * Whether to require all trust managers succeed. + */ + private final Strategy trustStrategy; + + /** + * Creates a new aggregate trust manager with the ALL {@link Strategy}. + * + * @param managers to aggregate + */ + public AggregateTrustManager(final X509TrustManager... managers) { + this(Strategy.ALL, managers); + } + + + /** + * Creates a new aggregate trust manager. + * + * @param strategy for processing trust managers + * @param managers to aggregate + */ + public AggregateTrustManager(final Strategy strategy, final X509TrustManager... managers) { + if (strategy == null) { + throw new NullPointerException("Strategy cannot be null"); + } + trustStrategy = strategy; + if (managers == null || managers.length == 0) { + throw new NullPointerException("Trust managers cannot be empty or null"); + } + trustManagers = Stream.of(managers) + .map(tm -> { + if (tm instanceof X509ExtendedTrustManager) { + return (X509ExtendedTrustManager) tm; + } else { + return new X509ExtendedTrustManagerWrapper(tm, new DefaultHostnameVerifier()); + } + }) + .toArray(X509ExtendedTrustManager[]::new); + } + + /** + * Returns the trust managers that are aggregated. + * + * @return trust managers + */ + public X509TrustManager[] getTrustManagers() { + return trustManagers; + } + + /** + * Returns the trust strategy. + * + * @return trust strategy + */ + public Strategy getTrustStrategy() { + return trustStrategy; + } + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType, final Socket socket) + throws CertificateException { + trustManagerCheck(tm -> tm.checkClientTrusted(chain, authType, socket)); + } + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType, final SSLEngine engine) + throws CertificateException { + trustManagerCheck(tm -> tm.checkClientTrusted(chain, authType, engine)); + } + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + trustManagerCheck(tm -> tm.checkClientTrusted(chain, authType)); + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType, final Socket socket) + throws CertificateException { + trustManagerCheck(tm -> tm.checkServerTrusted(chain, authType, socket)); + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType, final SSLEngine engine) + throws CertificateException { + trustManagerCheck(tm -> tm.checkServerTrusted(chain, authType, engine)); + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + trustManagerCheck(tm -> tm.checkServerTrusted(chain, authType)); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + final List issuers = new ArrayList<>(); + for (X509ExtendedTrustManager tm : trustManagers) { + Collections.addAll(issuers, tm.getAcceptedIssuers()); + } + return issuers.toArray(new X509Certificate[0]); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustManagers=" + Arrays.toString(trustManagers) + ", " + + "trustStrategy=" + trustStrategy + "]"; + } + + /** + * Invoke the supplied consumer for each trust manager. + * + * @param consumer to invoke + * @throws CertificateException if trust check fails. For multiple failures the first exception is thrown + */ + private void trustManagerCheck(final TrustManagerConsumer consumer) + throws CertificateException { + CertificateException certEx = null; + for (X509ExtendedTrustManager tm : trustManagers) { + try { + consumer.checkTrusted(tm); + if (trustStrategy == Strategy.ANY) { + return; + } + } catch (CertificateException e) { + if (trustStrategy == Strategy.ALL) { + throw e; + } + if (certEx == null) { + certEx = e; + } + } + } + if (certEx != null) { + throw certEx; + } + } + + + /** + * Enum to define how trust managers should be processed. + */ + public enum Strategy { + + /** + * all trust managers must succeed. + */ + ALL, + + /** + * any trust manager must succeed. + */ + ANY + } + + + /** + * Interface for consuming a trust manager. + */ + private interface TrustManagerConsumer { + + + /** + * Invoke the trust check for the supplied trust manager. + * + * @param tm trust manager + * @throws CertificateException if trust check fails + */ + void checkTrusted(X509ExtendedTrustManager tm) throws CertificateException; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AllowAnyHostnameVerifier.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AllowAnyHostnameVerifier.java new file mode 100644 index 0000000..c100212 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AllowAnyHostnameVerifier.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * Hostname verifier that returns true for any hostname. Use with caution. + * + */ +public class AllowAnyHostnameVerifier implements HostnameVerifier, CertificateHostnameVerifier { + + + @Override + public boolean verify(final String hostname, final SSLSession session) { + return true; + } + + + @Override + public boolean verify(final String hostname, final X509Certificate cert) { + return true; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AllowAnyTrustManager.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AllowAnyTrustManager.java new file mode 100644 index 0000000..e8762af --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/AllowAnyTrustManager.java @@ -0,0 +1,57 @@ + +package org.xbib.net.ldap.ssl; + +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +/** + * Trust manager that trusts any certificate. Use with caution. + * + */ +public class AllowAnyTrustManager extends X509ExtendedTrustManager { + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + } + + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType, final Socket socket) + throws CertificateException { + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType, final Socket socket) + throws CertificateException { + } + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType, final SSLEngine engine) + throws CertificateException { + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType, final SSLEngine engine) + throws CertificateException { + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CertificateHostnameVerifier.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CertificateHostnameVerifier.java new file mode 100644 index 0000000..015b860 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CertificateHostnameVerifier.java @@ -0,0 +1,21 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.cert.X509Certificate; + +/** + * Interface for verifying a hostname matching a certificate. + * + */ +public interface CertificateHostnameVerifier { + + + /** + * Verify the supplied hostname matches the supplied certificate. + * + * @param hostname to verify + * @param cert to verify hostname against + * @return whether hostname is valid for the supplied certificate + */ + boolean verify(String hostname, X509Certificate cert); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialConfig.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialConfig.java new file mode 100644 index 0000000..eb66614 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialConfig.java @@ -0,0 +1,23 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.GeneralSecurityException; + +/** + * Provides a base interface for all credential configurations. Since credential configs are invoked via reflection by + * the PropertyInvoker their method signatures are not important. They only need to be able to create an SSL context + * initializer once their properties have been set. + * + */ +public interface CredentialConfig { + + + /** + * Creates an SSL context initializer using the configured trust and authentication material in this config. + * + * @return SSL context initializer + * @throws GeneralSecurityException if the ssl context initializer cannot be created + */ + SSLContextInitializer createSSLContextInitializer() + throws GeneralSecurityException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialConfigFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialConfigFactory.java new file mode 100644 index 0000000..93c4ba1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialConfigFactory.java @@ -0,0 +1,256 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import org.xbib.net.ldap.LdapUtils; + +/** + * Utility class for creating credential configs when the underlying credential is already available for use. + * + */ +public final class CredentialConfigFactory { + + + /** + * Default constructor. + */ + private CredentialConfigFactory() { + } + + + /** + * Creates a KeyStoreCredentialConfig from the supplied truststore. + * + * @param trustStore to create credential config from + * @return credential config + */ + public static CredentialConfig createKeyStoreCredentialConfig(final KeyStore trustStore) { + return createKeyStoreCredentialConfig(trustStore, null, null, null, null); + } + + + /** + * Creates a KeyStoreCredentialConfig from the supplied truststore. + * + * @param trustStore to create credential config from + * @param trustStoreAliases to use in the truststore + * @return credential config + */ + public static CredentialConfig createKeyStoreCredentialConfig( + final KeyStore trustStore, + final String[] trustStoreAliases) { + return createKeyStoreCredentialConfig(trustStore, trustStoreAliases, null, null, null); + } + + + /** + * Creates a KeyStoreCredentialConfig from the supplied keystore and password. + * + * @param keyStore to create credential config from + * @param keyStorePassword to unlock the keystore + * @return credential config + */ + public static CredentialConfig createKeyStoreCredentialConfig(final KeyStore keyStore, final String keyStorePassword) { + return createKeyStoreCredentialConfig(null, null, keyStore, keyStorePassword, null); + } + + + /** + * Creates a KeyStoreCredentialConfig from the supplied keystore and password. + * + * @param keyStore to create credential config from + * @param keyStorePassword to unlock the keystore + * @param keyStoreAliases to use in the keystore + * @return credential config + */ + public static CredentialConfig createKeyStoreCredentialConfig( + final KeyStore keyStore, + final String keyStorePassword, + final String[] keyStoreAliases) { + return createKeyStoreCredentialConfig(null, null, keyStore, keyStorePassword, keyStoreAliases); + } + + + /** + * Creates a KeyStoreCredentialConfig from the supplied truststore, keystore and password. + * + * @param trustStore to create credential config from + * @param keyStore to create credential config from + * @param keyStorePassword to unlock the keystore + * @return credential config + */ + public static CredentialConfig createKeyStoreCredentialConfig( + final KeyStore trustStore, + final KeyStore keyStore, + final String keyStorePassword) { + return createKeyStoreCredentialConfig(trustStore, null, keyStore, keyStorePassword, null); + } + + + /** + * Creates a KeyStoreCredentialConfig from the supplied truststore, keystore and password. + * + * @param trustStore to create credential config from + * @param trustStoreAliases to use in the truststore + * @param keyStore to create credential config from + * @param keyStorePassword to unlock the keystore + * @param keyStoreAliases to use in the keystore + * @return credential config + */ + public static CredentialConfig createKeyStoreCredentialConfig( + final KeyStore trustStore, + final String[] trustStoreAliases, + final KeyStore keyStore, + final String keyStorePassword, + final String[] keyStoreAliases) { + return + // CheckStyle:AnonInnerLength OFF + new CredentialConfig() { + @Override + public SSLContextInitializer createSSLContextInitializer() + throws GeneralSecurityException { + final KeyStoreSSLContextInitializer sslInit = new KeyStoreSSLContextInitializer(); + if (trustStore != null) { + sslInit.setTrustKeystore(trustStore); + sslInit.setTrustAliases(trustStoreAliases); + } + if (keyStore != null) { + sslInit.setAuthenticationKeystore(keyStore); + sslInit.setAuthenticationPassword(keyStorePassword != null ? keyStorePassword.toCharArray() : null); + sslInit.setAuthenticationAliases(keyStoreAliases); + } + return sslInit; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustStore=" + trustStore + ", " + + "trustStoreAliases=" + Arrays.toString(trustStoreAliases) + ", " + + "keyStore=" + keyStore + ", " + + "keyStorePassword=" + (keyStorePassword != null ? "suppressed" : null) + ", " + + "keyStoreAliases=" + Arrays.toString(keyStoreAliases) + + "]"; + } + }; + // CheckStyle:AnonInnerLength ON + } + + + /** + * Creates a X509CredentialConfig from the supplied trust certificates. + * + * @param trustCertificates to create credential config from + * @return credential config + */ + public static CredentialConfig createX509CredentialConfig(final X509Certificate[] trustCertificates) { + return createX509CredentialConfig(trustCertificates, null, null); + } + + + /** + * Creates a X509CredentialConfig from the supplied authentication certificate and private key. + * + * @param authenticationCertificate to create credential config from + * @param authenticationKey that belongs to the certificate + * @return credential config + */ + public static CredentialConfig createX509CredentialConfig( + final X509Certificate authenticationCertificate, + final PrivateKey authenticationKey) { + return createX509CredentialConfig(null, authenticationCertificate, authenticationKey); + } + + + /** + * Creates a X509CredentialConfig from the supplied trust certificates, authentication certificate and private key. + * + * @param trustCertificates to create credential config from + * @param authenticationCertificate to create credential config from + * @param authenticationKey that belongs to the certificate + * @return credential config + */ + public static CredentialConfig createX509CredentialConfig( + final X509Certificate[] trustCertificates, + final X509Certificate authenticationCertificate, + final PrivateKey authenticationKey) { + return + // CheckStyle:AnonInnerLength OFF + new CredentialConfig() { + @Override + public SSLContextInitializer createSSLContextInitializer() + throws GeneralSecurityException { + final X509SSLContextInitializer sslInit = new X509SSLContextInitializer(); + if (trustCertificates != null) { + sslInit.setTrustCertificates(trustCertificates); + } + if (authenticationCertificate != null) { + sslInit.setAuthenticationCertificate(authenticationCertificate); + } + if (authenticationKey != null) { + sslInit.setAuthenticationKey(authenticationKey); + } + return sslInit; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustCertificates=" + + (trustCertificates != null ? "suppressed" : null) + ", " + + "authenticationCertificate=" + + (authenticationCertificate != null ? "suppressed" : null) + ", " + + "authenticationKey=" + + (authenticationKey != null ? "suppressed" : null) + + "]"; + } + }; + // CheckStyle:AnonInnerLength ON + } + + + /** + * Creates a X509CredentialConfig from PEM encoded certificate(s). + * + * @param trustCertificates to create credential config from + * @return credential config + */ + public static CredentialConfig createX509CredentialConfig(final String trustCertificates) { + return + new CredentialConfig() { + @Override + public SSLContextInitializer createSSLContextInitializer() + throws GeneralSecurityException { + final X509SSLContextInitializer sslInit = new X509SSLContextInitializer(); + try { + if (trustCertificates != null) { + final X509CertificatesCredentialReader certsReader = new X509CertificatesCredentialReader(); + final InputStream trustCertStream = new ByteArrayInputStream(LdapUtils.utf8Encode(trustCertificates)); + sslInit.setTrustCertificates(certsReader.read(trustCertStream)); + trustCertStream.close(); + } + } catch (IOException e) { + throw new GeneralSecurityException(e); + } + return sslInit; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustCertificates=" + (trustCertificates != null ? "suppressed" : null) + + "]"; + } + }; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialReader.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialReader.java new file mode 100644 index 0000000..9adbf54 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/CredentialReader.java @@ -0,0 +1,40 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; + +/** + * Reads a credential from an IO source. + * + * @param Type of credential read by this instance. + */ +public interface CredentialReader { + + + /** + * Reads a credential object from a path. + * + * @param path from which to read credential. + * @param params Arbitrary string parameters, e.g. password, needed to read the credential. + * @return credential read from data at path. + * @throws IOException On IO errors. + * @throws GeneralSecurityException On errors with the credential data. + */ + T read(String path, String... params) + throws IOException, GeneralSecurityException; + + + /** + * Reads a credential object from an input stream. + * + * @param is input stream from which to read credential. + * @param params Arbitrary string parameters, e.g. password, needed to read the credential. + * @return credential read from data in stream. + * @throws IOException On IO errors. + * @throws GeneralSecurityException On errors with the credential data. + */ + T read(InputStream is, String... params) + throws IOException, GeneralSecurityException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultHostnameVerifier.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultHostnameVerifier.java new file mode 100644 index 0000000..3911fd5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultHostnameVerifier.java @@ -0,0 +1,244 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.asn1.DefaultDERBuffer; +import org.xbib.net.ldap.dn.Dn; +import org.xbib.net.ldap.dn.NameValue; +import org.xbib.net.ldap.dn.RDn; + +/** + * Hostname verifier that provides an implementation similar to what occurs with JNDI startTLS. Verification occurs in + * the following order: + * + *
    + *
  • if hostname is IP, then cert must have exact match IP subjAltName
  • + *
  • hostname must match any DNS subjAltName if any exist
  • + *
  • hostname must match the first CN
  • + *
  • if cert begins with a wildcard, domains are used for matching
  • + *
+ * + */ +public class DefaultHostnameVerifier implements HostnameVerifier, CertificateHostnameVerifier { + + /** + * Hostname verifier delegate. + */ + private final HostnameVerifier verifier = new HostnameVerifierAdapter(this); + + @Override + public boolean verify(final String hostname, final SSLSession session) { + return verifier.verify(hostname, session); + } + + /** + * Verify if the hostname is an IP address using {@link LdapUtils#isIPAddress(String)}. Delegates to {@link + * #verifyIP(String, X509Certificate)} and {@link #verifyDNS(String, X509Certificate)} accordingly. + * + * @param hostname to verify + * @param cert to verify hostname against + * @return whether hostname is valid for the supplied certificate + */ + @Override + public boolean verify(final String hostname, final X509Certificate cert) { + final boolean b; + if (LdapUtils.isIPAddress(hostname)) { + b = verifyIP(hostname, cert); + } else { + b = verifyDNS(hostname, cert); + } + return b; + } + + /** + * Verify the certificate allows use of the supplied IP address. + * + *

From RFC2818: In some cases, the URI is specified as an IP address rather than a hostname. In this case, the + * iPAddress subjectAltName must be present in the certificate and must exactly match the IP in the URI.

+ * + * @param ip address to match in the certificate + * @param cert to inspect for the IP address + * @return whether the ip matched a subject alt name + */ + protected boolean verifyIP(final String ip, final X509Certificate cert) { + final String[] subjAltNames = getSubjectAltNames(cert, SubjectAltNameType.IP_ADDRESS); + for (String name : subjAltNames) { + if (ip.equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + + /** + * Verify the certificate allows use of the supplied DNS name. Note that only the first CN is used. + * + *

From RFC2818: If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. + * Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used. Although the + * use of the Common Name is existing practice, it is deprecated and Certification Authorities are encouraged to use + * the dNSName instead.

+ * + *

Matching is performed using the matching rules specified by [RFC2459]. If more than one identity of a given type + * is present in the certificate (e.g., more than one dNSName name, a match in any one of the set is considered + * acceptable.)

+ * + * @param hostname to match in the certificate + * @param cert to inspect for the hostname + * @return whether the hostname matched a subject alt name or CN + */ + protected boolean verifyDNS(final String hostname, final X509Certificate cert) { + boolean verified = false; + final String[] subjAltNames = getSubjectAltNames(cert, SubjectAltNameType.DNS_NAME); + if (subjAltNames.length > 0) { + // if subject alt names exist, one must match + for (String name : subjAltNames) { + if (isMatch(hostname, name)) { + verified = true; + break; + } + } + } else { + final String[] cns = getCNs(cert); + if (cns.length > 0) { + // the most specific CN refers to the last CN + if (isMatch(hostname, cns[cns.length - 1])) { + verified = true; + } + } + } + return verified; + } + + /** + * Returns the subject alternative names matching the supplied name type from the supplied certificate. + * + * @param cert to get subject alt names from + * @param type subject alt name type + * @return subject alt names + */ + private String[] getSubjectAltNames(final X509Certificate cert, final SubjectAltNameType type) { + final List names = new ArrayList<>(); + try { + final Collection> subjAltNames = cert.getSubjectAlternativeNames(); + if (subjAltNames != null) { + for (List generalName : subjAltNames) { + final Integer nameType = (Integer) generalName.get(0); + if (nameType == type.ordinal()) { + names.add((String) generalName.get(1)); + } + } + } + } catch (CertificateParsingException e) { + // + } + return names.toArray(new String[0]); + } + + /** + * Returns the CNs from the supplied certificate. + * + * @param cert to get CNs from + * @return CNs + */ + private String[] getCNs(final X509Certificate cert) { + final List names = new ArrayList<>(); + final byte[] encodedDn = cert.getSubjectX500Principal().getEncoded(); + if (encodedDn != null && encodedDn.length > 0) { + final X509DnDecoder decoder = new X509DnDecoder(); + final Dn dn = decoder.apply(new DefaultDERBuffer(encodedDn)); + for (RDn rdn : dn.getRDns()) { + // for multi value RDNs the first value is used + final NameValue nameValue = rdn.getNameValue("2.5.4.3"); + if (nameValue != null) { + names.add(nameValue.getStringValue()); + } + } + } + return names.toArray(new String[0]); + } + + /** + * Determines if the supplied hostname matches a name derived from the certificate. If the certificate name starts + * with '*', the domain components after the first '.' in each name are compared. + * + * @param hostname to match + * @param certName to match + * @return whether the hostname matched the cert name + */ + private boolean isMatch(final String hostname, final String certName) { + // must start with '*' and contain two domain components + final boolean isWildcard = certName.startsWith("*.") && certName.indexOf('.') < certName.lastIndexOf('.'); + + final boolean match; + if (isWildcard) { + final String certNameDomain = certName.substring(certName.indexOf(".")); + + final int hostnameIdx = hostname.contains(".") ? hostname.indexOf(".") : hostname.length(); + final String hostnameDomain = hostname.substring(hostnameIdx); + + match = certNameDomain.equalsIgnoreCase(hostnameDomain); + } else { + match = certName.equalsIgnoreCase(hostname); + } + return match; + } + + + /** + * Enum for subject alt name types. + */ + private enum SubjectAltNameType { + + /** + * other name (0). + */ + OTHER_NAME, + + /** + * ref822 name (1). + */ + RFC822_NAME, + + /** + * dns name (2). + */ + DNS_NAME, + + /** + * x400 address (3). + */ + X400_ADDRESS, + + /** + * directory name (4). + */ + DIRECTORY_NAME, + + /** + * edi party name (5). + */ + EDI_PARTY_NAME, + + /** + * uniform resource identifier (6). + */ + UNIFORM_RESOURCE_IDENTIFIER, + + /** + * ip address (7). + */ + IP_ADDRESS, + + /** + * registered id (8). + */ + REGISTERED_ID + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultSSLContextInitializer.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultSSLContextInitializer.java new file mode 100644 index 0000000..ead1289 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultSSLContextInitializer.java @@ -0,0 +1,83 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Arrays; +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +/** + * Provides a default implementation of SSL context initializer which allows the setting of trust and key managers in + * order to create an SSL context. + * + */ +public class DefaultSSLContextInitializer extends AbstractSSLContextInitializer { + + /** + * Whether default trust managers should be created. + */ + private final boolean createDefaultTrustManagers; + /** + * Key managers. + */ + private KeyManager[] keyManagers; + + + /** + * Creates a new default ssl context initializer. Default trust managers will be produced. + */ + public DefaultSSLContextInitializer() { + this(true); + } + + + /** + * Creates a new default ssl context initializer. + * + * @param defaultTrustManagers whether default trust managers should be created + */ + public DefaultSSLContextInitializer(final boolean defaultTrustManagers) { + createDefaultTrustManagers = defaultTrustManagers; + } + + + @Override + protected TrustManager[] createTrustManagers() + throws GeneralSecurityException { + if (createDefaultTrustManagers) { + final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + return tmf.getTrustManagers(); + } + return null; + } + + + @Override + public KeyManager[] getKeyManagers() + throws GeneralSecurityException { + return keyManagers; + } + + + /** + * Sets the key managers. + * + * @param managers key managers + */ + public void setKeyManagers(final KeyManager... managers) { + keyManagers = managers; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustManagers=" + Arrays.toString(trustManagers) + ", " + + "keyManagers=" + Arrays.toString(keyManagers) + ", " + + "createDefaultTrustManagers=" + createDefaultTrustManagers + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultTrustManager.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultTrustManager.java new file mode 100644 index 0000000..d02a6a0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/DefaultTrustManager.java @@ -0,0 +1,110 @@ + +package org.xbib.net.ldap.ssl; + +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Loads the trust managers from the default {@link TrustManagerFactory} and delegates to those. + * + */ +public class DefaultTrustManager extends X509ExtendedTrustManager { + + /** + * Default trust managers. + */ + private final X509ExtendedTrustManager[] trustManagers; + + + /** + * Creates a new default trust manager. + */ + public DefaultTrustManager() { + try { + final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + trustManagers = Stream.of(tmf.getTrustManagers()) + .map(X509ExtendedTrustManager.class::cast) + .toArray(X509ExtendedTrustManager[]::new); + } catch (GeneralSecurityException e) { + throw new IllegalStateException(e); + } + } + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType, final Socket socket) + throws CertificateException { + for (X509ExtendedTrustManager tm : trustManagers) { + tm.checkClientTrusted(chain, authType, socket); + } + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType, final Socket socket) + throws CertificateException { + for (X509ExtendedTrustManager tm : trustManagers) { + tm.checkServerTrusted(chain, authType, socket); + } + } + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType, final SSLEngine engine) + throws CertificateException { + for (X509ExtendedTrustManager tm : trustManagers) { + tm.checkClientTrusted(chain, authType, engine); + } + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType, final SSLEngine engine) + throws CertificateException { + for (X509ExtendedTrustManager tm : trustManagers) { + tm.checkServerTrusted(chain, authType, engine); + } + } + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + for (X509TrustManager tm : trustManagers) { + tm.checkClientTrusted(chain, authType); + } + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + for (X509TrustManager tm : trustManagers) { + tm.checkServerTrusted(chain, authType); + } + } + + + @Override + public X509Certificate[] getAcceptedIssuers() { + final List issuers = new ArrayList<>(); + if (trustManagers != null) { + for (X509TrustManager tm : trustManagers) { + Collections.addAll(issuers, tm.getAcceptedIssuers()); + } + } + return issuers.toArray(new X509Certificate[0]); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameResolver.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameResolver.java new file mode 100644 index 0000000..b2c33f6 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameResolver.java @@ -0,0 +1,60 @@ + +package org.xbib.net.ldap.ssl; + +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLSession; +import javax.net.ssl.StandardConstants; + +/** + * Resolves a hostname from an {@link SSLSession}. + * + */ +public class HostnameResolver { + /** + * SSL session. + */ + private final SSLSession sslSession; + + + /** + * Creates a new hostname resolver. + * + * @param session SSL session + */ + public HostnameResolver(final SSLSession session) { + sslSession = session; + } + + + /** + * Resolves a hostname from the SSL session. + * + * @return hostname + */ + public String resolve() { + String hostname = null; + if (sslSession instanceof ExtendedSSLSession) { + final SNIServerName sniName = ((ExtendedSSLSession) sslSession).getRequestedServerNames().stream() + .filter(n -> StandardConstants.SNI_HOST_NAME == n.getType()) + .findFirst() + .orElse(null); + if (sniName != null) { + if (sniName instanceof SNIHostName) { + hostname = ((SNIHostName) sniName).getAsciiName(); + } else { + try { + hostname = new SNIHostName(sniName.getEncoded()).getAsciiName(); + } catch (IllegalArgumentException e) { + // + } + } + } + } + if (hostname == null && sslSession != null) { + hostname = sslSession.getPeerHost(); + } + return hostname; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameVerifierAdapter.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameVerifierAdapter.java new file mode 100644 index 0000000..6262109 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameVerifierAdapter.java @@ -0,0 +1,59 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; + +/** + * Adapts a {@link CertificateHostnameVerifier} for use as a {@link HostnameVerifier}. This component can only be used + * with a verified SSL session as it accesses the certificate from {@link SSLSession#getPeerCertificates()}. + * + */ +public class HostnameVerifierAdapter implements HostnameVerifier { + + /** + * Hostname verifier to adapt. + */ + private final CertificateHostnameVerifier hostnameVerifier; + + + /** + * Creates a new hostname verifier adapter. + * + * @param verifier verifier to adapt + */ + public HostnameVerifierAdapter(final CertificateHostnameVerifier verifier) { + hostnameVerifier = verifier; + } + + + @Override + public boolean verify(final String hostname, final SSLSession session) { + boolean b = false; + if (session != null) { + try { + String name = null; + if (hostname != null) { + // if IPv6 strip off the "[]" + if (hostname.startsWith("[") && hostname.endsWith("]")) { + name = hostname.substring(1, hostname.length() - 1).trim(); + } else { + name = hostname.trim(); + } + } + b = hostnameVerifier.verify(name, (X509Certificate) session.getPeerCertificates()[0]); + } catch (SSLPeerUnverifiedException e) { + // + } + } + return b; + } + + + @Override + public String toString() { + return "[" + getClass().getName() + "@" + hashCode() + "::" + "hostnameVerifier=" + hostnameVerifier + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameVerifyingListener.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameVerifyingListener.java new file mode 100644 index 0000000..1625500 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/HostnameVerifyingListener.java @@ -0,0 +1,94 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.IOException; +import javax.net.ssl.HandshakeCompletedEvent; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; + +/** + * Handshake completed listener that invokes a hostname verifier. If hostname verification fails, the socket is closed + * and the SSL session is invalidated. + * + */ +public class HostnameVerifyingListener implements HandshakeCompletedListener { + /** + * Hostname verifier invoked when the handshake completes. + */ + private final HostnameVerifier hostnameVerifier; + + /** + * Whether this listener has been invoked. + */ + private boolean invoked; + + /** + * Whether hostname verification succeeded. + */ + private boolean verified; + + /** + * Hostname used in verification. + */ + private String hostname; + + + /** + * Creates a new verifying handshake completed listener. Hostname will be derived from the SSL session. + * + * @param verifier hostname verifier + */ + public HostnameVerifyingListener(final HostnameVerifier verifier) { + hostnameVerifier = verifier; + } + + + /** + * Creates a new verifying handshake completed listener. + * + * @param verifier hostname verifier + * @param name hostname to verify + */ + public HostnameVerifyingListener(final HostnameVerifier verifier, final String name) { + hostnameVerifier = verifier; + hostname = name; + } + + + @Override + public void handshakeCompleted(final HandshakeCompletedEvent event) { + invoked = true; + if (hostname == null) { + hostname = event.getSession().getPeerHost(); + } + if (!hostnameVerifier.verify(hostname, event.getSession())) { + try { + event.getSocket().close(); + } catch (IOException e) { + // + } + event.getSession().invalidate(); + } else { + verified = true; + } + } + + + /** + * Throws exception if hostname verification failed. + * + * @throws IllegalStateException if this listener has not been invoked + * @throws SSLPeerUnverifiedException if the hostname failed to verify + */ + public void peerVerified() + throws SSLPeerUnverifiedException { + if (!invoked) { + throw new IllegalStateException("Handshake has not completed"); + } + if (!verified) { + throw new SSLPeerUnverifiedException( + String.format("Hostname '%s' does not match the hostname in the server's certificate", hostname)); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreCredentialConfig.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreCredentialConfig.java new file mode 100644 index 0000000..32844fe --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreCredentialConfig.java @@ -0,0 +1,347 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides the properties necessary for creating an SSL context initializer with a keystore credential reader. + * + */ +public class KeyStoreCredentialConfig implements CredentialConfig { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1013; + + /** + * Handles loading keystores. + */ + private final KeyStoreCredentialReader keyStoreReader = new KeyStoreCredentialReader(); + + /** + * Name of the truststore to use for the SSL connection. + */ + private String trustStore; + + /** + * Password needed to open the truststore. + */ + private String trustStorePassword; + + /** + * Truststore type. + */ + private String trustStoreType; + + /** + * Truststore aliases to use. + */ + private String[] trustStoreAliases; + + /** + * Name of the keystore to use for the SSL connection. + */ + private String keyStore; + + /** + * Password needed to open the keystore. + */ + private String keyStorePassword; + + /** + * Keystore type. + */ + private String keyStoreType; + + /** + * Keystore aliases to use. + */ + private String[] keyStoreAliases; + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the name of the truststore to use. + * + * @return truststore name + */ + public String getTrustStore() { + return trustStore; + } + + /** + * Sets the name of the truststore to use. + * + * @param name truststore name + */ + public void setTrustStore(final String name) { + trustStore = name; + } + + /** + * Returns the password for the truststore. + * + * @return truststore password + */ + public String getTrustStorePassword() { + return trustStorePassword; + } + + /** + * Sets the password for the truststore. + * + * @param password truststore password + */ + public void setTrustStorePassword(final String password) { + trustStorePassword = password; + } + + /** + * Returns the type of the truststore. + * + * @return truststore type + */ + public String getTrustStoreType() { + return trustStoreType; + } + + /** + * Sets the type of the truststore. + * + * @param type truststore type + */ + public void setTrustStoreType(final String type) { + trustStoreType = type; + } + + /** + * Returns the aliases of the truststore to use. + * + * @return truststore aliases + */ + public String[] getTrustStoreAliases() { + return trustStoreAliases; + } + + /** + * Sets the aliases of the truststore to use. + * + * @param aliases truststore aliases + */ + public void setTrustStoreAliases(final String... aliases) { + trustStoreAliases = aliases; + } + + /** + * Returns the name of the keystore to use. + * + * @return keystore name + */ + public String getKeyStore() { + return keyStore; + } + + /** + * Sets the name of the keystore to use. + * + * @param name keystore name + */ + public void setKeyStore(final String name) { + keyStore = name; + } + + /** + * Returns the password for the keystore. + * + * @return keystore password + */ + public String getKeyStorePassword() { + return keyStorePassword; + } + + /** + * Sets the password for the keystore. + * + * @param password keystore password + */ + public void setKeyStorePassword(final String password) { + keyStorePassword = password; + } + + /** + * Returns the type of the keystore. + * + * @return keystore type + */ + public String getKeyStoreType() { + return keyStoreType; + } + + /** + * Sets the type of the keystore. + * + * @param type keystore type + */ + public void setKeyStoreType(final String type) { + keyStoreType = type; + } + + /** + * Returns the aliases of the keystore to use. + * + * @return keystore aliases + */ + public String[] getKeyStoreAliases() { + return keyStoreAliases; + } + + /** + * Sets the aliases of the keystore to use. + * + * @param aliases keystore aliases + */ + public void setKeyStoreAliases(final String... aliases) { + keyStoreAliases = aliases; + } + + @Override + public SSLContextInitializer createSSLContextInitializer() + throws GeneralSecurityException { + final KeyStoreSSLContextInitializer sslInit = new KeyStoreSSLContextInitializer(); + try { + if (trustStore != null) { + sslInit.setTrustKeystore(keyStoreReader.read(trustStore, trustStorePassword, trustStoreType)); + sslInit.setTrustAliases(trustStoreAliases); + } + if (keyStore != null) { + sslInit.setAuthenticationKeystore(keyStoreReader.read(keyStore, keyStorePassword, keyStoreType)); + sslInit.setAuthenticationPassword(keyStorePassword != null ? keyStorePassword.toCharArray() : null); + sslInit.setAuthenticationAliases(keyStoreAliases); + } + } catch (IOException e) { + throw new GeneralSecurityException(e); + } + return sslInit; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof KeyStoreCredentialConfig v) { + return LdapUtils.areEqual(trustStore, v.trustStore) && + LdapUtils.areEqual(trustStoreType, v.trustStoreType) && + LdapUtils.areEqual(trustStorePassword, v.trustStorePassword) && + LdapUtils.areEqual(trustStoreAliases, v.trustStoreAliases) && + LdapUtils.areEqual(keyStore, v.keyStore) && + LdapUtils.areEqual(keyStoreType, v.keyStoreType) && + LdapUtils.areEqual(keyStorePassword, v.keyStorePassword) && + LdapUtils.areEqual(keyStoreAliases, v.keyStoreAliases); + } + return false; + } + + @Override + public int hashCode() { + return + LdapUtils.computeHashCode( + HASH_CODE_SEED, + trustStore, + trustStoreType, + trustStorePassword, + trustStoreAliases, + keyStore, + keyStoreType, + keyStorePassword, + keyStoreAliases); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustStore=" + trustStore + ", " + + "trustStoreType=" + trustStoreType + ", " + + "trustStoreAliases=" + Arrays.toString(trustStoreAliases) + ", " + + "keyStore=" + keyStore + ", " + + "keyStoreType=" + keyStoreType + ", " + + "keyStoreAliases=" + Arrays.toString(keyStoreAliases) + "]"; + } + + // CheckStyle:OFF + public static class Builder { + + + private final KeyStoreCredentialConfig object = new KeyStoreCredentialConfig(); + + + protected Builder() { + } + + + public Builder trustStore(final String name) { + object.setTrustStore(name); + return this; + } + + + public Builder trustStorePassword(final String password) { + object.setTrustStorePassword(password); + return this; + } + + + public Builder trustStoreType(final String type) { + object.setTrustStoreType(type); + return this; + } + + + public Builder trustStoreAliases(final String... aliases) { + object.setTrustStoreAliases(aliases); + return this; + } + + + public Builder keyStore(final String name) { + object.setKeyStore(name); + return this; + } + + + public Builder keyStorePassword(final String password) { + object.setKeyStorePassword(password); + return this; + } + + + public Builder keyStoreType(final String type) { + object.setKeyStoreType(type); + return this; + } + + + public Builder keyStoreAliases(final String... aliases) { + object.setKeyStoreAliases(aliases); + return this; + } + + + public KeyStoreCredentialConfig build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreCredentialReader.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreCredentialReader.java new file mode 100644 index 0000000..7e57903 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreCredentialReader.java @@ -0,0 +1,55 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Arrays; + +/** + * Reads keystore credentials from a classpath, filepath, or stream resource. + * + */ +public class KeyStoreCredentialReader extends AbstractCredentialReader { + + + /** + * Reads a keystore from an input stream. + * + * @param is Input stream from which to read keystore. + * @param params Two optional parameters are supported: + * + *
    + *
  • keystore password
  • + *
  • keystore type; defaults to JVM default keystore format if omitted
  • + *
+ * + *

If only a single parameter is supplied, it is assumed to be the password.

+ * @return keystore read from data in stream. + * @throws IOException On IO errors. + * @throws GeneralSecurityException On errors with the credential data. + */ + @Override + public KeyStore read(final InputStream is, final String... params) + throws IOException, GeneralSecurityException { + char[] password = null; + if (params.length > 0 && params[0] != null) { + password = params[0].toCharArray(); + } + + String type = KeyStore.getDefaultType(); + if (params.length > 1 && params[1] != null) { + type = params[1]; + } + + final KeyStore keystore = KeyStore.getInstance(type); + if (is != null) { + keystore.load(getBufferedInputStream(is), password); + if (password != null) { + Arrays.fill(password, '0'); + } + } + return keystore; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreSSLContextInitializer.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreSSLContextInitializer.java new file mode 100644 index 0000000..cc703e7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreSSLContextInitializer.java @@ -0,0 +1,235 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Arrays; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +/** + * Provides an SSL context initializer which can use java KeyStores to create key and trust managers. + * + */ +public class KeyStoreSSLContextInitializer extends AbstractSSLContextInitializer { + + /** + * KeyStore used to create trust managers. + */ + private KeyStore trustKeystore; + + /** + * Aliases of trust entries to use. + */ + private String[] trustAliases; + + /** + * KeyStore used to create key managers. + */ + private KeyStore authenticationKeystore; + + /** + * Aliases of key entries to use. + */ + private String[] authenticationAliases; + + /** + * Password used to access the authentication keystore. + */ + private char[] authenticationPassword; + + + /** + * Returns the keystore to use for creating the trust managers. + * + * @return keystore + */ + public KeyStore getTrustKeystore() { + return trustKeystore; + } + + + /** + * Sets the keystore to use for creating the trust managers. + * + * @param keystore to set + */ + public void setTrustKeystore(final KeyStore keystore) { + trustKeystore = keystore; + } + + + /** + * Returns the aliases of the entries to use in the trust keystore + * + * @return trust aliases + */ + public String[] getTrustAliases() { + return trustAliases; + } + + + /** + * Sets the aliases of the entries to use in the trust keystore. + * + * @param aliases to use + */ + public void setTrustAliases(final String... aliases) { + trustAliases = aliases; + } + + + /** + * Returns the keystore to use for creating the key managers. + * + * @return keystore + */ + public KeyStore getAuthenticationKeystore() { + return authenticationKeystore; + } + + + /** + * Sets the keystore to use for creating the key managers. + * + * @param keystore to set + */ + public void setAuthenticationKeystore(final KeyStore keystore) { + authenticationKeystore = keystore; + } + + + /** + * Returns the aliases of the entries to use in the authentication keystore + * + * @return authentication aliases + */ + public String[] getAuthenticationAliases() { + return authenticationAliases; + } + + + /** + * Sets the aliases of the entries to use in the authentication keystore. + * + * @param aliases to use + */ + public void setAuthenticationAliases(final String... aliases) { + authenticationAliases = aliases; + } + + + /** + * Returns the password used for accessing the authentication keystore. + * + * @return authentication password + */ + public char[] getAuthenticationPassword() { + return authenticationPassword; + } + + + /** + * Sets the password used for accessing the authentication keystore. + * + * @param password to use for authentication + */ + public void setAuthenticationPassword(final char[] password) { + authenticationPassword = password; + } + + + @Override + protected TrustManager[] createTrustManagers() + throws GeneralSecurityException { + TrustManager[] tm = null; + if (trustKeystore != null) { + final TrustManagerFactory tmf = getTrustManagerFactory(trustKeystore, trustAliases); + tm = tmf.getTrustManagers(); + } + return tm; + } + + + /** + * Creates a new trust manager factory. + * + * @param keystore to initialize the trust manager factory + * @param aliases to include from the supplied keystore or null to include all entries + * @return trust manager factory + * @throws GeneralSecurityException if the trust manager factory cannot be initialized + */ + protected TrustManagerFactory getTrustManagerFactory(final KeyStore keystore, final String... aliases) + throws GeneralSecurityException { + final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + if (aliases != null && aliases.length > 0) { + final KeyStore ks = KeyStoreUtils.newInstance(); + for (String alias : aliases) { + final KeyStore.Entry entry = KeyStoreUtils.getEntry(alias, keystore, null); + KeyStoreUtils.setEntry(alias, entry, ks, null); + } + tmf.init(ks); + } else { + tmf.init(keystore); + } + return tmf; + } + + + @Override + public KeyManager[] getKeyManagers() + throws GeneralSecurityException { + KeyManager[] km = null; + if (authenticationKeystore != null && authenticationPassword != null) { + final KeyManagerFactory kmf = getKeyManagerFactory( + authenticationKeystore, + authenticationPassword, + authenticationAliases); + km = kmf.getKeyManagers(); + } + return km; + } + + + /** + * Creates a new key manager factory. + * + * @param keystore to initialize the key manager factory + * @param password to unlock the supplied keystore + * @param aliases to include from the supplied keystore or null to include all entries + * @return key manager factory + * @throws GeneralSecurityException if the key manager factory cannot be initialized + */ + protected KeyManagerFactory getKeyManagerFactory( + final KeyStore keystore, + final char[] password, + final String... aliases) + throws GeneralSecurityException { + final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (aliases != null && aliases.length > 0) { + final KeyStore ks = KeyStoreUtils.newInstance(password); + for (String alias : aliases) { + final KeyStore.Entry entry = KeyStoreUtils.getEntry(alias, keystore, password); + KeyStoreUtils.setEntry(alias, entry, ks, password); + } + kmf.init(ks, password); + } else { + kmf.init(keystore, password); + } + return kmf; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustManagers=" + Arrays.toString(trustManagers) + ", " + + "trustKeystore=" + trustKeystore + ", " + + "trustAliases=" + Arrays.toString(trustAliases) + ", " + + "authenticationKeystore=" + authenticationKeystore + ", " + + "authenticationAliases=" + Arrays.toString(authenticationAliases) + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreUtils.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreUtils.java new file mode 100644 index 0000000..d5ed53e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/KeyStoreUtils.java @@ -0,0 +1,164 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyStore; +import java.security.cert.Certificate; + +/** + * Provides utility methods for using a {@link KeyStore}. + * + */ +public final class KeyStoreUtils { + + /** + * Default keystore type. + */ + private static final String DEFAULT_TYPE = KeyStore.getDefaultType(); + + + /** + * Default constructor. + */ + private KeyStoreUtils() { + } + + + /** + * Creates a new {@link KeyStore} with the default keystore type and initializes it. + * + * @return initialized keystore + * @throws GeneralSecurityException if the keystore cannot be initialized + */ + public static KeyStore newInstance() + throws GeneralSecurityException { + return newInstance(DEFAULT_TYPE); + } + + + /** + * Creates a new {@link KeyStore} with the default keystore type and initializes it. + * + * @param password to protect the keystore + * @return initialized keystore + * @throws GeneralSecurityException if the keystore cannot be initialized + */ + public static KeyStore newInstance(final char[] password) + throws GeneralSecurityException { + return newInstance(DEFAULT_TYPE, password); + } + + + /** + * Creates a new {@link KeyStore} and initializes it. + * + * @param type of keystore instance + * @return initialized keystore + * @throws GeneralSecurityException if the keystore cannot be initialized + */ + public static KeyStore newInstance(final String type) + throws GeneralSecurityException { + return newInstance(type, null); + } + + + /** + * Creates a new {@link KeyStore} and initializes it. + * + * @param type of keystore instance + * @param password to protect the keystore + * @return initialized keystore + * @throws GeneralSecurityException if the keystore cannot be initialized + */ + public static KeyStore newInstance(final String type, final char[] password) + throws GeneralSecurityException { + final KeyStore.Builder builder = KeyStore.Builder.newInstance( + type, + null, + new KeyStore.PasswordProtection(password)); + return builder.getKeyStore(); + } + + + /** + * Returns a keystore entry from the supplied keystore. + * + * @param alias of the entry to return + * @param keystore to read the entry from + * @param password to access the keystore + * @return keystore entry + * @throws GeneralSecurityException if the keystore cannot be read + * @throws IllegalArgumentException if the alias does not exist + */ + public static KeyStore.Entry getEntry(final String alias, final KeyStore keystore, final char[] password) + throws GeneralSecurityException { + if (!keystore.containsAlias(alias)) { + throw new IllegalArgumentException("KeyStore does not contain alias " + alias); + } + return keystore.getEntry(alias, password != null ? new KeyStore.PasswordProtection(password) : null); + } + + + /** + * Sets a keystore entry on the supplied keystore. + * + * @param alias of the supplied entry + * @param entry to set + * @param keystore to set the entry on + * @param password to protect the entry + * @throws GeneralSecurityException if the keystore cannot be modified + */ + public static void setEntry( + final String alias, + final KeyStore.Entry entry, + final KeyStore keystore, + final char[] password) + throws GeneralSecurityException { + keystore.setEntry(alias, entry, password != null ? new KeyStore.PasswordProtection(password) : null); + } + + + /** + * Sets a key entry on the supplied keystore. + * + * @param alias of the supplied key + * @param keystore to set the key on + * @param password to protect the key + * @param key to set + * @param certs associated with the key + * @throws GeneralSecurityException if the keystore cannot be modified + */ + public static void setKeyEntry( + final String alias, + final KeyStore keystore, + final char[] password, + final Key key, + final Certificate... certs) + throws GeneralSecurityException { + keystore.setKeyEntry(alias, key, password, certs); + } + + + /** + * Sets certificate entries on the supplied keystore. For certificate arrays of size greater than 1, the alias is + * appended with an index. + * + * @param alias of the supplied certificate(s) + * @param keystore to set the cert(s) on + * @param certs to set + * @throws GeneralSecurityException if the keystore cannot be modified + */ + public static void setCertificateEntry(final String alias, final KeyStore keystore, final Certificate... certs) + throws GeneralSecurityException { + if (certs != null && certs.length > 0) { + if (certs.length == 1) { + keystore.setCertificateEntry(alias, certs[0]); + } else { + for (int i = 0; i < certs.length; i++) { + keystore.setCertificateEntry(String.format("%s%s", alias, i), certs[i]); + } + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/PrivateKeyCredentialReader.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/PrivateKeyCredentialReader.java new file mode 100644 index 0000000..9dc610e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/PrivateKeyCredentialReader.java @@ -0,0 +1,41 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import org.xbib.net.ldap.LdapUtils; + +/** + * Reads private key credentials from classpath, filepath, or stream resource. Supported private key formats include: + * PKCS8. + * + */ +public class PrivateKeyCredentialReader extends AbstractCredentialReader { + + + /** + * Reads a private key from an input stream. + * + * @param is Input stream from which to read private key. + * @param params A single optional parameter, algorithm, may be specified. The default is RSA. + * @return Private key read from data in stream. + * @throws IOException On IO errors. + * @throws GeneralSecurityException On errors with the credential data. + */ + @Override + public PrivateKey read(final InputStream is, final String... params) + throws IOException, GeneralSecurityException { + String algorithm = "RSA"; + if (params.length > 0 && params[0] != null) { + algorithm = params[0]; + } + + final KeyFactory kf = KeyFactory.getInstance(algorithm); + final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(LdapUtils.readInputStream(getBufferedInputStream(is))); + return kf.generatePrivate(spec); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/SSLContextInitializer.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/SSLContextInitializer.java new file mode 100644 index 0000000..7dd9ce8 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/SSLContextInitializer.java @@ -0,0 +1,53 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.GeneralSecurityException; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +/** + * Provides an interface for the initialization of new SSL contexts. + * + */ +public interface SSLContextInitializer { + + + /** + * Creates an initialized SSLContext for the supplied protocol. + * + * @param protocol type to use for SSL + * @return SSL context + * @throws GeneralSecurityException if the SSLContext cannot be created + */ + SSLContext initSSLContext(String protocol) + throws GeneralSecurityException; + + + /** + * Returns the trust managers used when creating SSL contexts. + * + * @return trust managers + * @throws GeneralSecurityException if an errors occurs while loading the TrustManagers + */ + TrustManager[] getTrustManagers() + throws GeneralSecurityException; + + + /** + * Sets the trust managers. May be in isolation or in conjunction with other trust material. + * + * @param managers trust managers + */ + void setTrustManagers(TrustManager... managers); + + + /** + * Returns the key managers used when creating SSL contexts. + * + * @return key managers + * @throws GeneralSecurityException if an errors occurs while loading the KeyManagers + */ + KeyManager[] getKeyManagers() + throws GeneralSecurityException; +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/SslConfig.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/SslConfig.java new file mode 100644 index 0000000..0bfaa59 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/SslConfig.java @@ -0,0 +1,359 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.GeneralSecurityException; +import java.time.Duration; +import java.util.Arrays; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.TrustManager; +import org.xbib.net.ldap.AbstractConfig; + +/** + * Contains all the configuration data for SSL and startTLS. + * + */ +public class SslConfig extends AbstractConfig { + + /** + * Configuration for the trust and authentication material to use for SSL and startTLS. + */ + private CredentialConfig credentialConfig; + + /** + * Trust managers. + */ + private TrustManager[] trustManagers; + + /** + * Certificate hostname verifier. + */ + private CertificateHostnameVerifier hostnameVerifier; + + /** + * Enabled cipher suites. + */ + private String[] enabledCipherSuites; + + /** + * Enabled protocol versions. + */ + private String[] enabledProtocols; + + /** + * Handshake completed listeners. + */ + private HandshakeCompletedListener[] handshakeCompletedListeners; + + /** + * Duration of time that handshakes will block. + */ + private Duration handshakeTimeout = Duration.ofMinutes(1); + + + /** + * Default constructor. + */ + public SslConfig() { + } + + + /** + * Creates a new ssl config. + * + * @param config credential config + */ + public SslConfig(final CredentialConfig config) { + credentialConfig = config; + } + + + /** + * Creates a new ssl config. + * + * @param managers trust managers + */ + public SslConfig(final TrustManager... managers) { + trustManagers = managers; + } + + + /** + * Creates a new ssl config. + * + * @param config credential config + * @param managers trust managers + */ + public SslConfig(final CredentialConfig config, final TrustManager... managers) { + credentialConfig = config; + trustManagers = managers; + } + + /** + * Returns a ssl config initialized with the supplied config. + * + * @param config ssl config to read properties from + * @return ssl config + */ + public static SslConfig copy(final SslConfig config) { + final SslConfig sc = new SslConfig(); + sc.setCredentialConfig(config.getCredentialConfig()); + sc.setTrustManagers(config.getTrustManagers()); + sc.setHostnameVerifier(config.getHostnameVerifier()); + sc.setEnabledCipherSuites(config.getEnabledCipherSuites()); + sc.setEnabledProtocols(config.getEnabledProtocols()); + sc.setHandshakeCompletedListeners(config.getHandshakeCompletedListeners()); + sc.setHandshakeTimeout(config.getHandshakeTimeout()); + return sc; + } + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns whether this ssl config contains any configuration data. + * + * @return whether all properties are null + */ + public boolean isEmpty() { + return + credentialConfig == null && trustManagers == null && hostnameVerifier == null && enabledCipherSuites == null && + enabledProtocols == null && handshakeCompletedListeners == null; + } + + /** + * Returns the credential config. + * + * @return credential config + */ + public CredentialConfig getCredentialConfig() { + return credentialConfig; + } + + /** + * Sets the credential config. + * + * @param config credential config + */ + public void setCredentialConfig(final CredentialConfig config) { + credentialConfig = config; + } + + /** + * Returns the trust managers. + * + * @return trust managers + */ + public TrustManager[] getTrustManagers() { + return trustManagers; + } + + /** + * Sets the trust managers. + * + * @param managers trust managers + */ + public void setTrustManagers(final TrustManager... managers) { + checkArrayContainsNull(managers); + trustManagers = managers; + } + + /** + * Returns the hostname verifier. + * + * @return hostname verifier + */ + public CertificateHostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + /** + * Sets the hostname verifier. + * + * @param verifier hostname verifier + */ + public void setHostnameVerifier(final CertificateHostnameVerifier verifier) { + hostnameVerifier = verifier; + } + + /** + * Returns the names of the SSL cipher suites to use for secure connections. + * + * @return cipher suites + */ + public String[] getEnabledCipherSuites() { + return enabledCipherSuites; + } + + /** + * Sets the SSL cipher suites to use for secure connections. + * + * @param suites cipher suites + */ + public void setEnabledCipherSuites(final String... suites) { + checkArrayContainsNull(suites); + enabledCipherSuites = suites; + } + + /** + * Returns the names of the SSL protocols to use for secure connections. + * + * @return enabled protocols + */ + public String[] getEnabledProtocols() { + return enabledProtocols; + } + + /** + * Sets the SSL protocol versions to use for secure connections. + * + * @param protocols enabled protocols + */ + public void setEnabledProtocols(final String... protocols) { + checkArrayContainsNull(protocols); + enabledProtocols = protocols; + } + + /** + * Returns the handshake completed listeners to use for secure connections. + * + * @return handshake completed listeners + */ + public HandshakeCompletedListener[] getHandshakeCompletedListeners() { + return handshakeCompletedListeners; + } + + /** + * Sets the handshake completed listeners to use for secure connections. + * + * @param listeners for SSL handshake events + */ + public void setHandshakeCompletedListeners(final HandshakeCompletedListener... listeners) { + checkArrayContainsNull(listeners); + handshakeCompletedListeners = listeners; + } + + /** + * Returns the handshake timeout. + * + * @return timeout + */ + public Duration getHandshakeTimeout() { + return handshakeTimeout; + } + + /** + * Sets the maximum amount of time that handshakes will block. + * + * @param time timeout for handshakes + */ + public void setHandshakeTimeout(final Duration time) { + if (time == null || time.isNegative()) { + throw new IllegalArgumentException("Handshake timeout cannot be null or negative"); + } + handshakeTimeout = time; + } + + /** + * Creates an {@link SSLContextInitializer} from this configuration. If a {@link CredentialConfig} is provided it is + * used, otherwise a {@link DefaultSSLContextInitializer} is created. + * + * @return SSL context initializer + * @throws GeneralSecurityException if the SSL context initializer cannot be created + */ + public SSLContextInitializer createSSLContextInitializer() + throws GeneralSecurityException { + final SSLContextInitializer initializer; + if (credentialConfig != null) { + initializer = credentialConfig.createSSLContextInitializer(); + } else { + if (trustManagers != null) { + initializer = new DefaultSSLContextInitializer(false); + } else { + initializer = new DefaultSSLContextInitializer(true); + } + } + + if (trustManagers != null) { + initializer.setTrustManagers(trustManagers); + } + return initializer; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "credentialConfig=" + credentialConfig + ", " + + "trustManagers=" + Arrays.toString(trustManagers) + ", " + + "hostnameVerifier=" + hostnameVerifier + ", " + + "enabledCipherSuites=" + Arrays.toString(enabledCipherSuites) + ", " + + "enabledProtocols=" + Arrays.toString(enabledProtocols) + ", " + + "handshakeCompletedListeners=" + Arrays.toString(handshakeCompletedListeners) + ", " + + "handshakeTimeout=" + handshakeTimeout + "]"; + } + + // CheckStyle:OFF + public static class Builder { + + + private final SslConfig object = new SslConfig(); + + + protected Builder() { + } + + + public Builder credentialConfig(final CredentialConfig config) { + object.setCredentialConfig(config); + return this; + } + + + public Builder trustManagers(final TrustManager... managers) { + object.setTrustManagers(managers); + return this; + } + + + public Builder hostnameVerifier(final CertificateHostnameVerifier verifier) { + object.setHostnameVerifier(verifier); + return this; + } + + + public Builder cipherSuites(final String... suites) { + object.setEnabledCipherSuites(suites); + return this; + } + + + public Builder protocols(final String... protocols) { + object.setEnabledProtocols(protocols); + return this; + } + + + public Builder handshakeListeners(final HandshakeCompletedListener... listeners) { + object.setHandshakeCompletedListeners(listeners); + return this; + } + + + public Builder handshakeTimeout(final Duration timeout) { + object.setHandshakeTimeout(timeout); + return this; + } + + + public SslConfig build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CertificateCredentialReader.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CertificateCredentialReader.java new file mode 100644 index 0000000..928fc49 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CertificateCredentialReader.java @@ -0,0 +1,24 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * Loads an X.509 certificate credential from a classpath, filepath, or stream resource. Supported certificate formats + * include: PEM, DER, and PKCS7. + * + */ +public class X509CertificateCredentialReader extends AbstractCredentialReader { + + + @Override + public X509Certificate read(final InputStream is, final String... params) + throws IOException, GeneralSecurityException { + final CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(getBufferedInputStream(is)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CertificatesCredentialReader.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CertificatesCredentialReader.java new file mode 100644 index 0000000..353d408 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CertificatesCredentialReader.java @@ -0,0 +1,50 @@ + +package org.xbib.net.ldap.ssl; + + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Loads X.509 certificate credentials from a classpath, filepath, or stream resource. + * When working with filepath, multiple files may be separated using a comma (i.e. {@code cert1.pem,cert2.crt}). + * Supported certificate formats include: PEM, DER, and PKCS7. + * + */ +public class X509CertificatesCredentialReader extends AbstractCredentialReader { + + + @Override + public X509Certificate[] read(final String path, final String... params) + throws IOException, GeneralSecurityException { + final String[] paths = path.split(","); + final List certificateList = new ArrayList<>(); + for (final String individualPath : paths) { + final X509Certificate[] parsedCertificates = super.read(individualPath, params); + certificateList.addAll(Arrays.asList(parsedCertificates)); + } + return certificateList.toArray(new X509Certificate[0]); + } + + + @Override + public X509Certificate[] read(final InputStream is, final String... params) + throws IOException, GeneralSecurityException { + final CertificateFactory cf = CertificateFactory.getInstance("X.509"); + final List certList = new ArrayList<>(); + final InputStream bufIs = getBufferedInputStream(is); + while (bufIs.available() > 0) { + final X509Certificate cert = (X509Certificate) cf.generateCertificate(bufIs); + if (cert != null) { + certList.add(cert); + } + } + return certList.toArray(new X509Certificate[0]); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CredentialConfig.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CredentialConfig.java new file mode 100644 index 0000000..95f7975 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509CredentialConfig.java @@ -0,0 +1,193 @@ + +package org.xbib.net.ldap.ssl; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import org.xbib.net.ldap.LdapUtils; + +/** + * Provides the properties necessary for creating an SSL context initializer with an X.509 credential reader. + * + */ +public class X509CredentialConfig implements CredentialConfig { + + /** + * hash code seed. + */ + private static final int HASH_CODE_SEED = 1009; + + /** + * Reads X.509 certificates credential. + */ + private final X509CertificatesCredentialReader certsReader = new X509CertificatesCredentialReader(); + + /** + * Reads X.509 certificate credential. + */ + private final X509CertificateCredentialReader certReader = new X509CertificateCredentialReader(); + + /** + * Reads private key credential. + */ + private final PrivateKeyCredentialReader keyReader = new PrivateKeyCredentialReader(); + + /** + * Name of the trust certificates to use for the SSL connection. + */ + private String trustCertificates; + + /** + * Name of the authentication certificate to use for the SSL connection. + */ + private String authenticationCertificate; + + /** + * Name of the key to use for the SSL connection. + */ + private String authenticationKey; + + /** + * Creates a builder for this class. + * + * @return new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the name of the trust certificates to use. + * + * @return trust certificates name + */ + public String getTrustCertificates() { + return trustCertificates; + } + + /** + * Sets the name of the trust certificates to use. + * + * @param name trust certificates name + */ + public void setTrustCertificates(final String name) { + trustCertificates = name; + } + + /** + * Returns the name of the authentication certificate to use. + * + * @return authentication certificate name + */ + public String getAuthenticationCertificate() { + return authenticationCertificate; + } + + /** + * Sets the name of the authentication certificate to use. + * + * @param name authentication certificate name + */ + public void setAuthenticationCertificate(final String name) { + authenticationCertificate = name; + } + + /** + * Returns the name of the authentication key to use. + * + * @return authentication key name + */ + public String getAuthenticationKey() { + return authenticationKey; + } + + /** + * Sets the name of the authentication key to use. + * + * @param name authentication key name + */ + public void setAuthenticationKey(final String name) { + authenticationKey = name; + } + + @Override + public SSLContextInitializer createSSLContextInitializer() + throws GeneralSecurityException { + final X509SSLContextInitializer sslInit = new X509SSLContextInitializer(); + try { + if (trustCertificates != null) { + sslInit.setTrustCertificates(certsReader.read(trustCertificates)); + } + if (authenticationCertificate != null) { + sslInit.setAuthenticationCertificate(certReader.read(authenticationCertificate)); + } + if (authenticationKey != null) { + sslInit.setAuthenticationKey(keyReader.read(authenticationKey)); + } + } catch (IOException e) { + throw new GeneralSecurityException(e); + } + return sslInit; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof X509CredentialConfig v) { + return LdapUtils.areEqual(trustCertificates, v.trustCertificates) && + LdapUtils.areEqual(authenticationCertificate, v.authenticationCertificate) && + LdapUtils.areEqual(authenticationKey, v.authenticationKey); + } + return false; + } + + @Override + public int hashCode() { + return LdapUtils.computeHashCode(HASH_CODE_SEED, trustCertificates, authenticationCertificate, authenticationKey); + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustCertificates=" + trustCertificates + ", " + + "authenticationCertificate=" + authenticationCertificate + ", " + + "authenticationKey=" + authenticationKey + "]"; + } + + // CheckStyle:OFF + public static class Builder { + + + private final X509CredentialConfig object = new X509CredentialConfig(); + + + protected Builder() { + } + + + public Builder trustCertificates(final String certificates) { + object.setTrustCertificates(certificates); + return this; + } + + + public Builder authenticationCertificate(final String certificate) { + object.setAuthenticationCertificate(certificate); + return this; + } + + + public Builder authenticationKey(final String key) { + object.setAuthenticationKey(key); + return this; + } + + + public X509CredentialConfig build() { + return object; + } + } + // CheckStyle:ON +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509DnDecoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509DnDecoder.java new file mode 100644 index 0000000..4190a3e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509DnDecoder.java @@ -0,0 +1,77 @@ + +package org.xbib.net.ldap.ssl; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.asn1.OidType; +import org.xbib.asn1.UniversalDERTag; +import org.xbib.net.ldap.dn.Dn; +import org.xbib.net.ldap.dn.NameValue; +import org.xbib.net.ldap.dn.RDn; + +/** + * Utility class for decoding the DER data in an X509 DN. + * + */ +public class X509DnDecoder implements Function { + + /** + * DER path for RDN parsing. + */ + private static final DERPath RDN_PATH = new DERPath("/SEQ/SET"); + + /** + * DER path for parsing attribute value assertion. + */ + private static final DERPath ASSERTION_PATH = new DERPath("/SEQ"); + + /** + * Converts bytes in the buffer to attribute value assertions by reading from the current position to the limit. + * + * @param encoded buffer containing DER-encoded data where the buffer is positioned at the tag of the oid and the + * limit is set beyond the last byte of attribute value data. + * @return decoded bytes as attribute value assertions + */ + private static List decode(final DERBuffer encoded) { + final List nameValues = new ArrayList<>(); + final DERParser parser = new DERParser(); + parser.registerHandler( + ASSERTION_PATH, + (p, e) -> { + if (UniversalDERTag.OID.getTagNo() != p.readTag(e).getTagNo()) { + throw new IllegalArgumentException("Expected OID tag"); + } + + final int seqLimit = e.limit(); + final int oidLength = p.readLength(e); + e.limit(e.position() + oidLength); + + final String oid = OidType.decode(e); + e.limit(seqLimit); + + p.readTag(e); + p.readLength(e); + nameValues.add(new NameValue(oid, e.getRemainingBytes())); + }); + parser.parse(encoded); + return nameValues; + } + + @Override + public Dn apply(final DERBuffer encoded) { + final List rdns = new ArrayList<>(); + final DERParser parser = new DERParser(); + parser.registerHandler( + RDN_PATH, + (p, e) -> { + rdns.add(new RDn(decode(e))); + e.position(e.limit()); + }); + parser.parse(encoded); + return new Dn(rdns); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509ExtendedTrustManagerWrapper.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509ExtendedTrustManagerWrapper.java new file mode 100644 index 0000000..24dab37 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509ExtendedTrustManagerWrapper.java @@ -0,0 +1,133 @@ + +package org.xbib.net.ldap.ssl; + +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Wraps an {@link X509TrustManager} in order to provide hostname verification. + * + */ +public class X509ExtendedTrustManagerWrapper extends X509ExtendedTrustManager { + + /** + * Trust manager. + */ + private final X509TrustManager trustManager; + + /** + * Hostname verifier. + */ + private final CertificateHostnameVerifier hostnameVerifier; + + + /** + * Creates a new X509 extended trust manager wrapper. + * + * @param manager to wrap + * @param verifier to verify hostname + */ + public X509ExtendedTrustManagerWrapper(final X509TrustManager manager, final CertificateHostnameVerifier verifier) { + trustManager = manager; + hostnameVerifier = verifier; + } + + + /** + * Resolves a hostname from the supplied session and invokes {@link #hostnameVerifier}. + * + * @param session to extract hostname from + * @param cert to verify hostname against + * @throws CertificateException if the hostname cannot be verified + */ + protected void verifyHostname(final SSLSession session, final X509Certificate cert) + throws CertificateException { + final HostnameResolver resolver = new HostnameResolver(session); + final String hostname = resolver.resolve(); + if (!hostnameVerifier.verify(hostname, cert)) { + throw new CertificateException("Hostname verification failed for " + hostname + " using " + hostnameVerifier); + } + } + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType, final Socket socket) + throws CertificateException { + if (trustManager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) trustManager).checkClientTrusted(chain, authType, socket); + } else { + trustManager.checkClientTrusted(chain, authType); + if (socket != null && socket.isConnected() && socket instanceof SSLSocket) { + verifyHostname(((SSLSocket) socket).getHandshakeSession(), chain[0]); + } else { + throw new CertificateException("Could not retrieve SSL session from socket"); + } + } + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType, final Socket socket) + throws CertificateException { + if (trustManager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) trustManager).checkServerTrusted(chain, authType, socket); + } else { + trustManager.checkServerTrusted(chain, authType); + if (socket != null && socket.isConnected() && socket instanceof SSLSocket) { + verifyHostname(((SSLSocket) socket).getHandshakeSession(), chain[0]); + } else { + throw new CertificateException("Could not retrieve SSL session from socket"); + } + } + } + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType, final SSLEngine engine) + throws CertificateException { + if (trustManager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) trustManager).checkClientTrusted(chain, authType, engine); + } else { + trustManager.checkClientTrusted(chain, authType); + verifyHostname(engine.getHandshakeSession(), chain[0]); + } + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType, final SSLEngine engine) + throws CertificateException { + if (trustManager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) trustManager).checkServerTrusted(chain, authType, engine); + } else { + trustManager.checkServerTrusted(chain, authType); + verifyHostname(engine.getHandshakeSession(), chain[0]); + } + } + + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + trustManager.checkClientTrusted(chain, authType); + } + + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + trustManager.checkServerTrusted(chain, authType); + } + + + @Override + public X509Certificate[] getAcceptedIssuers() { + return trustManager.getAcceptedIssuers(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509SSLContextInitializer.java b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509SSLContextInitializer.java new file mode 100644 index 0000000..ec9307b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/ssl/X509SSLContextInitializer.java @@ -0,0 +1,165 @@ + +package org.xbib.net.ldap.ssl; + +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +/** + * Provides an SSL context initializer which can use X.509 certificates to create key and trust managers. + * + */ +public class X509SSLContextInitializer extends AbstractSSLContextInitializer { + + /** + * Certificates used to create trust managers. + */ + private X509Certificate[] trustCerts; + + /** + * Certificate used to create key managers. + */ + private X509Certificate authenticationCert; + + /** + * Private key used to create key managers. + */ + private PrivateKey authenticationKey; + + + /** + * Returns the certificates to use for creating the trust managers. + * + * @return X.509 certificates + */ + public X509Certificate[] getTrustCertificates() { + return trustCerts; + } + + + /** + * Sets the certificates to use for creating the trust managers. + * + * @param certs X.509 certificates + */ + public void setTrustCertificates(final X509Certificate... certs) { + trustCerts = certs; + } + + + /** + * Returns the certificate to use for creating the key managers. + * + * @return X.509 certificate + */ + public X509Certificate getAuthenticationCertificate() { + return authenticationCert; + } + + + /** + * Sets the certificate to use for creating the key managers. + * + * @param cert X.509 certificate + */ + public void setAuthenticationCertificate(final X509Certificate cert) { + authenticationCert = cert; + } + + + /** + * Returns the private key associated with the authentication certificate. + * + * @return private key + */ + public PrivateKey getAuthenticationKey() { + return authenticationKey; + } + + + /** + * Sets the private key associated with the authentication certificate. + * + * @param key private key + */ + public void setAuthenticationKey(final PrivateKey key) { + authenticationKey = key; + } + + + @Override + protected TrustManager[] createTrustManagers() + throws GeneralSecurityException { + TrustManager[] tm = null; + if (trustCerts != null && trustCerts.length > 0) { + final TrustManagerFactory tmf = getTrustManagerFactory(trustCerts); + tm = tmf.getTrustManagers(); + } + return tm; + } + + + /** + * Creates a new trust manager factory. + * + * @param certs to add as trusted material + * @return trust manager factory + * @throws GeneralSecurityException if the trust manager factory cannot be initialized + */ + protected TrustManagerFactory getTrustManagerFactory(final X509Certificate[] certs) + throws GeneralSecurityException { + final KeyStore ks = KeyStoreUtils.newInstance(); + KeyStoreUtils.setCertificateEntry("ldap_trust_", ks, certs); + + final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + return tmf; + } + + + @Override + public KeyManager[] getKeyManagers() + throws GeneralSecurityException { + KeyManager[] km = null; + if (authenticationCert != null && authenticationKey != null) { + final KeyManagerFactory kmf = getKeyManagerFactory(authenticationCert, authenticationKey); + km = kmf.getKeyManagers(); + } + return km; + } + + + /** + * Creates a new key manager factory. + * + * @param cert to initialize the key manager factory + * @param key to initialize the key manager factory + * @return key manager factory + * @throws GeneralSecurityException if the key manager factory cannot be initialized + */ + protected KeyManagerFactory getKeyManagerFactory(final X509Certificate cert, final PrivateKey key) + throws GeneralSecurityException { + final KeyStore ks = KeyStoreUtils.newInstance(); + KeyStoreUtils.setKeyEntry("ldap_client_auth", ks, "changeit".toCharArray(), key, cert); + + final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, "changeit".toCharArray()); + return kmf; + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "trustManagers=" + Arrays.toString(trustManagers) + ", " + + "trustCerts=" + Arrays.toString(trustCerts) + ", " + + "authenticationCert=" + authenticationCert + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/templates/Query.java b/net-ldap/src/main/java/org/xbib/net/ldap/templates/Query.java new file mode 100644 index 0000000..56fbe5a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/templates/Query.java @@ -0,0 +1,157 @@ + +package org.xbib.net.ldap.templates; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.StringTokenizer; +import org.xbib.net.ldap.LdapUtils; + +/** + * Contains data associated with a query request. + * + */ +public class Query { + + /** + * Used for setting empty terms. + */ + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + /** + * Query separated into terms. + */ + private final String[] terms; + + /** + * Attributes to return with the ldap query. + */ + private String[] returnAttributes; + + /** + * Additional restrictions to place on every query. + */ + private String searchRestrictions; + + /** + * Start index of search results to return. + */ + private Integer fromResult; + + /** + * End index of search results to return. + */ + private Integer toResult; + + + /** + * Parses the query from a string into query terms. + * + * @param query to parse + */ + public Query(final String query) { + if (query != null) { + final List l = new ArrayList<>(); + final StringTokenizer queryTokens = new StringTokenizer(LdapUtils.toLowerCase(query).trim()); + while (queryTokens.hasMoreTokens()) { + l.add(queryTokens.nextToken()); + } + terms = l.toArray(new String[0]); + } else { + terms = EMPTY_STRING_ARRAY; + } + } + + + /** + * Returns the terms. + * + * @return query terms + */ + public String[] getTerms() { + return terms; + } + + /** + * Returns the return attributes. + * + * @return return attributes + */ + public String[] getReturnAttributes() { + return returnAttributes; + } + + /** + * Sets the return attributes. + * + * @param attrs return attributes + */ + public void setReturnAttributes(final String[] attrs) { + returnAttributes = attrs; + } + + /** + * Returns the search restrictions. + * + * @return search restrictions + */ + public String getSearchRestrictions() { + return searchRestrictions; + } + + /** + * Sets the search restrictions. + * + * @param restrictions search restrictions + */ + public void setSearchRestrictions(final String restrictions) { + searchRestrictions = restrictions; + } + + /** + * Returns the from result. + * + * @return from result + */ + public Integer getFromResult() { + return fromResult; + } + + /** + * Sets the index of the result to begin searching. + * + * @param i from index + */ + public void setFromResult(final Integer i) { + fromResult = i; + } + + /** + * Returns the to result. + * + * @return to result + */ + public Integer getToResult() { + return toResult; + } + + /** + * Sets the index of the result to stop searching. + * + * @param i to result + */ + public void setToResult(final Integer i) { + toResult = i; + } + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "terms=" + Arrays.toString(terms) + ", " + + "returnAttributes=" + Arrays.toString(returnAttributes) + ", " + + "searchRestrictions=" + searchRestrictions + ", " + + "fromResult=" + fromResult + ", " + + "toResult=" + toResult + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/templates/SearchTemplates.java b/net-ldap/src/main/java/org/xbib/net/ldap/templates/SearchTemplates.java new file mode 100644 index 0000000..724bcfc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/templates/SearchTemplates.java @@ -0,0 +1,227 @@ + +package org.xbib.net.ldap.templates; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.xbib.net.ldap.FilterTemplate; + +/** + * Contains a list of common search filter templates that can be formatted for any given query. + * + */ +public class SearchTemplates { + + /** + * Search filter templates. + */ + private final String[] filterTemplates; + + /** + * Appended to every search filter to restrict results. + */ + private String searchRestrictions; + + /** + * Term parsers for creating filter parameters. + */ + private TermParser[] termParsers = new TermParser[]{ + new DefaultTermParser(), + new InitialTermParser(), + }; + + + /** + * Creates a new search templates. + * + * @param templates list of search filters + */ + public SearchTemplates(final String... templates) { + filterTemplates = templates; + } + + + /** + * Returns the filter to use for search restrictions. + * + * @return search restrictions + */ + public String getSearchRestrictions() { + return searchRestrictions; + } + + + /** + * Sets the filter to use for search restrictions. + * + * @param restrictions search restrictions + */ + public void setSearchRestrictions(final String restrictions) { + searchRestrictions = restrictions; + } + + + /** + * Returns the term parsers used for creating filter parameters. + * + * @return term parsers + */ + public TermParser[] getTermParsers() { + return termParsers; + } + + + /** + * Sets the term parsers used for creating filter parameters. + * + * @param parsers term parsers + */ + public void setTermParsers(final TermParser... parsers) { + termParsers = parsers; + } + + + /** + * Creates the filter templates using configured templates and the supplied query. + * + * @param query to create search filter with + * @return filter templates + */ + public FilterTemplate[] format(final Query query) { + final List templates = new ArrayList<>(filterTemplates.length); + for (String template : filterTemplates) { + final FilterTemplate filter = new FilterTemplate( + concatFilters(template, query.getSearchRestrictions(), searchRestrictions)); + for (TermParser parser : termParsers) { + for (Map.Entry e : parser.parse(query.getTerms()).entrySet()) { + filter.setParameter(e.getKey(), e.getValue()); + } + } + templates.add(filter); + } + return templates.toArray(new FilterTemplate[0]); + } + + + /** + * Concatenates the supplied filters into a single filter will all arguments ANDED together. Null array values are + * ignored. + * + * @param filters to concatenate + * @return search filter + */ + private String concatFilters(final String... filters) { + final List nonNullFilters = new ArrayList<>(filters.length); + for (String s : filters) { + if (s != null) { + nonNullFilters.add(s); + } + } + if (nonNullFilters.size() > 1) { + final StringBuilder sb = new StringBuilder("(&"); + nonNullFilters.forEach(sb::append); + sb.append(")"); + return sb.toString(); + } else { + return nonNullFilters.get(0); + } + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "filterTemplates=" + Arrays.toString(filterTemplates) + ", " + + "searchRestrictions=" + searchRestrictions + ", " + + "termParsers=" + Arrays.toString(termParsers) + "]"; + } + + + /** + * Converts query terms into search filter parameters. + */ + public interface TermParser { + + + /** + * Returns search filter parameters for the supplied query terms. + * + * @param terms to parse + * @return search filter parameters + */ + Map parse(String[] terms); + } + + + /** + * Adds each term as a filter parameter using the name 'termX' where X is the index of the term. For the argument: + * {'fname', 'lname' }, produces: + * + *
+     * {
+     * 'term1' => 'fname',
+     * 'term2' => 'lname',
+     * }
+     * 
+ */ + public static class DefaultTermParser implements TermParser { + + + @Override + public Map parse(final String[] terms) { + final Map filterParams = new HashMap<>(terms.length); + for (int i = 1; i <= terms.length; i++) { + filterParams.put("term" + i, terms[i - 1]); + } + return filterParams; + } + } + + + /** + * Adds the first letter of each term as a filter parameter using the name 'initialX' where X is the index of the + * term. For the argument: {'fname', 'lname' }, produces: + * + *
+     * {
+     * 'initial1' => 'f',
+     * 'initial2' => 'l',
+     * }
+     * 
+ */ + public static class InitialTermParser implements TermParser { + + + @Override + public Map parse(final String[] terms) { + final Map filterParams = new HashMap<>(terms.length); + final String[] initialParams = getInitials(terms); + for (int i = 1; i <= initialParams.length; i++) { + filterParams.put("initial" + i, initialParams[i - 1]); + } + return filterParams; + } + + + /** + * This converts an array of names into an array of initials. + * + * @param names to convert to initials + * @return initials + */ + private String[] getInitials(final String[] names) { + final String[] initials = new String[names.length]; + for (int i = 0; i < initials.length; i++) { + if (names[i] != null && names[i].length() > 0) { + initials[i] = names[i].substring(0, 1); + } else { + initials[i] = null; + } + } + return initials; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/templates/SearchTemplatesOperation.java b/net-ldap/src/main/java/org/xbib/net/ldap/templates/SearchTemplatesOperation.java new file mode 100644 index 0000000..b0fa384 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/templates/SearchTemplatesOperation.java @@ -0,0 +1,183 @@ + +package org.xbib.net.ldap.templates; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import org.xbib.net.ldap.FilterTemplate; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.concurrent.SearchOperationWorker; + +/** + * Searches an LDAP using a defined set of search templates. For each term count some number of templates are defined + * and used for searching. + * + */ +public class SearchTemplatesOperation { + + /** + * Search executor. + */ + private SearchOperationWorker searchOperationWorker; + + /** + * Search templates. + */ + private SearchTemplates[] searchTemplates; + + + /** + * Default constructor. + */ + public SearchTemplatesOperation() { + } + + + /** + * Creates a new search templates operation. + * + * @param worker search operation worker + * @param templates search templates + */ + public SearchTemplatesOperation(final SearchOperationWorker worker, final SearchTemplates... templates) { + searchOperationWorker = worker; + searchTemplates = templates; + } + + + /** + * Returns the search operation worker. + * + * @return search operation worker + */ + public SearchOperationWorker getSearchOperationWorker() { + return searchOperationWorker; + } + + + /** + * Sets the search operation worker. + * + * @param worker search operation worker + */ + public void setSearchOperationWorker(final SearchOperationWorker worker) { + searchOperationWorker = worker; + } + + + /** + * Returns the search templates. + * + * @return search templates + */ + public SearchTemplates[] getSearchTemplates() { + return searchTemplates; + } + + + /** + * Sets the search templates. + * + * @param templates search templates + */ + public void setSearchTemplates(final SearchTemplates[] templates) { + searchTemplates = templates; + } + + + /** + * Applies the supplied query to a search templates and aggregates all results into a single search result. + * + * @param query to execute + * @return ldap result + */ + public SearchResponse execute(final Query query) { + SearchTemplates templates = null; + if (query.getTerms().length > 0) { + + int termCount = query.getTerms().length; + // if term count exceeds the highest defined templates + // use the highest set of templates available + if (termCount > searchTemplates.length) { + termCount = searchTemplates.length; + } + if (termCount > 0) { + templates = searchTemplates[termCount - 1]; + if (templates != null) { + // + } else { + // + } + } else { + // + } + } + + if (templates != null) { + return execute(templates.format(query), query.getReturnAttributes(), query.getFromResult(), query.getToResult()); + } else { + return null; + } + } + + + /** + * Performs an LDAP search with the supplied templates and aggregates all the search results together. + * + * @param templates to execute + * @param returnAttrs attributes to return from the search + * @param fromResult index to return results from + * @param toResult index to return results to + * @return ldap result containing all results + */ + protected SearchResponse execute( + final FilterTemplate[] templates, + final String[] returnAttrs, + final Integer fromResult, + final Integer toResult) { + // perform searches + final Collection responses = searchOperationWorker.execute(templates, returnAttrs); + + // iterate over all results and store each entry + final SearchResponse result = new SearchResponse(); + for (SearchResponse res : responses) { + for (LdapEntry entry : res.getEntries()) { + if (result.getEntries().stream().noneMatch(e -> entry.getParsedDn().equals(e.getParsedDn()))) { + result.addEntries(entry); + } else { + // + } + } + } + + final SearchResponse subResult; + if (fromResult != null) { + subResult = result.subResult(fromResult, Objects.requireNonNullElseGet(toResult, result::entrySize)); + } else if (toResult != null) { + subResult = result.subResult(0, toResult); + } else { + subResult = result; + } + return subResult; + } + + + /** + * Closes any resources associated with this object. + */ + public void close() { + if (searchOperationWorker != null) { + searchOperationWorker.getOperation().getConnectionFactory().close(); + } + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "searchOperationWorker=" + searchOperationWorker + ", " + + "searchTemplates=" + Arrays.toString(searchTemplates) + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractBinaryValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractBinaryValueTranscoder.java new file mode 100644 index 0000000..42e545b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractBinaryValueTranscoder.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.transcode; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Value transcoder which decodes and encodes to a byte array and therefore the string methods simply delegate to the + * binary methods. + * + * @param type of object to transcode + */ +public abstract class AbstractBinaryValueTranscoder implements ValueTranscoder { + + + @Override + public T decodeStringValue(final String value) { + return decodeBinaryValue(LdapUtils.utf8Encode(value)); + } + + + @Override + public String encodeStringValue(final T value) { + return LdapUtils.utf8Encode(encodeBinaryValue(value)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractPrimitiveValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractPrimitiveValueTranscoder.java new file mode 100644 index 0000000..fffebcf --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractPrimitiveValueTranscoder.java @@ -0,0 +1,41 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Base class for primitive value transcoders. + * + * @param type of object to transcode + */ +public abstract class AbstractPrimitiveValueTranscoder extends AbstractStringValueTranscoder { + + /** + * Whether this transcoder operates on a primitive or an object. + */ + private boolean primitive; + + + /** + * Returns whether this transcoder operates on a primitive value. + * + * @return whether this transcoder operates on a primitive value + */ + public boolean isPrimitive() { + return primitive; + } + + + /** + * Sets whether this transcoder operates on a primitive value. + * + * @param b whether this transcoder operates on a primitive value + */ + public void setPrimitive(final boolean b) { + primitive = b; + } + + + @Override + public String encodeStringValue(final T value) { + return value.toString(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractStringValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractStringValueTranscoder.java new file mode 100644 index 0000000..8c0dd8b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/AbstractStringValueTranscoder.java @@ -0,0 +1,25 @@ + +package org.xbib.net.ldap.transcode; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Value transcoder which decodes and encodes to a String and therefore the binary methods simply delegate to the string + * methods. + * + * @param type of object to transcode + */ +public abstract class AbstractStringValueTranscoder implements ValueTranscoder { + + + @Override + public T decodeBinaryValue(final byte[] value) { + return decodeStringValue(LdapUtils.utf8Encode(value)); + } + + + @Override + public byte[] encodeBinaryValue(final T value) { + return LdapUtils.utf8Encode(encodeStringValue(value)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/BooleanValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/BooleanValueTranscoder.java new file mode 100644 index 0000000..d7a25cc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/BooleanValueTranscoder.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes a boolean for use in an ldap attribute value. + * + */ +public class BooleanValueTranscoder extends AbstractPrimitiveValueTranscoder { + + + /** + * Default constructor. + */ + public BooleanValueTranscoder() { + } + + + /** + * Creates a new boolean value transcoder. + * + * @param b whether this transcoder is operating on a primitive + */ + public BooleanValueTranscoder(final boolean b) { + setPrimitive(b); + } + + + @Override + public Boolean decodeStringValue(final String value) { + return Boolean.valueOf(value); + } + + + @Override + public Class getType() { + return isPrimitive() ? boolean.class : Boolean.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ByteArrayValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ByteArrayValueTranscoder.java new file mode 100644 index 0000000..8f9b359 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ByteArrayValueTranscoder.java @@ -0,0 +1,27 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes a byte array for use in an ldap attribute value. + * + */ +public class ByteArrayValueTranscoder extends AbstractBinaryValueTranscoder { + + + @Override + public byte[] decodeBinaryValue(final byte[] value) { + return value; + } + + + @Override + public byte[] encodeBinaryValue(final byte[] value) { + return value; + } + + + @Override + public Class getType() { + return byte[].class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/CertificateValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/CertificateValueTranscoder.java new file mode 100644 index 0000000..f3bdf2a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/CertificateValueTranscoder.java @@ -0,0 +1,65 @@ + +package org.xbib.net.ldap.transcode; + +import java.io.ByteArrayInputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import org.xbib.net.ldap.LdapUtils; + +/** + * Decodes and encodes a certificate for use in an ldap attribute value. + * + */ +public class CertificateValueTranscoder implements ValueTranscoder { + + /** + * PEM cert header. + */ + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----" + System.getProperty("line.separator"); + + /** + * PEM cert footer. + */ + private static final String END_CERT = System.getProperty("line.separator") + "-----END CERTIFICATE-----"; + + + @Override + public Certificate decodeStringValue(final String value) { + return decodeBinaryValue(LdapUtils.utf8Encode(value)); + } + + + @Override + public Certificate decodeBinaryValue(final byte[] value) { + try { + final CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return cf.generateCertificate(new ByteArrayInputStream(value)); + } catch (CertificateException e) { + throw new IllegalArgumentException("Attribute value could not be decoded as a certificate", e); + } + } + + + @Override + public String encodeStringValue(final Certificate value) { + return BEGIN_CERT + LdapUtils.base64Encode(encodeBinaryValue(value)) + END_CERT; + } + + + @Override + public byte[] encodeBinaryValue(final Certificate value) { + try { + return value.getEncoded(); + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Certificate could not be encoded", e); + } + } + + + @Override + public Class getType() { + return Certificate.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/CharArrayValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/CharArrayValueTranscoder.java new file mode 100644 index 0000000..2849f77 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/CharArrayValueTranscoder.java @@ -0,0 +1,27 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes a character array for use in an ldap attribute value. + * + */ +public class CharArrayValueTranscoder extends AbstractStringValueTranscoder { + + + @Override + public char[] decodeStringValue(final String value) { + return value.toCharArray(); + } + + + @Override + public String encodeStringValue(final char[] value) { + return String.valueOf(value); + } + + + @Override + public Class getType() { + return char[].class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/DoubleValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/DoubleValueTranscoder.java new file mode 100644 index 0000000..4876a87 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/DoubleValueTranscoder.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes a double for use in an ldap attribute value. + * + */ +public class DoubleValueTranscoder extends AbstractPrimitiveValueTranscoder { + + + /** + * Default constructor. + */ + public DoubleValueTranscoder() { + } + + + /** + * Creates a new double value transcoder. + * + * @param b whether this transcoder is operating on a primitive + */ + public DoubleValueTranscoder(final boolean b) { + setPrimitive(b); + } + + + @Override + public Double decodeStringValue(final String value) { + return Double.valueOf(value); + } + + + @Override + public Class getType() { + return isPrimitive() ? double.class : Double.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/FloatValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/FloatValueTranscoder.java new file mode 100644 index 0000000..2cf6dbd --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/FloatValueTranscoder.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes a float for use in an ldap attribute value. + * + */ +public class FloatValueTranscoder extends AbstractPrimitiveValueTranscoder { + + + /** + * Default constructor. + */ + public FloatValueTranscoder() { + } + + + /** + * Creates a new float value transcoder. + * + * @param b whether this transcoder is operating on a primitive + */ + public FloatValueTranscoder(final boolean b) { + setPrimitive(b); + } + + + @Override + public Float decodeStringValue(final String value) { + return Float.valueOf(value); + } + + + @Override + public Class getType() { + return isPrimitive() ? float.class : Float.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/GeneralizedTimeValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/GeneralizedTimeValueTranscoder.java new file mode 100644 index 0000000..2409fda --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/GeneralizedTimeValueTranscoder.java @@ -0,0 +1,206 @@ + +package org.xbib.net.ldap.transcode; + +import java.text.ParseException; +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Decodes and encodes a generalized time for use in an ldap attribute value. See + * http://tools.ietf.org/html/rfc4517#section-3.3.13 + * + */ +public class GeneralizedTimeValueTranscoder extends AbstractStringValueTranscoder { + + /** + * Pattern for capturing the year in generalized time. + */ + private static final String YEAR_PATTERN = "(\\d{4})"; + + /** + * Pattern for capturing the month in generalized time. + */ + private static final String MONTH_PATTERN = "((?:\\x30[\\x31-\\x39])|(?:\\x31[\\x30-\\x32]))"; + + /** + * Pattern for capturing the day in generalized time. + */ + private static final String DAY_PATTERN = "((?:\\x30[\\x31-\\x39])" + + "|(?:[\\x31-\\x32][\\x30-\\x39])" + + "|(?:\\x33[\\x30-\\x31]))"; + + /** + * Pattern for capturing hours in generalized time. + */ + private static final String HOUR_PATTERN = "((?:[\\x30-\\x31][\\x30-\\x39])|(?:\\x32[\\x30-\\x33]))"; + + /** + * Pattern for capturing optional minutes in generalized time. + */ + private static final String MIN_PATTERN = "([\\x30-\\x35][\\x30-\\x39])?"; + + /** + * Pattern for capturing optional seconds in generalized time. + */ + private static final String SECOND_PATTERN = "([\\x30-\\x35][\\x30-\\x39])?"; + + /** + * Pattern for capturing optional fraction in generalized time. + */ + private static final String FRACTION_PATTERN = "([,.](\\d+))?"; + + /** + * Pattern for capturing timezone in generalized time. + */ + private static final String TIMEZONE_PATTERN = "(Z|(?:[+-]" + HOUR_PATTERN + MIN_PATTERN + "))"; + + /** + * Generalized time format regular expression. + */ + private static final Pattern TIME_REGEX = Pattern.compile( + YEAR_PATTERN + MONTH_PATTERN + DAY_PATTERN + HOUR_PATTERN + MIN_PATTERN + SECOND_PATTERN + FRACTION_PATTERN + + TIMEZONE_PATTERN); + + /** + * Date format. + */ + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss.SSS'Z'"); + + @Override + public ZonedDateTime decodeStringValue(final String value) { + try { + return parseGeneralizedTime(value); + } catch (ParseException | DateTimeException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public String encodeStringValue(final ZonedDateTime value) { + if (value.getZone().normalized().equals(ZoneOffset.UTC)) { + return value.format(DATE_FORMAT); + } else { + return value.withZoneSameInstant(ZoneOffset.UTC).format(DATE_FORMAT); + } + } + + @Override + public Class getType() { + return ZonedDateTime.class; + } + + /** + * Parses the supplied value and returns a date time. + * + * @param value of generalized time to parse + * @return date time initialized to the correct time + * @throws ParseException if the value does not contain correct generalized time syntax + */ + protected ZonedDateTime parseGeneralizedTime(final String value) + throws ParseException { + if (value == null) { + throw new IllegalArgumentException("String to parse cannot be null."); + } + + final Matcher m = TIME_REGEX.matcher(value); + if (!m.matches()) { + throw new ParseException("Invalid generalized time string.", value.length()); + } + + // CheckStyle:MagicNumber OFF + final ZoneId zoneId; + final String tzString = m.group(9); + if ("Z".equals(tzString)) { + zoneId = ZoneOffset.UTC; + } else { + zoneId = ZoneId.of("GMT" + tzString); + } + + // Set required time fields + final int year = Integer.parseInt(m.group(1)); + final int month = Integer.parseInt(m.group(2)); + final int dayOfMonth = Integer.parseInt(m.group(3)); + final int hour = Integer.parseInt(m.group(4)); + + FractionalPart fraction = FractionalPart.Hours; + + // Set optional minutes + int minutes = 0; + if (m.group(5) != null) { + fraction = FractionalPart.Minutes; + minutes = Integer.parseInt(m.group(5)); + } + + // Set optional seconds + int seconds = 0; + if (m.group(6) != null) { + fraction = FractionalPart.Seconds; + seconds = Integer.parseInt(m.group(6)); + } + + // Set optional fractional part + int millis = 0; + if (m.group(7) != null) { + millis = fraction.toMillis(m.group(8)); + } + // CheckStyle:MagicNumber ON + + return ZonedDateTime.of( + LocalDateTime.of(year, month, dayOfMonth, hour, minutes, seconds).plus(millis, ChronoUnit.MILLIS), zoneId); + } + + + /** + * Describes the fractional part of a generalized time string. + */ + private enum FractionalPart { + + /** + * Fractional hours. + */ + Hours(3600000), + + /** + * Fractional minutes. + */ + Minutes(60000), + + /** + * Fractional seconds. + */ + Seconds(1000); + + /** + * Scale factor to convert units to millis. + */ + private final int scaleFactor; + + + /** + * Creates a new fractional part. + * + * @param scale scale factor. + */ + FractionalPart(final int scale) { + scaleFactor = scale; + } + + + /** + * Converts the given fractional date part to milliseconds. + * + * @param fraction digits of fractional date part + * @return fraction converted to milliseconds. + */ + int toMillis(final String fraction) { + return (int) (Double.parseDouble('.' + fraction) * scaleFactor); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/IntegerValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/IntegerValueTranscoder.java new file mode 100644 index 0000000..6a3b9d7 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/IntegerValueTranscoder.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes an integer for use in an ldap attribute value. + * + */ +public class IntegerValueTranscoder extends AbstractPrimitiveValueTranscoder { + + + /** + * Default constructor. + */ + public IntegerValueTranscoder() { + } + + + /** + * Creates a new integer value transcoder. + * + * @param b whether this transcoder is operating on a primitive + */ + public IntegerValueTranscoder(final boolean b) { + setPrimitive(b); + } + + + @Override + public Integer decodeStringValue(final String value) { + return Integer.valueOf(value); + } + + + @Override + public Class getType() { + return isPrimitive() ? int.class : Integer.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/LongValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/LongValueTranscoder.java new file mode 100644 index 0000000..30f6b3c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/LongValueTranscoder.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes a long for use in an ldap attribute value. + * + */ +public class LongValueTranscoder extends AbstractPrimitiveValueTranscoder { + + + /** + * Default constructor. + */ + public LongValueTranscoder() { + } + + + /** + * Creates a new long value transcoder. + * + * @param b whether this transcoder is operating on a primitive + */ + public LongValueTranscoder(final boolean b) { + setPrimitive(b); + } + + + @Override + public Long decodeStringValue(final String value) { + return Long.valueOf(value); + } + + + @Override + public Class getType() { + return isPrimitive() ? long.class : Long.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ObjectValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ObjectValueTranscoder.java new file mode 100644 index 0000000..2349c5b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ObjectValueTranscoder.java @@ -0,0 +1,41 @@ + +package org.xbib.net.ldap.transcode; + +import org.xbib.net.ldap.LdapUtils; + +/** + * Decodes and encodes an object for use in an ldap attribute value. + * + */ +public class ObjectValueTranscoder implements ValueTranscoder { + + + @Override + public Object decodeStringValue(final String value) { + return value; + } + + + @Override + public Object decodeBinaryValue(final byte[] value) { + return value; + } + + + @Override + public String encodeStringValue(final Object value) { + return value.toString(); + } + + + @Override + public byte[] encodeBinaryValue(final Object value) { + return LdapUtils.utf8Encode(encodeStringValue(value)); + } + + + @Override + public Class getType() { + return Object.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ShortValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ShortValueTranscoder.java new file mode 100644 index 0000000..e22b4ee --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ShortValueTranscoder.java @@ -0,0 +1,38 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes a short for use in an ldap attribute value. + * + */ +public class ShortValueTranscoder extends AbstractPrimitiveValueTranscoder { + + + /** + * Default constructor. + */ + public ShortValueTranscoder() { + } + + + /** + * Creates a new short value transcoder. + * + * @param b whether this transcoder is operating on a primitive + */ + public ShortValueTranscoder(final boolean b) { + setPrimitive(b); + } + + + @Override + public Short decodeStringValue(final String value) { + return Short.valueOf(value); + } + + + @Override + public Class getType() { + return isPrimitive() ? short.class : Short.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/StringValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/StringValueTranscoder.java new file mode 100644 index 0000000..7b21904 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/StringValueTranscoder.java @@ -0,0 +1,27 @@ + +package org.xbib.net.ldap.transcode; + +/** + * Decodes and encodes a string for use in an ldap attribute value. + * + */ +public class StringValueTranscoder extends AbstractStringValueTranscoder { + + + @Override + public String decodeStringValue(final String value) { + return value; + } + + + @Override + public String encodeStringValue(final String value) { + return value; + } + + + @Override + public Class getType() { + return String.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/UUIDValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/UUIDValueTranscoder.java new file mode 100644 index 0000000..c8e32e0 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/UUIDValueTranscoder.java @@ -0,0 +1,29 @@ + +package org.xbib.net.ldap.transcode; + +import java.util.UUID; + +/** + * Decodes and encodes a UUID for use in an ldap attribute value. + * + */ +public class UUIDValueTranscoder extends AbstractStringValueTranscoder { + + + @Override + public UUID decodeStringValue(final String value) { + return UUID.fromString(value); + } + + + @Override + public String encodeStringValue(final UUID value) { + return value.toString(); + } + + + @Override + public Class getType() { + return UUID.class; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ValueTranscoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ValueTranscoder.java new file mode 100644 index 0000000..c412c25 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transcode/ValueTranscoder.java @@ -0,0 +1,76 @@ + +package org.xbib.net.ldap.transcode; + +import java.util.function.Function; + +/** + * Interface for decoding and encoding custom types for ldap attribute values. + * + * @param type of value + */ +public interface ValueTranscoder { + + + /** + * Decodes the supplied ldap attribute value into a custom type. + * + * @param value to decode + * @return decoded value + */ + T decodeStringValue(String value); + + + /** + * Decodes the supplied ldap attribute value into a custom type. + * + * @param value to decode + * @return decoded value + */ + T decodeBinaryValue(byte[] value); + + + /** + * Encodes the supplied value into an ldap attribute value. + * + * @param value to encode + * @return encoded value + */ + String encodeStringValue(T value); + + + /** + * Encodes the supplied value into an ldap attribute value. + * + * @param value to encode + * @return encoded value + */ + byte[] encodeBinaryValue(T value); + + + /** + * Returns the type produced by this value transcoder. + * + * @return type produced by this value transcoder + */ + Class getType(); + + + /** + * Functional implementation. + * + * @return decoder function + */ + default Function decoder() { + return this::decodeBinaryValue; + } + + + /** + * Functional implementation. + * + * @return encoder function + */ + default Function encoder() { + return this::encodeBinaryValue; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultCompareOperationHandle.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultCompareOperationHandle.java new file mode 100644 index 0000000..dce4d40 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultCompareOperationHandle.java @@ -0,0 +1,161 @@ + +package org.xbib.net.ldap.transport; + +import java.time.Duration; +import java.util.Arrays; +import org.xbib.net.ldap.CompareOperationHandle; +import org.xbib.net.ldap.CompareRequest; +import org.xbib.net.ldap.CompareResponse; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.handler.CompareValueHandler; +import org.xbib.net.ldap.handler.CompleteHandler; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Handle that notifies on the components of a compare request. + * + */ +public class DefaultCompareOperationHandle + extends DefaultOperationHandle implements CompareOperationHandle { + + /** + * Functions to handle the compare result. + */ + private CompareValueHandler[] onCompare; + + + /** + * Creates a new compare operation handle. + * + * @param req compare request to expect a response for + * @param conn the request will be executed on + * @param timeout duration to wait for a response + */ + public DefaultCompareOperationHandle(final CompareRequest req, final TransportConnection conn, final Duration timeout) { + super(req, conn, timeout); + } + + + @Override + public DefaultCompareOperationHandle send() { + super.send(); + return this; + } + + + @Override + public CompareResponse await() + throws LdapException { + return super.await(); + } + + + @Override + public DefaultCompareOperationHandle onResult(final ResultHandler... function) { + super.onResult(function); + return this; + } + + + @Override + public DefaultCompareOperationHandle onControl(final ResponseControlHandler... function) { + super.onControl(function); + return this; + } + + + @Override + public DefaultCompareOperationHandle onReferral(final ReferralHandler... function) { + super.onReferral(function); + return this; + } + + + @Override + public DefaultCompareOperationHandle onIntermediate(final IntermediateResponseHandler... function) { + super.onIntermediate(function); + return this; + } + + + @Override + public DefaultCompareOperationHandle onUnsolicitedNotification(final UnsolicitedNotificationHandler... function) { + super.onUnsolicitedNotification(function); + return this; + } + + + @Override + public DefaultCompareOperationHandle onException(final ExceptionHandler function) { + super.onException(function); + return this; + } + + + @Override + public DefaultCompareOperationHandle throwIf(final ResultPredicate function) { + super.throwIf(function); + return this; + } + + + @Override + public DefaultCompareOperationHandle onComplete(final CompleteHandler function) { + super.onComplete(function); + return this; + } + + + /** + * Sets the function to execute when a compare result is received. + * + * @param function to execute on a compare result + * @return this handle + */ + public DefaultCompareOperationHandle onCompare(final CompareValueHandler... function) { + onCompare = function; + initializeMessageFunctional((Object[]) onCompare); + return this; + } + + + /** + * Invokes {@link #onCompare}. + * + * @param response compare response + */ + public void compare(final CompareResponse response) { + if (getMessageID() != response.getMessageID()) { + final IllegalArgumentException e = new IllegalArgumentException( + "Invalid compare response " + response + " for handle " + this); + exception(new LdapException(e)); + throw e; + } + if (onCompare != null) { + for (CompareValueHandler func : onCompare) { + try { + if (response.getResultCode() == ResultCode.COMPARE_TRUE) { + func.accept(Boolean.TRUE); + } else if (response.getResultCode() == ResultCode.COMPARE_FALSE) { + func.accept(Boolean.FALSE); + } + } catch (Exception ex) { + // + } + } + } + } + + + @Override + public String toString() { + return super.toString() + ", " + "onCompare=" + Arrays.toString(onCompare); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultExtendedOperationHandle.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultExtendedOperationHandle.java new file mode 100644 index 0000000..ea17613 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultExtendedOperationHandle.java @@ -0,0 +1,159 @@ + +package org.xbib.net.ldap.transport; + +import java.time.Duration; +import java.util.Arrays; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.extended.ExtendedOperationHandle; +import org.xbib.net.ldap.extended.ExtendedRequest; +import org.xbib.net.ldap.extended.ExtendedResponse; +import org.xbib.net.ldap.handler.CompleteHandler; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.ExtendedValueHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Handle that notifies on the components of an extended request. + * + */ +public class DefaultExtendedOperationHandle + extends DefaultOperationHandle implements ExtendedOperationHandle { + + /** + * Functions to handle extended response name and value. + */ + private ExtendedValueHandler[] onExtended; + + + /** + * Creates a new extended operation handle. + * + * @param req search request to expect a response for + * @param conn the request will be executed on + * @param timeout duration to wait for a response + */ + public DefaultExtendedOperationHandle( + final ExtendedRequest req, + final TransportConnection conn, + final Duration timeout) { + super(req, conn, timeout); + } + + + @Override + public DefaultExtendedOperationHandle send() { + super.send(); + return this; + } + + + @Override + public ExtendedResponse await() + throws LdapException { + return super.await(); + } + + + @Override + public DefaultExtendedOperationHandle onResult(final ResultHandler... function) { + super.onResult(function); + return this; + } + + + @Override + public DefaultExtendedOperationHandle onControl(final ResponseControlHandler... function) { + super.onControl(function); + return this; + } + + + @Override + public DefaultExtendedOperationHandle onReferral(final ReferralHandler... function) { + super.onReferral(function); + return this; + } + + + @Override + public DefaultExtendedOperationHandle onIntermediate(final IntermediateResponseHandler... function) { + super.onIntermediate(function); + return this; + } + + + @Override + public DefaultExtendedOperationHandle onUnsolicitedNotification(final UnsolicitedNotificationHandler... function) { + super.onUnsolicitedNotification(function); + return this; + } + + + @Override + public DefaultExtendedOperationHandle onException(final ExceptionHandler function) { + super.onException(function); + return this; + } + + + @Override + public DefaultExtendedOperationHandle throwIf(final ResultPredicate function) { + super.throwIf(function); + return this; + } + + + @Override + public DefaultExtendedOperationHandle onComplete(final CompleteHandler function) { + super.onComplete(function); + return this; + } + + + /** + * Sets the function to execute when an extended response is received. + * + * @param function to execute on an extended response + * @return this handle + */ + public DefaultExtendedOperationHandle onExtended(final ExtendedValueHandler... function) { + onExtended = function; + initializeMessageFunctional((Object[]) onExtended); + return this; + } + + + /** + * Invokes {@link #onExtended}. + * + * @param response extended response + */ + public void extended(final ExtendedResponse response) { + if (getMessageID() != response.getMessageID()) { + final IllegalArgumentException e = new IllegalArgumentException( + "Invalid extended response " + response + " for handle " + this); + exception(new LdapException(e)); + throw e; + } + if (onExtended != null) { + for (ExtendedValueHandler func : onExtended) { + try { + func.accept(response.getResponseName(), response.getResponseValue()); + } catch (Exception ex) { + // + } + } + } + } + + + @Override + public String toString() { + return super.toString() + ", " + "onExtended=" + Arrays.toString(onExtended); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultOperationHandle.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultOperationHandle.java new file mode 100644 index 0000000..f8c13c5 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultOperationHandle.java @@ -0,0 +1,715 @@ + +package org.xbib.net.ldap.transport; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; +import org.xbib.net.ldap.AbandonRequest; +import org.xbib.net.ldap.AddRequest; +import org.xbib.net.ldap.AddResponse; +import org.xbib.net.ldap.BindRequest; +import org.xbib.net.ldap.BindResponse; +import org.xbib.net.ldap.CompareRequest; +import org.xbib.net.ldap.CompareResponse; +import org.xbib.net.ldap.DeleteRequest; +import org.xbib.net.ldap.DeleteResponse; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Message; +import org.xbib.net.ldap.ModifyDnRequest; +import org.xbib.net.ldap.ModifyDnResponse; +import org.xbib.net.ldap.ModifyRequest; +import org.xbib.net.ldap.ModifyResponse; +import org.xbib.net.ldap.OperationHandle; +import org.xbib.net.ldap.Request; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.UnbindRequest; +import org.xbib.net.ldap.control.ResponseControl; +import org.xbib.net.ldap.extended.CancelRequest; +import org.xbib.net.ldap.extended.ExtendedOperationHandle; +import org.xbib.net.ldap.extended.ExtendedRequest; +import org.xbib.net.ldap.extended.ExtendedResponse; +import org.xbib.net.ldap.extended.IntermediateResponse; +import org.xbib.net.ldap.extended.StartTLSRequest; +import org.xbib.net.ldap.extended.UnsolicitedNotification; +import org.xbib.net.ldap.handler.CompleteHandler; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Handle that notifies on the components of an LDAP operation request. + * + * @param type of request + * @param type of result + */ +public class DefaultOperationHandle implements OperationHandle { + + /** + * Predicate that requires any result message except unsolicited. + */ + private static final Predicate DEFAULT_RESPONSE_TIMEOUT_CONDITION = + new Predicate<>() { + @Override + public boolean test(final Message message) { + return message instanceof Result && !(message instanceof UnsolicitedNotification); + } + + @Override + public String toString() { + return "DEFAULT_RESPONSE_TIMEOUT_CONDITION"; + } + }; + /** + * Semaphore to determine when a response has been received. + */ + private final Semaphore responseSemaphore = new Semaphore(0); + /** + * Timestamp when the handle was created. + */ + private final Instant creationTime = Instant.now(); + /** + * Protocol request to send. + */ + private final Request request; + /** + * Connection to send the request on. + */ + private TransportConnection connection; + /** + * Time to wait for a response. + */ + private final Duration responseTimeout; + /** + * Protocol message ID. + */ + private Integer messageID; + /** + * Functions to handle response results. + */ + private ResultHandler[] onResult; + /** + * Functions to handle response controls. + */ + private ResponseControlHandler[] onControl; + /** + * Functions to handle referral URLs. + */ + private ReferralHandler[] onReferral; + /** + * Functions to handle intermediate responses. + */ + private IntermediateResponseHandler[] onIntermediate; + /** + * Function to handle exceptions. + */ + private ExceptionHandler onException; + /** + * Function to handle unsolicited notifications. + */ + private UnsolicitedNotificationHandler[] onUnsolicitedNotification; + /** + * Function to run when the operation completes. + */ + private CompleteHandler onComplete; + /** + * Function to run when a result is received to determine whether an exception should be raised. + */ + private ResultPredicate throwCondition; + /** + * Timestamp when the request was sent. See {@link TransportConnection#write(DefaultOperationHandle)}. + */ + private Instant sentTime; + + /** + * Timestamp when the result was received or an exception occurred. + */ + private Instant receivedTime; + + /** + * Whether this handle has consumed any messages. + */ + private boolean consumedMessage; + + /** + * Protocol response result. + */ + private S result; + + /** + * Exception encountered attempting to process the request. + */ + private LdapException exception; + + + /** + * Creates a new operation handle. + * + * @param req request to expect a response for + * @param conn the request will be executed on + * @param timeout duration to wait for a response + */ + public DefaultOperationHandle(final Q req, final TransportConnection conn, final Duration timeout) { + if (req == null) { + throw new IllegalArgumentException("Request cannot be null"); + } + if (conn == null) { + throw new IllegalArgumentException("Connection cannot be null"); + } + request = req; + connection = conn; + responseTimeout = timeout; + } + + + /** + * Returns a predicate to determine whether the responseTimeout semaphore should be released. + * + * @return response timeout condition + */ + protected Predicate getResponseTimeoutCondition() { + return DEFAULT_RESPONSE_TIMEOUT_CONDITION; + } + + + @Override + public DefaultOperationHandle send() { + if (sentTime != null) { + throw new IllegalStateException("Request for handle " + this + " has already been sent"); + } + if (connection == null) { + throw new IllegalStateException("Cannot execute request for handle " + this + " , connection is null"); + } + connection.write(this); + return this; + } + + + @Override + public S await() + throws LdapException { + try { + if (Duration.ZERO.equals(responseTimeout)) { + do { + responseSemaphore.acquire(); + } while (result == null && exception == null); + } else { + do { + if (!responseSemaphore.tryAcquire(responseTimeout.toMillis(), TimeUnit.MILLISECONDS)) { + abandon( + new LdapException( + ResultCode.LDAP_TIMEOUT, + "No response received in " + responseTimeout.toMillis() + "ms for handle " + this)); + break; + } + } while (result == null && exception == null); + } + } catch (InterruptedException e) { + exception(new LdapException(ResultCode.LOCAL_ERROR, e)); + } + if (result != null && exception == null) { + if (throwCondition != null) { + throwCondition.testAndThrow(result); + } + return result; + } + if (exception == null) { + throw new LdapException( + ResultCode.LOCAL_ERROR, + "Response completed for handle " + this + " without a result or exception"); + } + throw exception; + } + + + @Override + public DefaultOperationHandle onResult(final ResultHandler... function) { + onResult = function; + initializeMessageFunctional((Object[]) onResult); + return this; + } + + + @Override + public DefaultOperationHandle onControl(final ResponseControlHandler... function) { + onControl = function; + initializeMessageFunctional((Object[]) onControl); + return this; + } + + + @Override + public DefaultOperationHandle onReferral(final ReferralHandler... function) { + onReferral = function; + initializeMessageFunctional((Object[]) onReferral); + return this; + } + + + @Override + public DefaultOperationHandle onIntermediate(final IntermediateResponseHandler... function) { + onIntermediate = function; + initializeMessageFunctional((Object[]) onIntermediate); + return this; + } + + + @Override + public DefaultOperationHandle onUnsolicitedNotification(final UnsolicitedNotificationHandler... function) { + onUnsolicitedNotification = function; + initializeMessageFunctional((Object[]) onUnsolicitedNotification); + return this; + } + + + @Override + public DefaultOperationHandle onException(final ExceptionHandler function) { + onException = function; + initializeMessageFunctional(onException); + return this; + } + + + @Override + public DefaultOperationHandle onComplete(final CompleteHandler function) { + onComplete = function; + return this; + } + + + @Override + public DefaultOperationHandle throwIf(final ResultPredicate function) { + throwCondition = function; + return this; + } + + + /** + * Iterates over the supplied functions, set the connection and request if the type is {@link MessageFunctional}. + * + * @param functions to initialize + */ + @SuppressWarnings("unchecked") + protected void initializeMessageFunctional(final Object... functions) { + if (functions != null) { + for (Object o : functions) { + if (o instanceof MessageFunctional) { + ((MessageFunctional) o).setConnection(connection); + ((MessageFunctional) o).setRequest(request); + ((MessageFunctional) o).setHandle(this); + } + } + } + } + + + @Override + public void abandon() { + abandon(new LdapException(ResultCode.USER_CANCELLED, "Request abandoned")); + } + + + /** + * Abandons this operation. Any threads waiting on the result will receive an empty result. See {@link + * TransportConnection#operation(AbandonRequest)}. + * + * @param cause the reason this request was abandoned + */ + public void abandon(final LdapException cause) { + // Bind, unbind, StartTLS and Cancel cannot be abandoned + if (!(request instanceof BindRequest || + request instanceof UnbindRequest || + request instanceof StartTLSRequest || + request instanceof CancelRequest)) { + // Don't abandon a request if the response has been received + if (receivedTime == null) { + try { + connection.operation(new AbandonRequest(messageID)); + } catch (Exception e) { + // + } finally { + exception(cause); + } + } else { + exception(cause); + } + } else { + exception(cause); + } + } + + + @Override + public ExtendedOperationHandle cancel() { + if (sentTime == null) { + throw new IllegalStateException( + "Request has not been sent for handle " + this + ". Invoke send before calling this method."); + } + // Don't cancel a request if the response has been received + if (receivedTime != null) { + throw new IllegalStateException( + "Operation completed for handle " + this + ". Cancel cannot be invoked."); + } + final CompleteHandler completeHandler = onComplete; + onComplete = null; + final ExtendedOperationHandle handle = connection.operation(new CancelRequest(messageID)); + if (completeHandler != null) { + handle.onComplete(completeHandler); + } + return handle; + } + + + /** + * Whether the supplied result belongs to this handle. + * + * @param r to inspect + * @return whether the supplied result belong to this handle + */ + private boolean supports(final Result r) { + if (messageID != r.getMessageID()) { + return false; + } + boolean supports = false; + if (request instanceof AddRequest && r instanceof AddResponse) { + supports = true; + } else if (request instanceof BindRequest && r instanceof BindResponse) { + supports = true; + } else if (request instanceof CompareRequest && r instanceof CompareResponse) { + supports = true; + } else if (request instanceof DeleteRequest && r instanceof DeleteResponse) { + supports = true; + } else if (request instanceof ExtendedRequest && r instanceof ExtendedResponse) { + supports = true; + } else if (request instanceof ModifyDnRequest && r instanceof ModifyDnResponse) { + supports = true; + } else if (request instanceof ModifyRequest && r instanceof ModifyResponse) { + supports = true; + } else if (request instanceof SearchRequest && r instanceof SearchResponse) { + supports = true; + } + return supports; + } + + + /** + * Returns the message ID assigned to this handle. + * + * @return message ID + */ + public Integer getMessageID() { + return messageID; + } + + + @Override + public Instant getSentTime() { + return sentTime; + } + + + @Override + public Instant getReceivedTime() { + return receivedTime; + } + + + public ResultHandler[] getOnResult() { + return onResult; + } + + + public ResponseControlHandler[] getOnControl() { + return onControl; + } + + + public ReferralHandler[] getOnReferral() { + return onReferral; + } + + + public IntermediateResponseHandler[] getOnIntermediate() { + return onIntermediate; + } + + + public ExceptionHandler getOnException() { + return onException; + } + + + public CompleteHandler getOnComplete() { + return onComplete; + } + + + public ResultPredicate getThrowCondition() { + return throwCondition; + } + + + public UnsolicitedNotificationHandler[] getOnUnsolicitedNotification() { + return onUnsolicitedNotification; + } + + + /** + * Returns whether this handle has consumed any messages. + * + * @return whether this handle has consumed any messages + */ + public boolean hasConsumedMessage() { + return consumedMessage; + } + + + /** + * Returns the request. + * + * @return request + */ + public Request getRequest() { + return request; + } + + + /** + * Sets the message ID. + * + * @param id message ID + */ + public void messageID(final int id) { + messageID = id; + } + + + /** + * Sets the sent time to now. + */ + public void sent() { + sentTime = Instant.now(); + } + + + /** + * Invokes {@link #onResult} and sets the result. Handle is considered done when this is invoked. + * + * @param r result + */ + public void result(final S r) { + if (r == null) { + final IllegalArgumentException e = new IllegalArgumentException("Result cannot be null for handle " + this); + exception(new LdapException(e)); + throw e; + } + if (!supports(r)) { + final IllegalArgumentException e = new IllegalArgumentException("Invalid result " + r + " for handle " + this); + exception(new LdapException(e)); + throw e; + } + if (onResult != null) { + for (ResultHandler func : onResult) { + try { + func.accept(r); + } catch (Exception ex) { + // + } + } + } + result = r; + consumedMessage(r); + complete(); + } + + + /** + * Invokes {@link #onControl}. + * + * @param c response control + */ + public void control(final ResponseControl c) { + if (onControl != null) { + for (ResponseControlHandler func : onControl) { + try { + func.accept(c); + } catch (Exception ex) { + // + } + } + } + } + + + /** + * Invokes {@link #onReferral}. + * + * @param url referral url + */ + public void referral(final String... url) { + if (onReferral != null) { + for (ReferralHandler func : onReferral) { + try { + func.accept(url); + } catch (Exception ex) { + // + } + } + } + } + + + /** + * Invokes {@link #onIntermediate}. + * + * @param r intermediate response + */ + public void intermediate(final IntermediateResponse r) { + if (getMessageID() != r.getMessageID()) { + final IllegalArgumentException e = new IllegalArgumentException( + "Invalid intermediate response " + r + " for handle " + this); + exception(new LdapException(e)); + throw e; + } + if (onIntermediate != null) { + for (Consumer func : onIntermediate) { + try { + func.accept(r); + } catch (Exception ex) { + // + } + } + } + consumedMessage(r); + } + + + /** + * Invokes {@link #onUnsolicitedNotification}. + * + * @param u unsolicited notification + */ + public void unsolicitedNotification(final UnsolicitedNotification u) { + if (onUnsolicitedNotification != null) { + for (UnsolicitedNotificationHandler func : onUnsolicitedNotification) { + try { + func.accept(u); + } catch (Exception ex) { + // + } + } + } + consumedMessage(u); + } + + + /** + * Invokes {@link #onException} followed by {@link #complete()}. + * + * @param e exception + */ + public void exception(final LdapException e) { + if (e == null) { + final IllegalArgumentException ex = new IllegalArgumentException("Exception cannot be null for handle " + this); + exception(new LdapException(ex)); + throw ex; + } + if (onException != null) { + try { + onException.accept(e); + } catch (Exception ex) { + // + } + } + exception = e; + consumedMessage(); + complete(); + } + + + /** + * Indicates that a protocol message was consumed by a supplied consumer. + */ + protected void consumedMessage() { + consumedMessage(true); + } + + + /** + * Indicates that a protocol message was consumed by a supplied consumer. + * + * @param message that was consumed + */ + protected void consumedMessage(final Message message) { + consumedMessage(getResponseTimeoutCondition().test(message)); + } + + + /** + * Indicates that a protocol message was consumed by a supplied consumer. + * + * @param signalResponseSemaphore whether to signal the response semaphore + */ + private void consumedMessage(final boolean signalResponseSemaphore) { + consumedMessage = true; + if (signalResponseSemaphore) { + responseSemaphore.release(); + } + } + + + /** + * Releases the latch and sets the response as received. Invokes {@link #onComplete}. Handle is considered done when + * this is invoked. + */ + private synchronized void complete() { + try { + if (receivedTime != null) { + return; + } + receivedTime = Instant.now(); + if (onComplete != null) { + try { + onComplete.execute(); + } catch (Exception e) { + // + } + } + } finally { + try { + if (connection != null) { + connection.complete(this); + } + } catch (Exception e) { + // + } + connection = null; + } + } + + + @Override + public String toString() { + return getClass().getName() + + "@" + hashCode() + "::" + + "messageID=" + messageID + ", " + + "request=" + request + ", " + + "connection=" + connection + ", " + + "responseTimeout=" + responseTimeout + ", " + + "creationTime=" + creationTime + ", " + + "sentTime=" + sentTime + ", " + + "receivedTime=" + receivedTime + ", " + + "consumedMessage=" + consumedMessage + ", " + + "result=" + result + ", " + + "exception=" + exception; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultSaslClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultSaslClient.java new file mode 100644 index 0000000..e80ed0c --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultSaslClient.java @@ -0,0 +1,124 @@ + +package org.xbib.net.ldap.transport; + +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import org.xbib.net.ldap.BindResponse; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.sasl.DefaultSaslClientRequest; +import org.xbib.net.ldap.sasl.Mechanism; +import org.xbib.net.ldap.sasl.QualityOfProtection; +import org.xbib.net.ldap.sasl.SaslClient; + +/** + * SASL client that negotiates the details of the bind operation. + * + */ +public class DefaultSaslClient implements SaslClient { + + /** + * Underlying SASL client. + */ + private javax.security.sasl.SaslClient client; + + + /** + * Returns the underlying SASL client. + * + * @return SASL client + */ + public javax.security.sasl.SaslClient getClient() { + return client; + } + + + /** + * Performs a SASL bind. + * + * @param conn to perform the bind on + * @param request SASL request to perform + * @return final result of the bind process + * @throws SaslException if an error occurs + */ + public BindResponse bind(final TransportConnection conn, final DefaultSaslClientRequest request) + throws SaslException { + BindResponse response; + final String serverName = conn.getLdapURL().getHostname(); + try { + client = Sasl.createSaslClient( + new String[]{request.getMechanism().mechanism()}, + request.getAuthorizationID(), + "ldap", + serverName, + request.getSaslProperties(), + request); + + byte[] bytes = client.hasInitialResponse() ? client.evaluateChallenge(new byte[0]) : null; + response = conn.operation(request.createBindRequest(bytes)).execute(); + if (ResultCode.SASL_BIND_IN_PROGRESS != response.getResultCode()) { + return response; + } + while (!client.isComplete() && ResultCode.SASL_BIND_IN_PROGRESS == response.getResultCode()) { + bytes = client.evaluateChallenge(response.getServerSaslCreds()); + response = conn.operation(request.createBindRequest(bytes)).execute(); + } + if (ResultCode.SASL_BIND_IN_PROGRESS == response.getResultCode()) { + throw new SaslException( + "SASL client error: client completed but bind still in progress for " + request + " with " + response); + } + if (!client.isComplete() && response.getServerSaslCreds() != null) { + // server may return additional data for the client in the last response + client.evaluateChallenge(response.getServerSaslCreds()); + } + if (!client.isComplete() && ResultCode.SUCCESS == response.getResultCode()) { + throw new SaslException("SASL client error: client did not complete for " + request + " with " + response); + } + return response; + } catch (Throwable e) { + dispose(); + if (e instanceof SaslException) { + throw (SaslException) e; + } + throw new SaslException("SASL bind failed for " + request, e); + } + } + + + /** + * Returns the SASL mechanism for this client. See {@link javax.security.sasl.SaslClient#getMechanismName()}. + * + * @return SASL mechanism + */ + public Mechanism getMechanism() { + return Mechanism.valueOf(client.getMechanismName()); + } + + + /** + * Returns the QOP for this client. See {@link javax.security.sasl.SaslClient#getNegotiatedProperty(String)}. + * + * @return QOP or null if the underlying sasl client has not completed + */ + public QualityOfProtection getQualityOfProtection() { + if (!client.isComplete()) { + return null; + } + return QualityOfProtection.fromString((String) client.getNegotiatedProperty(Sasl.QOP)); + } + + + /** + * Disposes the underlying SASL client. See {@link javax.security.sasl.SaslClient#dispose()}. + */ + public void dispose() { + if (client != null) { + try { + client.dispose(); + } catch (SaslException e) { + // + } finally { + client = null; + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultSearchOperationHandle.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultSearchOperationHandle.java new file mode 100644 index 0000000..d43a3b4 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/DefaultSearchOperationHandle.java @@ -0,0 +1,289 @@ + +package org.xbib.net.ldap.transport; + +import java.time.Duration; +import java.util.Arrays; +import java.util.function.Predicate; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.Message; +import org.xbib.net.ldap.SearchOperationHandle; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.SearchResultReference; +import org.xbib.net.ldap.extended.IntermediateResponse; +import org.xbib.net.ldap.handler.CompleteHandler; +import org.xbib.net.ldap.handler.ExceptionHandler; +import org.xbib.net.ldap.handler.IntermediateResponseHandler; +import org.xbib.net.ldap.handler.LdapEntryHandler; +import org.xbib.net.ldap.handler.ReferralHandler; +import org.xbib.net.ldap.handler.ResponseControlHandler; +import org.xbib.net.ldap.handler.ResultHandler; +import org.xbib.net.ldap.handler.ResultPredicate; +import org.xbib.net.ldap.handler.SearchReferenceHandler; +import org.xbib.net.ldap.handler.SearchResultHandler; +import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler; + +/** + * Handle that notifies on the components of a search request. + * + */ +public class DefaultSearchOperationHandle + extends DefaultOperationHandle implements SearchOperationHandle { + + /** + * Predicate that requires any message except unsolicited. + */ + private static final Predicate SEARCH_RESPONSE_TIMEOUT_CONDITION = + new Predicate<>() { + @Override + public boolean test(final Message message) { + return message instanceof IntermediateResponse || + message instanceof LdapEntry || + message instanceof SearchResultReference || + message instanceof SearchResponse; + } + + @Override + public String toString() { + return "SEARCH_RESPONSE_TIMEOUT_CONDITION"; + } + }; + + /** + * Whether to automatically sort search results. + */ + private static final boolean SORT_RESULTS = Boolean.parseBoolean( + System.getProperty("org.xbib.net.ldap.sortSearchResults", "false")); + + /** + * Functions to handle response entries. + */ + private LdapEntryHandler[] onEntry; + + /** + * Functions to handle response references. + */ + private SearchReferenceHandler[] onReference; + + /** + * Functions to handle complete response. + */ + private SearchResultHandler[] onSearchResult; + + /** + * Synthetic result that is built as entries and references are received. + */ + private SearchResponse result = new SearchResponse(); + + + /** + * Creates a new search operation handle. + * + * @param req search request to expect a response for + * @param conn the request will be executed on + * @param timeout duration to wait for a response + */ + public DefaultSearchOperationHandle(final SearchRequest req, final TransportConnection conn, final Duration timeout) { + super(req, conn, timeout); + } + + + @Override + protected Predicate getResponseTimeoutCondition() { + return SEARCH_RESPONSE_TIMEOUT_CONDITION; + } + + + @Override + public DefaultSearchOperationHandle send() { + super.send(); + return this; + } + + + @Override + public SearchResponse await() + throws LdapException { + final SearchResponse done = super.await(); + result.initialize(done); + if (SORT_RESULTS) { + result = SearchResponse.sort(result); + } + if (onSearchResult != null) { + for (SearchResultHandler func : onSearchResult) { + try { + result = func.apply(result); + } catch (Exception ex) { + // + } + } + } + return result; + } + + + @Override + public SearchResponse execute() + throws LdapException { + return send().await(); + } + + + @Override + public DefaultSearchOperationHandle onResult(final ResultHandler... function) { + super.onResult(function); + return this; + } + + + @Override + public DefaultSearchOperationHandle onControl(final ResponseControlHandler... function) { + super.onControl(function); + return this; + } + + + @Override + public DefaultSearchOperationHandle onReferral(final ReferralHandler... function) { + super.onReferral(function); + return this; + } + + + @Override + public DefaultSearchOperationHandle onIntermediate(final IntermediateResponseHandler... function) { + super.onIntermediate(function); + return this; + } + + + @Override + public DefaultSearchOperationHandle onUnsolicitedNotification(final UnsolicitedNotificationHandler... function) { + super.onUnsolicitedNotification(function); + return this; + } + + + @Override + public DefaultSearchOperationHandle onException(final ExceptionHandler function) { + super.onException(function); + return this; + } + + + @Override + public DefaultSearchOperationHandle throwIf(final ResultPredicate function) { + super.throwIf(function); + return this; + } + + + @Override + public DefaultSearchOperationHandle onComplete(final CompleteHandler function) { + super.onComplete(function); + return this; + } + + + @Override + public DefaultSearchOperationHandle onEntry(final LdapEntryHandler... function) { + onEntry = function; + initializeMessageFunctional((Object[]) onEntry); + return this; + } + + + @Override + public DefaultSearchOperationHandle onReference(final SearchReferenceHandler... function) { + onReference = function; + initializeMessageFunctional((Object[]) onReference); + return this; + } + + + @Override + public DefaultSearchOperationHandle onSearchResult(final SearchResultHandler... function) { + onSearchResult = function; + initializeMessageFunctional((Object[]) onSearchResult); + return this; + } + + + public LdapEntryHandler[] getOnEntry() { + return onEntry; + } + + + public SearchReferenceHandler[] getOnReference() { + return onReference; + } + + + public SearchResultHandler[] getOnSearchResult() { + return onSearchResult; + } + + + /** + * Invokes {@link #onEntry}. + * + * @param r search result entry + */ + public void entry(final LdapEntry r) { + if (getMessageID() != r.getMessageID()) { + final IllegalArgumentException e = new IllegalArgumentException("Invalid entry " + r + " for handle " + this); + exception(new LdapException(e)); + throw e; + } + LdapEntry e = r; + if (onEntry != null) { + for (LdapEntryHandler func : onEntry) { + try { + e = func.apply(e); + } catch (Exception ex) { + // + } + } + } + if (e != null) { + result.addEntries(e); + } + consumedMessage(r); + } + + + /** + * Invokes {@link #onReference}. + * + * @param r search result reference + */ + public void reference(final SearchResultReference r) { + if (getMessageID() != r.getMessageID()) { + final IllegalArgumentException e = new IllegalArgumentException("Invalid reference " + r + " for handle " + this); + exception(new LdapException(e)); + throw e; + } + if (onReference != null) { + for (SearchReferenceHandler func : onReference) { + try { + func.accept(r); + } catch (Exception ex) { + // + } + } + } + result.addReferences(r); + consumedMessage(r); + } + + + @Override + public String toString() { + // do not log the result object, it is not thread safe + return super.toString() + ", " + + "onEntry=" + Arrays.toString(onEntry) + ", " + + "onReference=" + Arrays.toString(onReference) + ", " + + "onSearchResult=" + Arrays.toString(onSearchResult); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/GssApiSaslClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/GssApiSaslClient.java new file mode 100644 index 0000000..6ea6cdc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/GssApiSaslClient.java @@ -0,0 +1,83 @@ + +package org.xbib.net.ldap.transport; + +import java.util.HashMap; +import java.util.Map; +import javax.security.auth.Subject; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; +import javax.security.sasl.SaslException; +import org.xbib.net.ldap.BindResponse; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.sasl.GssApiBindRequest; +import org.xbib.net.ldap.sasl.SaslClient; + +/** + * GSSAPI SASL client that implements the JAAS details to perform an LDAP bind with a kerberos principal. If a specific + * JAAS name is set on the {@link GssApiBindRequest} that configuration will be used. Else if no JAAS configuration + * properties are supplied a configuration with the name 'gssapi' will be attempted. Otherwise the + * 'com.sun.security.auth.module.Krb5LoginModule' is instantiated and used with any options provided from {@link + * GssApiBindRequest}. This allows configuration to occur both from a JAAS login configuration file or by setting + * properties directly on the request. + * + */ +public class GssApiSaslClient implements SaslClient { + + + /** + * Performs a GSSAPI SASL bind. + * + * @param conn to perform the bind on + * @param request SASL request to perform + * @return final result of the bind process + * @throws LoginException if an error occurs + * @throws SaslException if an error occurs + */ + public BindResponse bind(final TransportConnection conn, final GssApiBindRequest request) + throws LoginException, SaslException { + final Subject subject; + if (request.getJaasName() != null) { + if (request.getJaasRefreshConfig()) { + try { + Configuration.getConfiguration().refresh(); + } catch (Exception e) { + // + } + } + final LoginContext context = new LoginContext(request.getJaasName(), request); + context.login(); + subject = context.getSubject(); + } else { + final LoginModule loginModule; + try { + loginModule = (LoginModule) Class.forName(request.getJaasLoginModule()).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new SaslException( + "Could not instantiate JAAS module '" + request.getJaasLoginModule() + "' for GSSAPI", e); + } + + subject = new Subject(); + final Map state = new HashMap<>(); + loginModule.initialize(subject, request, state, request.getJaasOptions()); + if (loginModule.login()) { + if (!loginModule.commit()) { + loginModule.abort(); + throw new LoginException("Commit failed for " + request + " using " + loginModule); + } + } else { + loginModule.abort(); + throw new LoginException("Login failed for " + request + " using " + loginModule); + } + } + + final BindResponse result; + try { + result = conn.operation(request); + } catch (LdapException e) { + throw new SaslException("SASL GSSAPI operation failed for " + request, e); + } + return result; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/MessageFunctional.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/MessageFunctional.java new file mode 100644 index 0000000..4ffba24 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/MessageFunctional.java @@ -0,0 +1,101 @@ + +package org.xbib.net.ldap.transport; + +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.OperationHandle; +import org.xbib.net.ldap.Request; +import org.xbib.net.ldap.Result; + +/** + * Base class for processing a message that is initialized with the request and connection. + * + * @param type of request + * @param type of result + */ +// CheckStyle:AbstractClassName OFF +public abstract class MessageFunctional { + + /** + * Connection the request occurred on. + */ + private Connection connection; + + /** + * Request that produced the message. + */ + private Q request; + + /** + * Operation handle that sent the request. + */ + private OperationHandle handle; + + + public Connection getConnection() { + return connection; + } + + + public void setConnection(final TransportConnection conn) { + connection = conn; + } + + + public Q getRequest() { + return request; + } + + + public void setRequest(final Q req) { + request = req; + } + + + public OperationHandle getHandle() { + return handle; + } + + + public void setHandle(final OperationHandle h) { + handle = h; + } + + + /** + * Marker class to inject connection and request properties. + * + * @param type of request + * @param type of result + * @param the type of the input to the function + * @param the type of the result of the function + */ + public abstract static class Function + extends MessageFunctional implements java.util.function.Function { + } + + + /** + * Marker class to inject connection and request properties. + * + * @param type of request + * @param type of result + * @param the type of the input to the operation + */ + public abstract static class Consumer + extends MessageFunctional implements java.util.function.Consumer { + } + + + /** + * Marker class to inject connection and request properties. + * + * @param type of request + * @param type of result + * @param the type of the first argument to the operation + * @param the type of the second argument to the operation + */ + public abstract static class BiConsumer + extends MessageFunctional implements java.util.function.BiConsumer { + } +} +// CheckStyle:AbstractClassName ON diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/ResponseParser.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/ResponseParser.java new file mode 100644 index 0000000..be4246a --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/ResponseParser.java @@ -0,0 +1,167 @@ + +package org.xbib.net.ldap.transport; + +import java.util.Optional; +import org.xbib.net.ldap.AddResponse; +import org.xbib.net.ldap.BindResponse; +import org.xbib.net.ldap.CompareResponse; +import org.xbib.net.ldap.DeleteResponse; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.Message; +import org.xbib.net.ldap.ModifyDnResponse; +import org.xbib.net.ldap.ModifyResponse; +import org.xbib.net.ldap.SearchResponse; +import org.xbib.net.ldap.SearchResultReference; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.DERPath; +import org.xbib.net.ldap.extended.ExtendedResponse; +import org.xbib.net.ldap.extended.IntermediateResponse; +import org.xbib.net.ldap.extended.NoticeOfDisconnection; +import org.xbib.net.ldap.extended.SyncInfoMessage; + +/** + * Parses a buffer looking for an LDAP response message. + * + */ +public class ResponseParser { + + /** + * Bind response DER path. + */ + private static final DERPath BIND_PATH = new DERPath("/SEQ/APP(1)"); + + /** + * Search entry DER path. + */ + private static final DERPath ENTRY_PATH = new DERPath("/SEQ/APP(4)"); + + /** + * Search response DER path. + */ + private static final DERPath SEARCH_PATH = new DERPath("/SEQ/APP(5)"); + + /** + * Modify response DER path. + */ + private static final DERPath MODIFY_PATH = new DERPath("/SEQ/APP(7)"); + + /** + * Add response DER path. + */ + private static final DERPath ADD_PATH = new DERPath("/SEQ/APP(9)"); + + /** + * Delete response DER path. + */ + private static final DERPath DELETE_PATH = new DERPath("/SEQ/APP(11)"); + + /** + * Modify DN response DER path. + */ + private static final DERPath MODIFY_DN_PATH = new DERPath("/SEQ/APP(13)"); + + /** + * Compare response DER path. + */ + private static final DERPath COMPARE_PATH = new DERPath("/SEQ/APP(15)"); + + /** + * Search reference result DER path. + */ + private static final DERPath SEARCH_REFERENCE_PATH = new DERPath("/SEQ/APP(19)"); + + /** + * Extended response DER path. + */ + private static final DERPath EXTENDED_PATH = new DERPath("/SEQ/APP(24)"); + + /** + * Intermediate response DER path. + */ + private static final DERPath INTERMEDIATE_PATH = new DERPath("/SEQ/APP(25)"); + + /** + * Parser for decoding LDAP messages. + */ + private final DERParser parser = new DERParser(); + + /** + * Message produced from parsing a DER buffer. + */ + private Message message; + + + /** + * Creates a new response parser. + */ + public ResponseParser() { + parser.registerHandler(BIND_PATH, (p, e) -> { + e.clear(); + message = new BindResponse(e); + }); + parser.registerHandler(ENTRY_PATH, (p, e) -> { + e.clear(); + message = new LdapEntry(e); + }); + parser.registerHandler(SEARCH_PATH, (p, e) -> { + e.clear(); + message = new SearchResponse(e); + }); + parser.registerHandler(MODIFY_PATH, (p, e) -> { + e.clear(); + message = new ModifyResponse(e); + }); + parser.registerHandler(ADD_PATH, (p, e) -> { + e.clear(); + message = new AddResponse(e); + }); + parser.registerHandler(DELETE_PATH, (p, e) -> { + e.clear(); + message = new DeleteResponse(e); + }); + parser.registerHandler(MODIFY_DN_PATH, (p, e) -> { + e.clear(); + message = new ModifyDnResponse(e); + }); + parser.registerHandler(COMPARE_PATH, (p, e) -> { + e.clear(); + message = new CompareResponse(e); + }); + parser.registerHandler(SEARCH_REFERENCE_PATH, (p, e) -> { + e.clear(); + message = new SearchResultReference(e); + }); + parser.registerHandler(EXTENDED_PATH, (p, e) -> { + e.clear(); + final ExtendedResponse extRes = new ExtendedResponse(e); + if (NoticeOfDisconnection.OID.equals(extRes.getResponseName())) { + message = new NoticeOfDisconnection(); + } else { + message = extRes; + } + }); + parser.registerHandler(INTERMEDIATE_PATH, (p, e) -> { + e.clear(); + final IntermediateResponse intRes = new IntermediateResponse(e); + if (SyncInfoMessage.OID.equals(intRes.getResponseName())) { + e.clear(); + message = new SyncInfoMessage(e); + } else { + message = intRes; + } + }); + } + + + /** + * Examines the supplied buffer and parses an LDAP response message if one is found. + * + * @param buffer to parse + * @return optional LDAP message + */ + public Optional parse(final DERBuffer buffer) { + parser.parse(buffer); + return Optional.ofNullable(message); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/ScramSaslClient.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/ScramSaslClient.java new file mode 100644 index 0000000..a6f63bc --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/ScramSaslClient.java @@ -0,0 +1,473 @@ + +package org.xbib.net.ldap.transport; + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.xbib.net.ldap.BindResponse; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.sasl.Mechanism; +import org.xbib.net.ldap.sasl.SaslBindRequest; +import org.xbib.net.ldap.sasl.SaslClient; +import org.xbib.net.ldap.sasl.ScramBindRequest; + +/** + * SASL client that implements the SCRAM protocol. See RFC 5802. + * + */ +public class ScramSaslClient implements SaslClient { + + /** + * Creates a new MAC using the supplied algorithm and key. + * + * @param algorithm of the MAC + * @param key to seed the MAC + * @return new mac + */ + private static Mac createMac(final String algorithm, final byte[] key) { + try { + final Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(key, algorithm)); + return mac; + } catch (Exception e) { + throw new IllegalStateException("Could not create MAC", e); + } + } + + /** + * Digests the supplied data using the supplied algorithm. + * + * @param algorithm of the digest + * @param data to digest + * @return digested data + */ + private static byte[] createDigest(final String algorithm, final byte[] data) { + try { + return MessageDigest.getInstance(algorithm).digest(data); + } catch (Exception e) { + throw new IllegalStateException("Could not create digest", e); + } + } + + /** + * Performs a SCRAM SASL bind. + * + * @param conn to perform the bind on + * @param request SASL request to perform + * @return final result of the bind process + * @throws LdapException if an error occurs + */ + public BindResponse bind(final TransportConnection conn, final ScramBindRequest request) + throws LdapException { + final ClientFirstMessage clientFirstMessage = new ClientFirstMessage(request.getUsername(), request.getNonce()); + + final BindResponse serverFirstResult = conn.operation( + new SaslBindRequest( + request.getMechanism().mechanism(), LdapUtils.utf8Encode(clientFirstMessage.encode(), false))).execute(); + + if (serverFirstResult.getResultCode() != ResultCode.SASL_BIND_IN_PROGRESS) { + if (serverFirstResult.isSuccess()) { + throw new IllegalStateException( + "Unexpected success result from SCRAM SASL bind: " + serverFirstResult.getResultCode()); + } + return serverFirstResult; + } + + final ClientFinalMessage clientFinalMessage = new ClientFinalMessage( + request.getMechanism(), + request.getPassword(), + clientFirstMessage, + new ServerFirstMessage(clientFirstMessage, serverFirstResult)); + + final BindResponse serverFinalResult = conn.operation( + new SaslBindRequest( + request.getMechanism().mechanism(), LdapUtils.utf8Encode(clientFinalMessage.encode(), false))).execute(); + + final ServerFinalMessage serverFinalMessage = new ServerFinalMessage( + request.getMechanism(), + clientFinalMessage, + serverFinalResult); + + if (!serverFinalResult.isSuccess() && serverFinalMessage.isVerified()) { + throw new IllegalStateException("Verified server message but result was not a success"); + } else if (serverFinalResult.isSuccess() && !serverFinalMessage.isVerified()) { + throw new IllegalStateException("Received success from server but message could not be verified"); + } + return serverFinalResult; + } + + /** + * Properties associated with the client first message. + */ + static class ClientFirstMessage { + + /** + * GS2 header for no channel binding. + */ + private static final String GS2_NO_CHANNEL_BINDING = "n,,"; + + /** + * Default nonce size. + */ + private static final int DEFAULT_NONCE_SIZE = 16; + + /** + * Username to authenticate. + */ + private final String clientUsername; + + /** + * Protocol nonce. + */ + private final String clientNonce; + + /** + * Message produced from the username and nonce. + */ + private final String message; + + + /** + * Creates a new client first message. If nonce is null a random is created for this client. + * + * @param username to authenticate + * @param nonce to supply to the server or null + */ + ClientFirstMessage(final String username, final byte[] nonce) { + clientUsername = username; + if (nonce == null) { + final SecureRandom random = new SecureRandom(); + final byte[] b = new byte[DEFAULT_NONCE_SIZE]; + random.nextBytes(b); + clientNonce = LdapUtils.base64Encode(b); + } else { + clientNonce = LdapUtils.base64Encode(nonce); + } + message = "n=".concat(clientUsername).concat(",").concat("r=").concat(clientNonce); + } + + + public String getNonce() { + return clientNonce; + } + + + public String getMessage() { + return message; + } + + + /** + * Encodes this message to send to the server. This methods prepends the message with a GS2 header indicating that + * no channel binding is supported. + * + * @return encoded message + */ + public String encode() { + return GS2_NO_CHANNEL_BINDING.concat(message); + } + } + + /** + * Properties associated with the final client message. + */ + static class ClientFinalMessage { + + /** + * GS2 header for no channel binding. + */ + private static final String GS2_NO_CHANNEL_BINDING = LdapUtils.base64Encode("n,,"); + + /** + * 4-octet encoding of the integer 1. + */ + private static final byte[] INTEGER_ONE = {0x00, 0x00, 0x00, 0x01,}; + + /** + * Bytes for the client key hmac. + */ + private static final byte[] CLIENT_KEY_INIT = LdapUtils.utf8Encode("Client Key"); + + /** + * Scram SASL mechanism. + */ + private final Mechanism mechanism; + + /** + * Channel binding attribute plus the combined nonce. + */ + private final String withoutProof; + + /** + * Client first message plus the server first message plus the withoutProof string. + */ + private final String message; + + /** + * Computed password using the server salt and iterations. + */ + private final byte[] saltedPassword; + + + /** + * Creates a new client final message. + * + * @param mech scram mechanism + * @param password to authenticate the user with + * @param clientFirstMessage first message sent to the server + * @param serverFirstMessage first response from the server + */ + ClientFinalMessage( + final Mechanism mech, + final String password, + final ClientFirstMessage clientFirstMessage, + final ServerFirstMessage serverFirstMessage) { + mechanism = mech; + saltedPassword = createSaltedPassword( + mechanism.properties()[1], + password, + serverFirstMessage.getSalt(), + serverFirstMessage.getIterations()); + + withoutProof = "c=".concat(GS2_NO_CHANNEL_BINDING).concat(",") + .concat("r=").concat(serverFirstMessage.getCombinedNonce()); + + message = clientFirstMessage.getMessage().concat(",") + .concat(serverFirstMessage.getMessage()).concat(",") + .concat(withoutProof); + } + + /** + * Computes a salted password. + * + * @param algorithm of the MAC + * @param password to seed the MAC with + * @param salt for the MAC + * @param iterations of the MAC + * @return salted password + */ + private static byte[] createSaltedPassword( + final String algorithm, + final String password, + final byte[] salt, + final int iterations) { + // create an HMAC using the UTF-8 password + final Mac mac = createMac(algorithm, LdapUtils.utf8Encode(password, false)); + + // Per the RFC, seed the salt with the bytes of integer 1 + byte[] bytes = Arrays.copyOf(salt, salt.length + INTEGER_ONE.length); + System.arraycopy(INTEGER_ONE, 0, bytes, salt.length, INTEGER_ONE.length); + + // first iteration is the MAC of the salt and integer 1 + bytes = mac.doFinal(bytes); + + // remaining iterations create the MAC of the previous MAC and XOR that result with the previous MAC + final byte[] xor = bytes; + for (int i = 1; i < iterations; i++) { + final byte[] macResult = mac.doFinal(bytes); + for (int j = 0; j < macResult.length; j++) { + xor[j] ^= macResult[j]; + } + bytes = macResult; + } + return xor; + } + + public byte[] getSaltedPassword() { + return saltedPassword; + } + + public String getMessage() { + return message; + } + + /** + * Encodes this message to send to the server. Concatenation of the message without proof and the proof. + * + * @return encoded message + */ + public String encode() { + final byte[] clientKey = createMac(mechanism.properties()[1], saltedPassword).doFinal(CLIENT_KEY_INIT); + final byte[] storedKey = createDigest(mechanism.properties()[0], clientKey); + + final byte[] clientSignature = + createMac(mechanism.properties()[1], storedKey).doFinal(LdapUtils.utf8Encode(message, false)); + + final byte[] clientProof = new byte[clientKey.length]; + for (int i = 0; i < clientProof.length; i++) { + clientProof[i] = (byte) (clientKey[i] ^ clientSignature[i]); + } + + return withoutProof.concat(",p=").concat(LdapUtils.base64Encode(clientProof)); + } + } + + /** + * Properties associated with the first server response. + */ + static class ServerFirstMessage { + /** + * Minimum number of iterations we will allow. + */ + private static final int MINIMUM_ITERATION_COUNT = 4096; + + /** + * The server SASL credentials. + */ + private final String message; + + /** + * Nonce parsed from the SASL credentials. + */ + private final String combinedNonce; + + /** + * Salt parsed from the SASL credentials. + */ + private final byte[] salt; + + /** + * Iterations parsed from the SASL credentials. + */ + private final int iterations; + + + /** + * Creates a new server first message. + * + * @param clientFirstMessage first message sent to the server + * @param result response to the first message + */ + ServerFirstMessage(final ClientFirstMessage clientFirstMessage, final BindResponse result) { + if (result.getServerSaslCreds() == null || result.getServerSaslCreds().length == 0) { + throw new IllegalArgumentException("Bind response missing server SASL credentials"); + } + + message = LdapUtils.utf8Encode(result.getServerSaslCreds(), false); + final Map attributes = Stream.of(message.split(",")) + .map(s -> s.split("=", 2)).collect(Collectors.toMap(attr -> attr[0], attr -> attr[1])); + + final String r = attributes.get("r"); + if (r == null) { + throw new IllegalArgumentException("Invalid SASL credentials, missing server nonce"); + } + if (!r.startsWith(clientFirstMessage.getNonce())) { + throw new IllegalArgumentException("Invalid SASL credentials, missing client nonce"); + } + combinedNonce = r; + + final String s = attributes.get("s"); + if (s == null) { + throw new IllegalArgumentException("Invalid SASL credentials, missing server salt"); + } + salt = LdapUtils.base64Decode(s); + + final String i = attributes.get("i"); + iterations = Integer.parseInt(i); + if (iterations < MINIMUM_ITERATION_COUNT) { + throw new IllegalArgumentException("Invalid SASL credentials, iterations minimum value is 4096"); + } + } + + + public String getMessage() { + return message; + } + + + public String getCombinedNonce() { + return combinedNonce; + } + + + public byte[] getSalt() { + return salt; + } + + + public int getIterations() { + return iterations; + } + } + + /** + * Verifies the final server message. + */ + static class ServerFinalMessage { + + /** + * Bytes for the server key hmac. + */ + private static final byte[] SERVER_KEY_INIT = LdapUtils.utf8Encode("Server Key"); + + /** + * Server SASL credentials. + */ + private final String message; + + /** + * Whether the server message was successfully verified. + */ + private final boolean verified; + + + /** + * Creates a new server final message. + * + * @param mech scram mechanism + * @param clientFinalMessage final message sent to the server + * @param result response to the final message + */ + ServerFinalMessage( + final Mechanism mech, + final ClientFinalMessage clientFinalMessage, + final BindResponse result) { + if (result.getServerSaslCreds() == null || result.getServerSaslCreds().length == 0) { + throw new IllegalArgumentException("Bind response missing server SASL credentials"); + } + + message = LdapUtils.utf8Encode(result.getServerSaslCreds(), false); + final Map attributes = Stream.of(message.split(",")) + .map(s -> s.split("=", 2)).collect(Collectors.toMap(attr -> attr[0], attr -> attr[1])); + + final String e = attributes.get("e"); + + if (result.getResultCode() != ResultCode.SUCCESS) { + verified = false; + } else { + final String serverSignature = attributes.get("v"); + if (serverSignature == null) { + throw new IllegalArgumentException("Invalid SASL credentials, missing server verification"); + } + + // compare the server signature in the message to what we expect + final byte[] serverKey = + createMac(mech.properties()[1], clientFinalMessage.getSaltedPassword()).doFinal(SERVER_KEY_INIT); + final String expectedServerSignature = LdapUtils.base64Encode( + createMac(mech.properties()[1], serverKey).doFinal( + LdapUtils.utf8Encode(clientFinalMessage.getMessage(), false))); + if (!expectedServerSignature.equals(serverSignature)) { + throw new IllegalArgumentException("Invalid SASL credentials, incorrect server verification"); + } + verified = true; + } + } + + + /** + * Returns whether the server final message was successfully verified. + * + * @return whether the server message was verified. + */ + public boolean isVerified() { + return verified; + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/Transport.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/Transport.java new file mode 100644 index 0000000..6a1a451 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/Transport.java @@ -0,0 +1,28 @@ + +package org.xbib.net.ldap.transport; + +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionConfig; + +/** + * Provides an abstraction layer for different {@link TransportConnection} implementations. + * + */ +public interface Transport { + + + /** + * Create a connection object. Implementations should not open a TCP socket in this method. + * + * @param cc connection configuration + * @return connection + */ + Connection create(ConnectionConfig cc); + + + /** + * Free any resources associated with this transport. + */ + default void close() { + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/TransportConnection.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/TransportConnection.java new file mode 100644 index 0000000..a14c4a9 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/TransportConnection.java @@ -0,0 +1,229 @@ + +package org.xbib.net.ldap.transport; + +import java.time.Instant; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import org.xbib.net.ldap.ActivePassiveConnectionStrategy; +import org.xbib.net.ldap.ConnectException; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.ConnectionStrategy; +import org.xbib.net.ldap.InitialRetryMetadata; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapURL; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.RetryMetadata; +import org.xbib.net.ldap.UnbindRequest; + +/** + * Base class for connection implementations. + * + */ +public abstract class TransportConnection implements Connection { + + /** + * Only one invocation of open can occur at a time. + */ + protected final ReentrantLock openLock = new ReentrantLock(); + + /** + * Only one invocation of close can occur at a time. + */ + protected final ReentrantLock closeLock = new ReentrantLock(); + + /** + * Provides host connection configuration. + */ + protected final ConnectionConfig connectionConfig; + /** + * Connection strategy for this connection. Default value is {@link ActivePassiveConnectionStrategy}. + */ + private final ConnectionStrategy connectionStrategy; + /** + * Time of the last successful open for this connection. + */ + protected Instant lastSuccessfulOpen; + + + /** + * Creates a new transport connection. + * + * @param config connection configuration + */ + public TransportConnection(final ConnectionConfig config) { + if (config == null) { + throw new NullPointerException("Connection config cannot be null"); + } + connectionConfig = config; + connectionStrategy = connectionConfig.getConnectionStrategy(); + synchronized (connectionStrategy) { + if (!connectionStrategy.isInitialized()) { + connectionStrategy.initialize(connectionConfig.getLdapUrl(), new Predicate<>() { + @Override + public boolean test(final LdapURL url) { + return TransportConnection.this.test(url); + } + + @Override + public String toString() { + return "DEFAULT_ACTIVATE_CONDITION"; + } + }); + } + } + } + + + @Override + public void open() + throws LdapException { + if (openLock.tryLock()) { + try { + if (isOpen()) { + throw new ConnectException(ResultCode.CONNECT_ERROR, "Connection is already open"); + } + final RetryMetadata metadata = new InitialRetryMetadata(lastSuccessfulOpen); + LdapException lastThrown; + do { + try { + strategyOpen(metadata); + lastThrown = null; + break; + } catch (LdapException e) { + lastThrown = e; + } + } while (lastThrown != null && connectionConfig.getAutoReconnectCondition().test(metadata)); + if (lastThrown != null) { + throw lastThrown; + } + if (isOpen()) { + lastSuccessfulOpen = Instant.now(); + } else { + throw new ConnectException(ResultCode.CONNECT_ERROR, "Channel closed immediately after open"); + } + } finally { + openLock.unlock(); + } + } else { + throw new LdapException(ResultCode.CONNECT_ERROR, "Open in progress"); + } + } + + + /** + * Method to support reopening a connection that was previously established. This method differs from {@link #open()} + * in that the autoReconnectCondition is tested before the open is attempted. + * + * @param metadata associated with this reopen + * @throws LdapException if the open fails + */ + protected void reopen(final RetryMetadata metadata) + throws LdapException { + if (openLock.tryLock()) { + try { + if (isOpen()) { + throw new ConnectException(ResultCode.CONNECT_ERROR, "Connection is already open"); + } + LdapException lastThrown = null; + while (connectionConfig.getAutoReconnectCondition().test(metadata)) { + try { + strategyOpen(metadata); + lastThrown = null; + break; + } catch (LdapException e) { + lastThrown = e; + } + } + if (lastThrown != null) { + throw lastThrown; + } + if (isOpen()) { + lastSuccessfulOpen = Instant.now(); + } + } finally { + openLock.unlock(); + } + } else { + throw new LdapException(ResultCode.CONNECT_ERROR, "Open in progress"); + } + } + + + /** + * Retrieves URLs from the connection strategy and attempts each one, in order, until a connection is made or the list + * is exhausted. + * + * @param metadata to track URL success and failure + * @throws LdapException if a connection cannot be established + */ + protected void strategyOpen(final RetryMetadata metadata) + throws LdapException { + boolean strategyProducedUrls = false; + LdapException lastThrown = null; + for (LdapURL url : connectionStrategy) { + strategyProducedUrls = true; + try { + open(url); + connectionStrategy.success(url); + metadata.recordSuccess(Instant.now()); + lastThrown = null; + break; + } catch (ConnectException e) { + connectionStrategy.failure(url); + lastThrown = e; + } + } + if (!strategyProducedUrls) { + throw new IllegalStateException("Connection strategy did not produce any LDAP URLs"); + } + if (lastThrown != null) { + metadata.recordFailure(Instant.now()); + throw lastThrown; + } + } + + + /** + * Determine whether the supplied URL is acceptable for use. + * + * @param url LDAP URL to test + * @return whether URL can be become active + */ + protected abstract boolean test(LdapURL url); + + + /** + * Attempt to open a connection to the supplied LDAP URL. + * + * @param url LDAP URL to connect to + * @throws LdapException if opening the connection fails + */ + protected abstract void open(LdapURL url) throws LdapException; + + + /** + * Executes an unbind operation. Clients should close connections using {@link #close()}. + * + * @param request unbind request + */ + protected abstract void operation(UnbindRequest request); + + + /** + * Write the request in the supplied handle to the LDAP server. This method does not throw, it should report + * exceptions to the handle. + * + * @param handle for the operation write + */ + protected abstract void write(DefaultOperationHandle handle); + + + /** + * Report that the supplied handle has completed. Allows the connection to clean up any resources associated with the + * handle. + * + * @param handle that has completed + */ + protected abstract void complete(DefaultOperationHandle handle); +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/TransportFactory.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/TransportFactory.java new file mode 100644 index 0000000..8f73898 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/TransportFactory.java @@ -0,0 +1,87 @@ + +package org.xbib.net.ldap.transport; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.ldap.ConnectionFactory; +import org.xbib.net.ldap.DefaultConnectionFactory; +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.PooledConnectionFactory; +import org.xbib.net.ldap.SingleConnectionFactory; +import org.xbib.net.ldap.transport.netty.ConnectionFactoryTransport; +import org.xbib.net.ldap.transport.netty.ConnectionTransport; + +/** + * Factory for creating connection transports. + * + */ +public final class TransportFactory { + + /** + * Ldap transport system property. + */ + private static final String POOLED_FACTORY_TRANSPORT_PROPERTY = "org.xbib.net.ldap.transport.pooledConnectionFactory"; + + /** + * Ldap transport system property. + */ + private static final String SINGLE_FACTORY_TRANSPORT_PROPERTY = "org.xbib.net.ldap.transport.singleConnectionFactory"; + + /** + * Map of connection factory class to transport constructor. + */ + private static final Map, Constructor> TRANSPORT_OVERRIDE; + + // initialize TRANSPORT_OVERRIDE + static { + final Map, Constructor> constructors = new HashMap<>(2); + final Constructor pooledTransport = LdapUtils.createConstructorFromProperty(POOLED_FACTORY_TRANSPORT_PROPERTY); + if (pooledTransport != null) { + constructors.put(PooledConnectionFactory.class, pooledTransport); + } + final Constructor singleTransport = LdapUtils.createConstructorFromProperty(SINGLE_FACTORY_TRANSPORT_PROPERTY); + if (singleTransport != null) { + constructors.put(SingleConnectionFactory.class, singleTransport); + } + TRANSPORT_OVERRIDE = Collections.unmodifiableMap(constructors); + } + + + /** + * Default constructor. + */ + private TransportFactory() { + } + + + /** + * The {@link #TRANSPORT_OVERRIDE} map is checked and that class is loaded if provided. Otherwise, the default + * transport for the supplied class is provided. + * + * @param clazz to return transport for + * @return transport + */ + public static Transport getTransport(final Class clazz) { + if (TRANSPORT_OVERRIDE.containsKey(clazz)) { + try { + return (Transport) TRANSPORT_OVERRIDE.get(clazz).newInstance(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + final Transport transport; + if (PooledConnectionFactory.class.isAssignableFrom(clazz)) { + transport = new ConnectionFactoryTransport(); + } else if (SingleConnectionFactory.class.isAssignableFrom(clazz)) { + transport = new ConnectionTransport.SingleThread(); + } else if (DefaultConnectionFactory.class.isAssignableFrom(clazz)) { + transport = new ConnectionTransport.SingleThread(); + } else { + // be conservative for unknown connection factory types + transport = new ConnectionTransport.SingleThread(); + } + return transport; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/AutoReadFlowControlHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/AutoReadFlowControlHandler.java new file mode 100644 index 0000000..c230d5b --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/AutoReadFlowControlHandler.java @@ -0,0 +1,39 @@ + +package org.xbib.net.ldap.transport.netty; + +import java.util.concurrent.atomic.AtomicInteger; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; + +/** + * Keeps a counter of messages that have been sent down the pipeline. That counter is decremented whenever a read is + * requested. A read is only propagated to the channel when all messages have completed. This handler is intended to be + * used with {@link NettyConnection.AutoReadEventHandler}. + * + */ +public class AutoReadFlowControlHandler extends ChannelDuplexHandler { + + /** + * Number of messages in the pipeline. + */ + private final AtomicInteger messageCount = new AtomicInteger(); + + + @Override + public void channelRead(final ChannelHandlerContext ctx, final Object msg) + throws Exception { + // increments the message count which blocks further reads until all inbound messages have been processed + messageCount.incrementAndGet(); + ctx.fireChannelRead(msg); + } + + + @Override + public void read(final ChannelHandlerContext ctx) + throws Exception { + // prevents outbound handlers from reading more data until all inbound messages have been read + if (messageCount.updateAndGet(i -> i > 0 ? i - 1 : 0) == 0) { + ctx.read(); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/ConnectionFactoryTransport.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/ConnectionFactoryTransport.java new file mode 100644 index 0000000..3c8931f --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/ConnectionFactoryTransport.java @@ -0,0 +1,99 @@ + +package org.xbib.net.ldap.transport.netty; + +/** + * Creates netty connections using the best fit event loop group based on the operating system. See {@link + * io.netty.channel.epoll.Epoll#isAvailable()} and {@link io.netty.channel.kqueue.KQueue#isAvailable()}. The event loop + * group is shutdown when the connection factory is closed. + * + */ +public class ConnectionFactoryTransport extends NettyConnectionFactoryTransport { + + + /** + * Creates a new connection factory transport. + */ + public ConnectionFactoryTransport() { + this(0); + } + + + /** + * Creates a new connection factory transport. + * + * @param ioThreads number of threads used for I/O in the event loop group + */ + public ConnectionFactoryTransport(final int ioThreads) { + this(ConnectionFactoryTransport.class.getSimpleName(), ioThreads); + } + + + /** + * Creates a new connection factory transport. + * + * @param name to assign the thread pool + * @param ioThreads number of threads used for I/O in the event loop group + */ + public ConnectionFactoryTransport(final String name, final int ioThreads) { + super( + NettyUtils.getDefaultSocketChannelType(), + NettyUtils.createDefaultEventLoopGroup(name + "-io", ioThreads), + null); + } + + + /** + * Creates a new connection factory transport. + * + * @param ioThreads number of threads used for I/O in the event loop group + * @param messageThreads number of threads for LDAP message handling in the event loop group + */ + public ConnectionFactoryTransport(final int ioThreads, final int messageThreads) { + this(ConnectionFactoryTransport.class.getSimpleName(), ioThreads, messageThreads); + } + + + /** + * Creates a new connection factory transport. + * + * @param name to assign the thread pool + * @param ioThreads number of threads used for I/O in the event loop group + * @param messageThreads number of threads for LDAP message handling in the event loop group + */ + public ConnectionFactoryTransport(final String name, final int ioThreads, final int messageThreads) { + super( + NettyUtils.getDefaultSocketChannelType(), + NettyUtils.createDefaultEventLoopGroup(name + "-io", ioThreads), + NettyUtils.createDefaultEventLoopGroup(name + "-messages", messageThreads)); + } + + + /** + * A {@link ConnectionFactoryTransport} configured with a single underlying thread. + */ + public static class SingleThread extends ConnectionFactoryTransport { + + + /** + * Default constructor. + */ + public SingleThread() { + super(SingleThread.class.getSimpleName(), 1); + } + } + + + /** + * A {@link ConnectionFactoryTransport} configured with two underlying threads. + */ + public static class DualThread extends ConnectionFactoryTransport { + + + /** + * Default constructor. + */ + public DualThread() { + super(DualThread.class.getSimpleName(), 2); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/ConnectionTransport.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/ConnectionTransport.java new file mode 100644 index 0000000..171f957 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/ConnectionTransport.java @@ -0,0 +1,137 @@ + +package org.xbib.net.ldap.transport.netty; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.transport.Transport; + +/** + * Creates netty connections using the best fit event loop group based on the operating system. See {@link + * io.netty.channel.epoll.Epoll#isAvailable()} and {@link io.netty.channel.kqueue.KQueue#isAvailable()}. The event loop + * group is shutdown when the connection is closed. + * + */ +public class ConnectionTransport implements Transport { + + /** + * Number of I/O threads. + */ + private final int numIoThreads; + + /** + * Number of message threads. + */ + private int numMessageThreads = -1; + + + /** + * Creates a new connection transport. + */ + public ConnectionTransport() { + this(0); + } + + + /** + * Creates a new connection transport. + * + * @param ioThreads number of threads used for I/O in the event loop group + */ + public ConnectionTransport(final int ioThreads) { + numIoThreads = ioThreads; + } + + + /** + * Creates a new connection transport. + * + * @param ioThreads number of threads used for I/O in the event loop group + * @param messageThreads number of threads for LDAP message handling in the event loop group + */ + public ConnectionTransport(final int ioThreads, final int messageThreads) { + numIoThreads = ioThreads; + numMessageThreads = messageThreads; + } + + + /** + * Returns the socket channel type used with the event loop group. + * + * @return socket channel type + */ + protected Class getSocketChannelType() { + return NettyUtils.getDefaultSocketChannelType(); + } + + + /** + * Returns a new event loop group with the supplied name and number of threads. + * + * @param name of the event loop group + * @param numThreads number of worker threads + * @return new event loop group + */ + protected EventLoopGroup createEventLoopGroup(final String name, final int numThreads) { + return NettyUtils.createDefaultEventLoopGroup(name, numThreads); + } + + + @Override + public Connection create(final ConnectionConfig cc) { + if (numMessageThreads != -1) { + return new NettyConnection( + cc, + getSocketChannelType(), + createEventLoopGroup(getClass().getSimpleName() + "@" + hashCode() + "-io", numIoThreads), + createEventLoopGroup(getClass().getSimpleName() + "@" + hashCode() + "-messages", numMessageThreads), + true); + } + return new NettyConnection( + cc, + getSocketChannelType(), + createEventLoopGroup(getClass().getSimpleName() + "@" + hashCode() + "-io", numIoThreads), + null, + true); + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "numIoThreads=" + numIoThreads + ", " + + "numMessageThreads=" + numMessageThreads + "]"; + } + + + /** + * A {@link ConnectionTransport} configured with a single underlying thread. + */ + public static class SingleThread extends ConnectionTransport { + + + /** + * Default constructor. + */ + public SingleThread() { + super(1); + } + } + + + /** + * A {@link ConnectionTransport} configured with two underlying threads. + */ + public static class DualThread extends ConnectionTransport { + + + /** + * Default constructor. + */ + public DualThread() { + super(2); + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/EncodedRequest.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/EncodedRequest.java new file mode 100644 index 0000000..38cdb6d --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/EncodedRequest.java @@ -0,0 +1,62 @@ + +package org.xbib.net.ldap.transport.netty; + +import org.xbib.net.ldap.LdapUtils; +import org.xbib.net.ldap.Request; + +/** + * Wrapper object that stores an encoded request with its message ID. + * + */ +public class EncodedRequest { + + /** + * Protocol message ID. + */ + private final int messageID; + + /** + * Encoded request. + */ + private final byte[] encoded; + + + /** + * Creates a new encoded request. + * + * @param id message ID + * @param request to encode + */ + public EncodedRequest(final int id, final Request request) { + messageID = id; + encoded = request.encode(messageID); + } + + + /** + * Returns the message ID. + * + * @return message ID + */ + public int getMessageID() { + return messageID; + } + + + /** + * Returns the encoded request. + * + * @return encoded request. + */ + public byte[] getEncoded() { + return encoded; + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "messageID=" + messageID + ", " + + "encoded=" + String.valueOf(LdapUtils.hexEncode(encoded)); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/HandleMap.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/HandleMap.java new file mode 100644 index 0000000..1701260 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/HandleMap.java @@ -0,0 +1,286 @@ + +package org.xbib.net.ldap.transport.netty; + +import java.time.Duration; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.extended.UnsolicitedNotification; +import org.xbib.net.ldap.transport.DefaultOperationHandle; + +/** + * Container for operation handles that are waiting on a response from the LDAP server. + * + */ +final class HandleMap { + /** + * Ldap netty transport system property. + */ + private static final String THROTTLE_REQUESTS_PROPERTY = "org.xbib.net.ldap.transport.netty.throttleRequests"; + + /** + * Ldap netty transport system property. + */ + private static final String THROTTLE_TIMEOUT_PROPERTY = "org.xbib.net.ldap.transport.netty.throttleTimeout"; + + /** + * If property is greater than zero, use the throttle semaphore. + */ + private static final int THROTTLE_REQUESTS = Integer.parseInt(System.getProperty(THROTTLE_REQUESTS_PROPERTY, "0")); + + /** + * Maximum time to wait for the throttle semaphore. Default is 60 seconds. + */ + private static final Duration THROTTLE_TIMEOUT = Duration.ofSeconds( + Long.parseLong(System.getProperty(THROTTLE_TIMEOUT_PROPERTY, "60"))); + + /** + * Map of message IDs to their operation handle. + */ + private final Map pending = new ConcurrentHashMap<>(); + + /** + * Only one notification can occur at a time. + */ + private final AtomicBoolean notificationLock = new AtomicBoolean(); + + /** + * Semaphore to throttle incoming requests. + */ + private final Semaphore throttle; + + /** + * Whether this queue is currently accepting new handles. + */ + private boolean open; + + + /** + * Creates a new handle map. + */ + HandleMap() { + if (THROTTLE_REQUESTS > 0) { + throttle = new Semaphore(THROTTLE_REQUESTS); + } else { + throttle = null; + } + } + + + /** + * Open this queue to receive new handles. + */ + public void open() { + open = true; + } + + + /** + * Close the queue to new handles. + */ + public void close() { + open = false; + } + + + /** + * Returns whether this handle map is open. + * + * @return is open + */ + public boolean isOpen() { + return open; + } + + + /** + * Returns the operation handle for the supplied message id. Returns null if this queue is not open. + * + * @param id message id + * @return operation handle or null + */ + public DefaultOperationHandle get(final int id) { + return open ? pending.get(id) : null; + } + + + /** + * Removes the operation handle from the supplied message id. Returns null if this queue is not open. + * + * @param id message id + * @return operation handle or null + */ + public DefaultOperationHandle remove(final int id) { + if (open) { + final DefaultOperationHandle handle = pending.remove(id); + releaseThrottle(1); + return handle; + } + return null; + } + + + /** + * Puts the supplied operation handle into the queue if the supplied id doesn't already exist in the queue. + * + * @param id message id + * @param handle to put + * @return null or existing operation handle for the id + * @throws LdapException if this queue is not open + */ + public DefaultOperationHandle put(final int id, final DefaultOperationHandle handle) + throws LdapException { + if (!open) { + throw new LdapException(ResultCode.CONNECT_ERROR, "Connection is closed, could not store handle " + handle); + } + acquireThrottle(); + return pending.putIfAbsent(id, handle); + } + + + /** + * Returns all the operation handles in the queue. + * + * @return all operation handles + */ + public Collection handles() { + return pending.values(); + } + + + /** + * Returns the size of this queue. + * + * @return queue size + */ + public int size() { + return pending.size(); + } + + + /** + * Removes all operation handles from the queue. + */ + public void clear() { + releaseThrottle(pending.size()); + pending.clear(); + } + + + /** + * Attempt to acquire the throttle semaphore. No-op if throttling is not enabled. + * + * @throws LdapException if the semaphore cannot be acquired or the thread is interrupted + */ + private void acquireThrottle() + throws LdapException { + if (throttle != null) { + try { + if (!throttle.tryAcquire(THROTTLE_TIMEOUT.toSeconds(), TimeUnit.SECONDS)) { + throw new LdapException(ResultCode.LOCAL_ERROR, "Could not acquire request semaphore"); + } + } catch (InterruptedException e) { + throw new LdapException(ResultCode.LOCAL_ERROR, "Could not acquire request semaphore", e); + } + } + } + + + /** + * Release permits on the throttle semaphore. No-op if throttling is not enabled. + * + * @param permits number of permits to release + */ + private void releaseThrottle(final int permits) { + if (throttle != null) { + throttle.release(permits); + } + } + + + /** + * Invokes {@link DefaultOperationHandle#abandon()} for all handles that have sent a request but not received a + * response. This method removes all handles from the queue. + */ + public void abandonRequests() { + if (notificationLock.compareAndSet(false, true)) { + try { + final Iterator i = pending.values().iterator(); + while (i.hasNext()) { + final DefaultOperationHandle h = i.next(); + if (h.getSentTime() != null && h.getReceivedTime() == null) { + i.remove(); + releaseThrottle(1); + h.abandon(); + } + } + } finally { + notificationLock.set(false); + } + } else { + // + } + } + + + /** + * Notifies all operation handles in the queue that an exception has occurred. See {@link + * DefaultOperationHandle#exception(LdapException)}. This method removes all handles from the queue. + * + * @param e exception to provides to handles + */ + public void notifyOperationHandles(final LdapException e) { + if (notificationLock.compareAndSet(false, true)) { + try { + final Iterator i = pending.values().iterator(); + while (i.hasNext()) { + final DefaultOperationHandle h = i.next(); + i.remove(); + releaseThrottle(1); + h.exception(e); + } + } finally { + notificationLock.set(false); + } + } else { + // + } + } + + + /** + * Send the supplied notification to all handles waiting for a response. + * + * @param notification to send to response handles + */ + public void notifyOperationHandles(final UnsolicitedNotification notification) { + if (notificationLock.compareAndSet(false, true)) { + try { + pending.values().forEach(h -> { + if (h.getSentTime() != null && h.getReceivedTime() == null) { + h.unsolicitedNotification(notification); + } + }); + } finally { + notificationLock.set(false); + } + } else { + // + } + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "open=" + open + ", " + + "throttle=" + throttle + ", " + + "handles=" + pending; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/MessageFrameDecoder.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/MessageFrameDecoder.java new file mode 100644 index 0000000..9b0c4da --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/MessageFrameDecoder.java @@ -0,0 +1,72 @@ + +package org.xbib.net.ldap.transport.netty; + +import java.util.List; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import org.xbib.asn1.DERBuffer; +import org.xbib.asn1.DERParser; +import org.xbib.asn1.UniversalDERTag; + +/** + * Reads the input byte buffer until an entire message is available. + * + */ +public class MessageFrameDecoder extends ByteToMessageDecoder { + + + @Override + protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) { + if (in.readableBytes() <= 2) { + return; + } + + final int readerIdx = in.readerIndex(); + final int writerIdx = in.writerIndex(); + int len = 0; + try { + final DERBuffer buffer = new NettyDERBuffer(in.readSlice(in.readableBytes())); + len = readMessageLength(buffer); + } finally { + // return the reader and writer indexes back to their initial position + in.setIndex(readerIdx, writerIdx); + } + if (len > 0) { + final ByteBuf retained = in.readRetainedSlice(len); + out.add(retained); + if (ctx != null) { + ctx.fireUserEventTriggered(NettyConnection.MessageStatus.READ); + } + } + } + + + /** + * Inspects the supplied buffer for a {@link UniversalDERTag#SEQ} tag and confirms the buffer contains enough bytes + * for the length specified for the tag. + * + * @param buffer to read + * @return DER message length + * @throws IllegalArgumentException if the buffer doesn't contain a SEQ tag + */ + private int readMessageLength(final DERBuffer buffer) { + final DERParser messageParser = new DERParser(); + final int tag = messageParser.readTag(buffer).getTagNo(); + if (UniversalDERTag.SEQ.getTagNo() != tag) { + throw new IllegalArgumentException("Invalid message tag: " + tag); + } + try { + final int len = messageParser.readLength(buffer); + if (buffer.position() + len <= buffer.capacity()) { + return buffer.position() + len; + } + } catch (IndexOutOfBoundsException e) { + // it's possible to receive a multi-byte length without all the bytes + // don't log that outcome as a warning + } catch (Exception e) { + // + } + return -1; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyConnection.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyConnection.java new file mode 100644 index 0000000..0961380 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyConnection.java @@ -0,0 +1,1561 @@ + +package org.xbib.net.ldap.transport.netty; + +import java.net.InetSocketAddress; +import java.security.GeneralSecurityException; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.SimpleUserEventChannelHandler; +import io.netty.channel.SingleThreadEventLoop; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ScheduledFuture; +import org.xbib.net.ldap.AbandonRequest; +import org.xbib.net.ldap.AbstractRequestMessage; +import org.xbib.net.ldap.AddRequest; +import org.xbib.net.ldap.AddResponse; +import org.xbib.net.ldap.BindRequest; +import org.xbib.net.ldap.BindResponse; +import org.xbib.net.ldap.ClosedRetryMetadata; +import org.xbib.net.ldap.CompareRequest; +import org.xbib.net.ldap.CompareResponse; +import org.xbib.net.ldap.ConnectException; +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.ConnectionInitializer; +import org.xbib.net.ldap.ConnectionValidator; +import org.xbib.net.ldap.DeleteRequest; +import org.xbib.net.ldap.DeleteResponse; +import org.xbib.net.ldap.LdapEntry; +import org.xbib.net.ldap.LdapException; +import org.xbib.net.ldap.LdapURL; +import org.xbib.net.ldap.Message; +import org.xbib.net.ldap.ModifyDnRequest; +import org.xbib.net.ldap.ModifyDnResponse; +import org.xbib.net.ldap.ModifyRequest; +import org.xbib.net.ldap.ModifyResponse; +import org.xbib.net.ldap.Result; +import org.xbib.net.ldap.ResultCode; +import org.xbib.net.ldap.SearchRequest; +import org.xbib.net.ldap.SearchResultReference; +import org.xbib.net.ldap.UnbindRequest; +import org.xbib.net.ldap.control.RequestControl; +import org.xbib.net.ldap.extended.ExtendedRequest; +import org.xbib.net.ldap.extended.ExtendedResponse; +import org.xbib.net.ldap.extended.IntermediateResponse; +import org.xbib.net.ldap.extended.StartTLSRequest; +import org.xbib.net.ldap.extended.UnsolicitedNotification; +import org.xbib.net.ldap.sasl.DefaultSaslClientRequest; +import org.xbib.net.ldap.sasl.QualityOfProtection; +import org.xbib.net.ldap.sasl.SaslClient; +import org.xbib.net.ldap.sasl.SaslClientRequest; +import org.xbib.net.ldap.ssl.HostnameResolver; +import org.xbib.net.ldap.ssl.HostnameVerifierAdapter; +import org.xbib.net.ldap.ssl.SSLContextInitializer; +import org.xbib.net.ldap.ssl.SslConfig; +import org.xbib.net.ldap.transport.DefaultCompareOperationHandle; +import org.xbib.net.ldap.transport.DefaultExtendedOperationHandle; +import org.xbib.net.ldap.transport.DefaultOperationHandle; +import org.xbib.net.ldap.transport.DefaultSaslClient; +import org.xbib.net.ldap.transport.DefaultSearchOperationHandle; +import org.xbib.net.ldap.transport.ResponseParser; +import org.xbib.net.ldap.transport.TransportConnection; + +/** + * Netty based connection implementation. + * + */ +public final class NettyConnection extends TransportConnection { + + /** + * Request encoder pipeline handler. + */ + private static final RequestEncoder REQUEST_ENCODER = new RequestEncoder(); + + /** + * Inbound handler to read the next message if autoRead is false. + */ + private static final AutoReadEventHandler READ_NEXT_MESSAGE = new AutoReadEventHandler(); + + /** + * Type of channel. + */ + private final Class channelType; + + /** + * Event worker group used to process I/O. + */ + private final EventLoopGroup ioWorkerGroup; + + /** + * Event worker group used to process inbound messages. + */ + private final EventLoopGroup messageWorkerGroup; + + /** + * Whether to shutdown the event loop groups on {@link #close()}. + */ + private final boolean shutdownOnClose; + + /** + * Netty channel configuration options. + */ + private final Map channelOptions; + + /** + * Queue holding requests that haven't received a response. + */ + private final HandleMap pendingResponses; + + /** + * Listener notified when the connection is closed. + */ + private final CloseFutureListener closeListener = new CloseFutureListener(); + + /** + * Message ID counter, incremented as requests are sent. + */ + private final AtomicInteger messageID = new AtomicInteger(1); + + /** + * Block operations while a reconnect is occurring. + */ + private final ReentrantReadWriteLock reconnectLock = new ReentrantReadWriteLock(); + + /** + * Operation lock when a bind occurs. + */ + private final ReentrantReadWriteLock bindLock = new ReentrantReadWriteLock(); + + /** + * Executor for scheduling various connection related tasks that cannot or should not be handled by the netty + * event loop groups. Reconnects in particular require a dedicated thread as the event loop group may be shared or may + * not be configured with enough threads to handle the task. + */ + private ExecutorService connectionExecutor; + + /** + * URL derived from the connection strategy. + */ + private LdapURL ldapURL; + + /** + * Connection to the LDAP server. + */ + private Channel channel; + + /** + * Time this connection was successfully established, null if the connection is not open. + */ + private Instant connectTime; + + /** + * Last exception received on the inbound pipeline. + */ + private Throwable inboundException; + + + /** + * Creates a new connection. Netty supports various transport implementations including NIO, EPOLL, KQueue, etc. The + * class type and event loop group are tightly coupled in this regard. + * + * @param config connection configuration + * @param type type of channel + * @param ioGroup event loop group that handles I/O and supports the channel type, cannot be null + * @param messageGroup event loop group that handles inbound messages, can be null + * @param shutdownGroups whether to shutdown the event loop groups when the connection is closed + */ + public NettyConnection( + final ConnectionConfig config, + final Class type, + final EventLoopGroup ioGroup, + final EventLoopGroup messageGroup, + final boolean shutdownGroups) { + super(config); + if (ioGroup == null) { + throw new NullPointerException("I/O worker group cannot be null"); + } + channelType = type; + ioWorkerGroup = ioGroup; + messageWorkerGroup = messageGroup; + channelOptions = new HashMap<>(); + channelOptions.put(ChannelOption.SO_KEEPALIVE, true); + channelOptions.put(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) config.getConnectTimeout().toMillis()); + if (config.getTransportOptions() != null && !config.getTransportOptions().isEmpty()) { + for (Map.Entry e : config.getTransportOptions().entrySet()) { + final ChannelOption option = ChannelOption.valueOf(e.getKey()); + final Object value = e.getValue(); + if (value instanceof String) { + channelOptions.put(option, convertChannelOption((String) value)); + } else { + channelOptions.put(option, value); + } + } + } + shutdownOnClose = shutdownGroups; + pendingResponses = new HandleMap(); + } + + + /** + * Performs a best effort at converting a channel option value to the correct type. Handles Boolean and Integer types. + * + * @param value to convert + * @return converted value or the supplied value if no conversion occurred + */ + private Object convertChannelOption(final String value) { + if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) { + return Boolean.valueOf(value); + } else { + try { + return Integer.parseInt(value); + } catch (NumberFormatException ignored) { + } + } + return value; + } + + + /** + * Creates a Netty {@link Bootstrap} with the supplied client initializer. + * + * @param initializer to provide to the bootstrap + * @return Netty bootstrap + */ + @SuppressWarnings("unchecked") + private Bootstrap createBootstrap(final ClientInitializer initializer) { + if (ioWorkerGroup.isShutdown()) { + throw new IllegalStateException("Attempt to open connection with shutdown event loop on " + this); + } + final Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(ioWorkerGroup); + bootstrap.channel(channelType); + channelOptions.forEach(bootstrap::option); + bootstrap.handler(initializer); + return bootstrap; + } + + + @Override + protected boolean test(final LdapURL url) { + final NettyConnection conn = new NettyConnection( + connectionConfig, + channelType, + ioWorkerGroup, + messageWorkerGroup, + false); + try { + conn.open(url); + return true; + } catch (LdapException e) { + return false; + } finally { + conn.close(); + } + } + + + @Override + protected void open(final LdapURL url) + throws LdapException { + if (isOpen()) { + throw new IllegalStateException("Connection is already open"); + } + if (openLock.tryLock()) { + try { + inboundException = null; + ldapURL = url; + if (connectionExecutor == null) { + connectionExecutor = Executors.newCachedThreadPool( + r -> { + final Thread t = new Thread(r, getClass().getSimpleName() + "@" + hashCode()); + t.setDaemon(true); + return t; + }); + } + channel = connectInternal(); + channel.closeFuture().addListener(closeListener); + pendingResponses.open(); + openInitialize(url); + connectTime = Instant.now(); + } finally { + openLock.unlock(); + } + } else { + throw new ConnectException(ResultCode.CONNECT_ERROR, "Open in progress"); + } + } + + + /** + * Initializes this connection for use after it has been established. If startTLS is configured it will be performed. + * Any configured connection initializers are invoked. + * + * @param url LDAP URL to connect to + * @throws LdapException if initializing the connection fails + */ + private void openInitialize(final LdapURL url) + throws LdapException { + try { + // startTLS request must occur after the connection is ready + if (connectionConfig.getUseStartTLS()) { + final Result result = operation(new StartTLSRequest()); + if (!result.isSuccess()) { + throw new ConnectException( + ResultCode.CONNECT_ERROR, + "StartTLS returned response: " + result + " for URL " + url); + } + } + // initialize the connection + if (connectionConfig.getConnectionInitializers() != null) { + for (ConnectionInitializer initializer : connectionConfig.getConnectionInitializers()) { + final Result result = initializer.initialize(this); + if (!result.isSuccess()) { + throw new ConnectException( + ResultCode.CONNECT_ERROR, + "Connection initializer " + initializer + " returned response: " + result + " for URL " + url); + } + } + } + } catch (Exception e) { + try { + notifyOperationHandlesOfClose(); + pendingResponses.close(); + if (isOpen()) { + channel.closeFuture().removeListener(closeListener); + channel.close().addListener(new LogFutureListener()); + } + } finally { + pendingResponses.clear(); + channel = null; + } + throw e; + } + } + + + @Override + public LdapURL getLdapURL() { + return ldapURL; + } + + + /** + * Creates a Netty bootstrap and connects to the LDAP server. Handles the details of adding an SSL handler to the + * pipeline. This method waits until the connection is established. + * + * @return channel for the established connection. + * @throws ConnectException if the connection fails + */ + private Channel connectInternal() + throws ConnectException { + final ClientInitializer initializer = createClientInitializer(); + final ChannelFuture future = connectBootstrap(initializer); + waitForConnectionEstablish(initializer, future); + return future.channel(); + } + + + /** + * Creates a new client initializer. If the {@link #ldapURL} is LDAPS an SSL handler is added to the client + * initializer. + * + * @return client initializer + * @throws ConnectException if the SSL engine cannot be initialized + */ + private ClientInitializer createClientInitializer() + throws ConnectException { + SslHandler handler = null; + if (ldapURL.getScheme().equals("ldaps")) { + try { + handler = createSslHandler(connectionConfig); + } catch (SSLException e) { + throw new ConnectException(ResultCode.CONNECT_ERROR, e); + } + } + return new ClientInitializer(handler); + } + + + /** + * Creates a Netty SSL handler using the supplied connection config. + * + * @param config containing SSL config + * @return SSL handler + * @throws SSLException if the SSL engine cannot be initialized + */ + private SslHandler createSslHandler(final ConnectionConfig config) + throws SSLException { + final SslConfig sc = config.getSslConfig() != null ? + SslConfig.copy(config.getSslConfig()) : new SslConfig(); + final SSLContext ctx; + try { + final SSLContextInitializer initializer = sc.createSSLContextInitializer(); + ctx = initializer.initSSLContext("TLS"); + } catch (GeneralSecurityException e) { + throw new SSLException("Could not initialize SSL context", e); + } + final SSLEngine engine = ctx.createSSLEngine(ldapURL.getHostname(), ldapURL.getPort()); + engine.setUseClientMode(true); + if (sc.getEnabledProtocols() != null) { + engine.setEnabledProtocols(sc.getEnabledProtocols()); + } + if (sc.getEnabledCipherSuites() != null) { + engine.setEnabledCipherSuites(sc.getEnabledCipherSuites()); + } + if (sc.getHostnameVerifier() == null) { + final SSLParameters sslParams = engine.getSSLParameters(); + sslParams.setEndpointIdentificationAlgorithm("LDAPS"); + engine.setSSLParameters(sslParams); + } + final SslHandler handler = new SslHandler(engine); + handler.setHandshakeTimeout(sc.getHandshakeTimeout().toMillis(), TimeUnit.MILLISECONDS); + return handler; + } + + + /** + * Creates a new bootstrap with the supplied initializer and uses that bootstrap to connect to the ldapURL. + * + * @param initializer to create bootstrap with + * @return channel future produced from the connect invocation + */ + private ChannelFuture connectBootstrap(final ClientInitializer initializer) { + final Bootstrap bootstrap = createBootstrap(initializer); + final ChannelFuture future; + if (ldapURL.getInetAddress() != null) { + future = bootstrap.connect(ldapURL.getInetAddress(), ldapURL.getPort()); + } else { + future = bootstrap.connect(new InetSocketAddress(ldapURL.getHostname(), ldapURL.getPort())); + } + return future; + } + + + /** + * Waits until the TCP connection has completed. + * + * @param initializer used to determine whether to wait for the SSL handshake to complete + * @param future to wait on + * @throws ConnectException if the connection fails + */ + private void waitForConnectionEstablish(final ClientInitializer initializer, final ChannelFuture future) + throws ConnectException { + final CountDownLatch channelLatch = new CountDownLatch(1); + future.addListener((ChannelFutureListener) f -> channelLatch.countDown()); + try { + // wait until the connection future is complete + // note that the wait time is controlled by the connectTimeout property in ConnectionConfig + // if a deadlock occurs here, there may not be enough threads available in the worker group + if (!channelLatch.await(connectionConfig.getConnectTimeout().multipliedBy(2).toMillis(), TimeUnit.MILLISECONDS)) { + future.cancel(true); + } + } catch (InterruptedException e) { + future.cancel(true); + } + if (future.isCancelled()) { + throw new ConnectException(ResultCode.CONNECT_ERROR, "Connection cancelled"); + } + if (!future.isSuccess()) { + if (future.cause() != null) { + throw new ConnectException(ResultCode.SERVER_DOWN, future.cause()); + } else { + throw new ConnectException(ResultCode.SERVER_DOWN, "Connection could not be opened"); + } + } + + if (initializer.isSsl()) { + // socket is connected, wait for SSL handshake to complete + try { + waitForSSLHandshake(future.channel()); + } catch (SSLException e) { + future.channel().close(); + throw new ConnectException(ResultCode.CONNECT_ERROR, e); + } + } + } + + + /** + * Waits until the SSL handshake has completed. + * + * @param ch that the handshake is occurring on + * @throws SSLException if the handshake fails + */ + private void waitForSSLHandshake(final Channel ch) + throws SSLException { + // socket is connected, wait for SSL handshake to complete + final CountDownLatch sslLatch = new CountDownLatch(1); + final SslHandler handler = ch.pipeline().get(SslHandler.class); + final Future sslFuture = handler.handshakeFuture(); + sslFuture.addListener(f -> sslLatch.countDown()); + try { + // wait until the connection future is complete + // note that the wait time is controlled by the handshakeTimeout property in SslConfig + if (!sslLatch.await(handler.getHandshakeTimeoutMillis() * 2, TimeUnit.MILLISECONDS)) { + sslFuture.cancel(true); + } + } catch (InterruptedException e) { + sslFuture.cancel(true); + } + if (sslFuture.isCancelled()) { + throw new SSLException("SSL handshake cancelled"); + } + if (!sslFuture.isSuccess()) { + final SSLException sslEx; + if (sslFuture.cause() != null) { + sslEx = new SSLException(sslFuture.cause()); + } else { + sslEx = new SSLException("SSL handshake failure"); + } + if (inboundException != null) { + sslEx.addSuppressed(inboundException); + } + throw sslEx; + } + if (connectionConfig.getSslConfig() != null && connectionConfig.getSslConfig().getHostnameVerifier() != null) { + final HostnameVerifier verifier = new HostnameVerifierAdapter( + connectionConfig.getSslConfig().getHostnameVerifier()); + final SSLSession session = handler.engine().getSession(); + final HostnameResolver resolver = new HostnameResolver(session); + final String hostname = resolver.resolve(); + if (!verifier.verify(hostname, session)) { + throw new SSLPeerUnverifiedException("Hostname verification failed for " + hostname + " using " + verifier); + } + } + } + + + /** + * Performs a startTLS operation. This method can only be invoked when a connection is opened. + * + * @param request to send + * @return result of the startTLS operation + * @throws LdapException if the operation fails + */ + Result operation(final StartTLSRequest request) + throws LdapException { + throwIfClosed(); + if (channel.pipeline().get(SslHandler.class) != null) { + throw new ConnectException(ResultCode.LOCAL_ERROR, "SslHandler is already in use"); + } + final DefaultExtendedOperationHandle handle = new DefaultExtendedOperationHandle( + request, + this, + request.getResponseTimeout() != null ? request.getResponseTimeout() : connectionConfig.getStartTLSTimeout()); + final Result result; + try { + result = handle.execute(); + } catch (LdapException e) { + throw new ConnectException(ResultCode.CONNECT_ERROR, "StartTLS operation failed", e); + } + if (result.isSuccess()) { + try { + channel.pipeline().addFirst("ssl", createSslHandler(connectionConfig)); + waitForSSLHandshake(channel); + } catch (SSLException e) { + throw new ConnectException(ResultCode.CONNECT_ERROR, e); + } + } else { + throw new ConnectException(ResultCode.CONNECT_ERROR, "StartTLS operation failed with result " + result); + } + return result; + } + + + @Override + protected void operation(final UnbindRequest request) { + if (reconnectLock.readLock().tryLock()) { + try { + if (!isOpen()) { + } else { + if (bindLock.readLock().tryLock()) { + try { + final EncodedRequest encodedRequest = new EncodedRequest(getAndIncrementMessageID(), request); + channel.writeAndFlush(encodedRequest).addListener( + ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + } finally { + bindLock.readLock().unlock(); + } + } else { + throw new IllegalStateException("Bind in progress, cannot send unbind request"); + } + } + } finally { + reconnectLock.readLock().unlock(); + } + } + } + + + /** + * Performs a SASL bind operation that uses a custom client. + * + * @param request to send + * @return result of the GSS-API bind operation + * @throws LdapException if the operation fails or another bind is in progress + */ + @Override + @SuppressWarnings("unchecked") + public BindResponse operation(final SaslClientRequest request) + throws LdapException { + throwIfClosed(); + if (!bindLock.writeLock().tryLock()) { + throw new LdapException(ResultCode.LOCAL_ERROR, "Operation in progress, cannot send bind request"); + } + try { + final SaslClient client = request.getSaslClient(); + final BindResponse result; + try { + result = client.bind(this, request); + } catch (Exception e) { + if (e instanceof LdapException) { + throw (LdapException) e; + } else { + throw new LdapException(ResultCode.LOCAL_ERROR, e); + } + } + if (result == null) { + throw new LdapException(ResultCode.LOCAL_ERROR, "SASL operation failed"); + } + return result; + } finally { + bindLock.writeLock().unlock(); + } + } + + + /** + * Performs a SASL client bind operation. + * + * @param request to send + * @return result of the SASL client bind operation + * @throws LdapException if the operation fails or another bind is in progress + */ + @Override + @SuppressWarnings("unchecked") + public BindResponse operation(final DefaultSaslClientRequest request) + throws LdapException { + throwIfClosed(); + if (!bindLock.writeLock().tryLock()) { + throw new LdapException(ResultCode.LOCAL_ERROR, "Operation in progress, cannot send bind request"); + } + try { + final SaslClient client = request.getSaslClient(); + if (client instanceof DefaultSaslClient) { + final DefaultSaslClient defaultClient = (DefaultSaslClient) client; + final BindResponse response; + boolean saslSecurity = false; + try { + response = defaultClient.bind(this, request); + if (response.getResultCode() == ResultCode.SUCCESS) { + final QualityOfProtection qop = defaultClient.getQualityOfProtection(); + if (QualityOfProtection.AUTH_INT == qop || QualityOfProtection.AUTH_CONF == qop) { + if (channel.pipeline().get(SaslHandler.class) != null) { + channel.pipeline().remove(SaslHandler.class); + } + if (channel.pipeline().get(SslHandler.class) != null) { + channel.pipeline().addAfter("ssl", "sasl", new SaslHandler(defaultClient.getClient())); + } else { + channel.pipeline().addFirst("sasl", new SaslHandler(defaultClient.getClient())); + } + saslSecurity = true; + } + } + return response; + } catch (Exception e) { + throw new LdapException(ResultCode.LOCAL_ERROR, "SASL bind operation failed", e); + } finally { + if (!saslSecurity) { + defaultClient.dispose(); + } + } + } else { + final BindResponse result; + try { + result = client.bind(this, request); + } catch (Exception e) { + if (e instanceof LdapException) { + throw (LdapException) e; + } else { + throw new LdapException(ResultCode.LOCAL_ERROR, e); + } + } + if (result == null) { + throw new LdapException(ResultCode.LOCAL_ERROR, "SASL GSSAPI operation failed"); + } + return result; + } + } finally { + bindLock.writeLock().unlock(); + } + } + + + @Override + public void operation(final AbandonRequest request) { + final DefaultOperationHandle handle = pendingResponses.remove(request.getMessageID()); + if (reconnectLock.readLock().tryLock()) { + try { + if (!isOpen()) { + if (handle != null) { + handle.exception(new LdapException(ResultCode.SERVER_DOWN, "Connection is not open")); + } + } else { + if (bindLock.readLock().tryLock()) { + try { + final EncodedRequest encodedRequest = new EncodedRequest(getAndIncrementMessageID(), request); + channel.writeAndFlush(encodedRequest).addListener( + ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + } finally { + bindLock.readLock().unlock(); + } + } else { + if (handle != null) { + handle.exception(new LdapException(ResultCode.LOCAL_ERROR, "Bind in progress")); + } + } + } + } finally { + reconnectLock.readLock().unlock(); + } + } else { + if (handle != null) { + handle.exception(new LdapException(ResultCode.SERVER_DOWN, "Reconnect in progress")); + } + } + } + + + @Override + public DefaultOperationHandle operation(final AddRequest request) { + return new DefaultOperationHandle<>( + request, + this, + request.getResponseTimeout() != null ? request.getResponseTimeout() : connectionConfig.getResponseTimeout()); + } + + + @Override + public BindOperationHandle operation(final BindRequest request) { + if (request instanceof AbstractRequestMessage) { + return new BindOperationHandle( + request, + this, + ((AbstractRequestMessage) request).getResponseTimeout() != null ? + ((AbstractRequestMessage) request).getResponseTimeout() : connectionConfig.getResponseTimeout()); + } + return new BindOperationHandle(request, this, connectionConfig.getResponseTimeout()); + } + + + @Override + public DefaultCompareOperationHandle operation(final CompareRequest request) { + return new DefaultCompareOperationHandle( + request, + this, + request.getResponseTimeout() != null ? request.getResponseTimeout() : connectionConfig.getResponseTimeout()); + } + + + @Override + public DefaultOperationHandle operation(final DeleteRequest request) { + return new DefaultOperationHandle<>( + request, + this, + request.getResponseTimeout() != null ? request.getResponseTimeout() : connectionConfig.getResponseTimeout()); + } + + + @Override + public DefaultExtendedOperationHandle operation(final ExtendedRequest request) { + if (request instanceof StartTLSRequest) { + throw new IllegalArgumentException("StartTLS can only be invoked when the connection is opened"); + } + return new DefaultExtendedOperationHandle( + request, + this, + request.getResponseTimeout() != null ? request.getResponseTimeout() : connectionConfig.getResponseTimeout()); + } + + + @Override + public DefaultOperationHandle operation(final ModifyRequest request) { + return new DefaultOperationHandle<>( + request, + this, + request.getResponseTimeout() != null ? request.getResponseTimeout() : connectionConfig.getResponseTimeout()); + } + + + @Override + public DefaultOperationHandle operation(final ModifyDnRequest request) { + return new DefaultOperationHandle<>( + request, + this, + request.getResponseTimeout() != null ? request.getResponseTimeout() : connectionConfig.getResponseTimeout()); + } + + + @Override + public DefaultSearchOperationHandle operation(final SearchRequest request) { + return new DefaultSearchOperationHandle( + request, + this, + request.getResponseTimeout() != null ? request.getResponseTimeout() : connectionConfig.getResponseTimeout()); + } + + + @Override + @SuppressWarnings("unchecked") + protected void write(final DefaultOperationHandle handle) { + try { + final boolean gotReconnectLock; + if (Duration.ZERO.equals(connectionConfig.getReconnectTimeout())) { + reconnectLock.readLock().lock(); + gotReconnectLock = true; + } else { + gotReconnectLock = reconnectLock.readLock().tryLock( + connectionConfig.getReconnectTimeout().toMillis(), TimeUnit.MILLISECONDS); + } + if (gotReconnectLock) { + try { + if (!isOpen()) { + handle.exception(new LdapException(ResultCode.SERVER_DOWN, "Connection is closed, write aborted")); + } else { + if (bindLock.readLock().tryLock()) { + try { + final EncodedRequest encodedRequest = new EncodedRequest( + getAndIncrementMessageID(), + handle.getRequest()); + handle.messageID(encodedRequest.getMessageID()); + try { + if (pendingResponses.put(encodedRequest.getMessageID(), handle) != null) { + throw new LdapException( + ResultCode.ENCODING_ERROR, + "Request already exists for ID " + encodedRequest.getMessageID()); + } + } catch (LdapException e) { + if (inboundException != null) { + throw new LdapException(ResultCode.SERVER_DOWN, e.getMessage(), inboundException); + } + throw e; + } + channel.writeAndFlush(encodedRequest).addListeners( + ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE, + f -> { + if (f.isSuccess()) { + handle.sent(); + } + }); + } finally { + bindLock.readLock().unlock(); + } + } else { + handle.exception(new LdapException(ResultCode.LOCAL_ERROR, "Bind in progress")); + } + } + } finally { + reconnectLock.readLock().unlock(); + } + } else { + handle.exception(new LdapException(ResultCode.SERVER_DOWN, "Reconnect in progress")); + } + } catch (Exception e) { + if (e instanceof LdapException) { + handle.exception((LdapException) e); + } else { + handle.exception(new LdapException(ResultCode.LOCAL_ERROR, e)); + } + } + } + + + @Override + protected void complete(final DefaultOperationHandle handle) { + if (handle != null && handle.getMessageID() != null) { + pendingResponses.remove(handle.getMessageID()); + } + } + + + /** + * Returns the value of the next message ID and increments the counter. + * + * @return message ID + */ + int getAndIncrementMessageID() { + return messageID.getAndUpdate(i -> i < Integer.MAX_VALUE ? i + 1 : 1); + } + + + /** + * Returns the value of the next message ID. + * + * @return message ID + */ + int getMessageID() { + return messageID.get(); + } + + + /** + * Sets the value of the next message ID. + * + * @param i message ID + */ + void setMessageID(final int i) { + if (i < 1) { + throw new IllegalArgumentException("messageID must be greater than zero"); + } + messageID.set(i); + } + + + /** + * Returns the channel options. + * + * @return channel options + */ + Map getChannelOptions() { + return channelOptions; + } + + + /** + * Closes this connection. Abandons all pending responses and sends an unbind to the LDAP server if the connection is + * open when this method is invoked. + * + * @param controls to send with the unbind request when closing the connection + */ + @Override + public void close(final RequestControl... controls) { + if (closeLock.tryLock()) { + try { + pendingResponses.close(); + if (connectionExecutor != null) { + connectionExecutor.shutdown(); + } + if (isOpen()) { + channel.closeFuture().removeListener(closeListener); + // abandon outstanding requests + if (pendingResponses.size() > 0) { + pendingResponses.abandonRequests(); + } + // unbind + final UnbindRequest req = new UnbindRequest(); + req.setControls(controls); + operation(req); + if (channel != null) { + channel.close().addListener(new LogFutureListener()); + } + } else { + notifyOperationHandlesOfClose(); + } + } finally { + pendingResponses.clear(); + connectionExecutor = null; + channel = null; + connectTime = null; + if (shutdownOnClose) { + NettyUtils.shutdownGracefully(ioWorkerGroup); + if (messageWorkerGroup != null) { + NettyUtils.shutdownGracefully(messageWorkerGroup); + } + } + closeLock.unlock(); + } + } + } + + + /** + * Sends an exception notification to all pending responses that the connection has been closed. Since this invokes + * any configured exception handlers, notifications will use the {@link #messageWorkerGroup} if it is configured. + */ + private void notifyOperationHandlesOfClose() { + if (pendingResponses.size() > 0) { + final LdapException ex; + if (inboundException == null) { + ex = new LdapException(ResultCode.SERVER_DOWN, "Connection closed"); + } else if (inboundException instanceof LdapException) { + ex = (LdapException) inboundException; + } else { + ex = new LdapException(ResultCode.SERVER_DOWN, inboundException); + } + if (messageWorkerGroup != null) { + messageWorkerGroup.execute(() -> pendingResponses.notifyOperationHandles(ex)); + } else { + pendingResponses.notifyOperationHandles(ex); + } + } + } + + + /** + * Attempts to reestablish the channel for this connection. + * + * @throws IllegalStateException if the connection is open + */ + private void reconnect() { + if (isOpen()) { + throw new IllegalStateException("Reconnect cannot be invoked when the connection is open"); + } + if (isOpening()) { + notifyOperationHandlesOfClose(); + return; + } + if (isClosing()) { + notifyOperationHandlesOfClose(); + return; + } + if (!reconnectLock.isWriteLocked()) { + boolean gotReconnectLock; + try { + if (Duration.ZERO.equals(connectionConfig.getReconnectTimeout())) { + reconnectLock.writeLock().lock(); + gotReconnectLock = true; + } else { + gotReconnectLock = reconnectLock.writeLock().tryLock( + connectionConfig.getReconnectTimeout().toMillis(), TimeUnit.MILLISECONDS); + } + } catch (InterruptedException e) { + gotReconnectLock = false; + } + if (gotReconnectLock) { + List replayOperations = null; + try { + try { + reopen(new ClosedRetryMetadata(lastSuccessfulOpen, inboundException)); + } catch (Exception e) { + // + } + // replay operations that have been sent, but have not received a response + // notify all other operations + if (isOpen() && connectionConfig.getAutoReplay()) { + replayOperations = pendingResponses.handles().stream() + .filter(h -> h.getSentTime() != null && !h.hasConsumedMessage()) + .collect(Collectors.toList()); + replayOperations.forEach(h -> pendingResponses.remove(h.getMessageID())); + // notify outstanding requests that have received a response + notifyOperationHandlesOfClose(); + } else { + notifyOperationHandlesOfClose(); + } + } finally { + reconnectLock.writeLock().unlock(); + } + if (replayOperations != null && !replayOperations.isEmpty()) { + replayOperations.forEach(this::write); + } + } + } else { + throw new IllegalStateException("Reconnect is already in progress"); + } + } + + + /** + * Returns whether the underlying Netty channel is open. See {@link Channel#isOpen()}. + * + * @return whether the Netty channel is open + */ + public boolean isOpen() { + return channel != null && channel.isOpen(); + } + + + /** + * Returns whether this connection is currently attempting to open. + * + * @return whether the Netty channel is in the process of opening + */ + private boolean isOpening() { + if (openLock.tryLock()) { + try { + return false; + } finally { + openLock.unlock(); + } + } else { + return true; + } + } + + + /** + * Returns whether this connection is currently attempting to close. + * + * @return whether the Netty channel is in the process of closing + */ + private boolean isClosing() { + if (closeLock.tryLock()) { + try { + return false; + } finally { + closeLock.unlock(); + } + } else { + return true; + } + } + + + /** + * Throws an exception if the Netty channel is closed. See {@link #isOpen()}. + * + * @throws LdapException if the connection is closed + */ + private void throwIfClosed() + throws LdapException { + if (!isOpen()) { + throw new LdapException(ResultCode.SERVER_DOWN, "Connection is closed"); + } + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "ldapUrl=" + ldapURL + ", " + + "isOpen=" + isOpen() + ", " + + "connectTime=" + connectTime + ", " + + "connectionConfig=" + connectionConfig + ", " + + "channel=" + channel; + } + + + /** + * Enum that describes the state of an LDAP message in the pipeline. + */ + protected enum MessageStatus { + /** + * All bytes for a message have been read. + */ + READ, + + /** + * Bytes have been decoded into a concrete message. + */ + DECODED, + + /** + * Message has passed through the entire pipeline. + */ + COMPLETE, + } + + /** + * Encodes an LDAP request into its DER bytes. See {@link EncodedRequest#getEncoded()}. This class prefers direct + * byte buffers. + */ + @ChannelHandler.Sharable + protected static class RequestEncoder extends MessageToByteEncoder { + + @Override + protected void encode(final ChannelHandlerContext ctx, final EncodedRequest msg, final ByteBuf out) { + out.writeBytes(msg.getEncoded()); + } + + + @Override + protected ByteBuf allocateBuffer( + final ChannelHandlerContext ctx, + final EncodedRequest msg, + final boolean preferDirect) { + final int msgSize = msg.getEncoded().length; + if (preferDirect) { + return ctx.alloc().ioBuffer(msgSize); + } else { + return ctx.alloc().heapBuffer(msgSize); + } + } + } + + /** + * Decodes byte buffer into a concrete LDAP response message. See {@link ResponseParser}. Note that {@link + * ByteToMessageDecoder} is stateful so this class cannot be marked sharable. + */ + protected static class MessageDecoder extends ByteToMessageDecoder { + + + @Override + protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) + throws LdapException { + final ResponseParser parser = new ResponseParser(); + final Message message = parser.parse(new NettyDERBuffer(in)) + .orElseThrow(() -> new LdapException(ResultCode.DECODING_ERROR, "No response found")); + out.add(message); + if (ctx != null) { + ctx.fireUserEventTriggered(MessageStatus.DECODED); + } + } + } + + /** + * Initiates a channel read when an LDAP message has been processed and auto read is false. This handler also + * initiates a channel read when it becomes active to bootstrap the initial read. + */ + @ChannelHandler.Sharable + protected static class AutoReadEventHandler extends SimpleUserEventChannelHandler { + + @Override + public void channelActive(final ChannelHandlerContext ctx) + throws Exception { + // invoking ctx.channel().read() starts at the tail of the pipeline + ctx.channel().read(); + ctx.fireChannelActive(); + } + + + @Override + protected void eventReceived(final ChannelHandlerContext ctx, final MessageStatus evt) { + if (MessageStatus.COMPLETE == evt) { + // invoking ctx.read() starts at this handler + ctx.read(); + } + } + } + + /** + * Bind specific operation handle that locks other operations until the bind completes. + */ + public class BindOperationHandle extends DefaultOperationHandle { + + + /** + * Creates a new bind operation handle. + * + * @param req bind request to expect a response for + * @param conn the request will be executed on + * @param timeout duration to wait for a response + */ + BindOperationHandle(final BindRequest req, final TransportConnection conn, final Duration timeout) { + super(req, conn, timeout); + } + + + @Override + public BindOperationHandle send() { + throw new UnsupportedOperationException("Bind requests are synchronous, invoke execute"); + } + + + @Override + public BindResponse await() { + throw new UnsupportedOperationException("Bind requests are synchronous, invoke execute"); + } + + + @Override + public BindResponse execute() + throws LdapException { + if (bindLock.writeLock().tryLock()) { + try { + super.send(); + return super.await(); + } finally { + bindLock.writeLock().unlock(); + } + } else { + throw new IllegalStateException("Operation in progress, cannot send bind request"); + } + } + } + + /** + * Listener that logs the future success state when it occurs. + */ + private final class LogFutureListener implements ChannelFutureListener { + + + @Override + public void operationComplete(final ChannelFuture future) { + } + } + + /** + * Listener for channel close events. If {@link ConnectionConfig#getAutoReconnect()} is true, a connection reconnect + * is attempted on a separate thread. + */ + private final class CloseFutureListener implements ChannelFutureListener { + + /** + * Whether this listener is in the process of reconnecting. + */ + private final AtomicBoolean reconnecting = new AtomicBoolean(); + + + @Override + public void operationComplete(final ChannelFuture future) { + inboundException = future.cause(); + if (connectionConfig.getAutoReconnect() && !isOpening() && !isClosing()) { + if (connectionExecutor != null && !connectionExecutor.isShutdown()) { + connectionExecutor.execute( + () -> { + if (reconnecting.compareAndSet(false, true)) { + try { + reconnect(); + } catch (Exception e) { + // + } finally { + reconnecting.set(false); + } + } + }); + } + } else { + notifyOperationHandlesOfClose(); + } + } + } + + /** + * Sets up the Netty pipeline for this connection. Handler configuration looks like: + *

+ * +-------------------------------------------------------------------+ + * | ChannelPipeline | + * | | + * | +-----------------------+ +-----------+----------+ | + * | | InboundMessageHandler | | RequestEncoder | | + * | +----------+------------+ +-----------+----------+ | + * | /|\ | | + * | | | | + * | +----------+------------+ | | + * | | MessageDecoder | | | + * | +----------+------------+ | | + * | /|\ | | + * | | | | + * | +----------+------------+ | | + * | | MessageFrameDecoder | | | + * | +----------+------------+ | | + * | /|\ | | + * | | \|/ | + * | +----------+------------+ +-----------+----------+ | + * | | I/O READ | | I/O WRITE | | + * | +----------+------------+ +-----------+----------+ | + * | /|\ \|/ | + * +---------------+-------------------------------------+-------------+ + */ + private class ClientInitializer extends ChannelInitializer { + + /** + * SSL handler. + */ + private final SslHandler sslHandler; + + + /** + * Creates a new client initializer. + * + * @param handler SSL handler or null + */ + ClientInitializer(final SslHandler handler) { + sslHandler = handler; + } + + + @Override + public void initChannel(final SocketChannel ch) { + if (sslHandler != null) { + ch.pipeline().addFirst("ssl", sslHandler); + } + // inbound handlers are processed top to bottom + // outbound handlers are processed bottom to top + ch.pipeline().addLast("frame_decoder", new MessageFrameDecoder()); + ch.pipeline().addLast("response_decoder", new MessageDecoder()); + if (!ch.config().isAutoRead()) { + ch.pipeline().addLast("flow_control_handler", new AutoReadFlowControlHandler()); + } + if (messageWorkerGroup != null) { + ch.pipeline().addLast(messageWorkerGroup, "message_handler", new InboundMessageHandler()); + } else { + ch.pipeline().addLast("message_handler", new InboundMessageHandler()); + } + if (!ch.config().isAutoRead()) { + ch.pipeline().addLast("next_message_handler", READ_NEXT_MESSAGE); + } + ch.pipeline().addLast("request_encoder", REQUEST_ENCODER); + if (connectionConfig.getConnectionValidator() != null) { + ch.pipeline().addLast("validate_conn", new ValidatorHandler(connectionConfig.getConnectionValidator())); + } + ch.pipeline().addLast("inbound_exception_handler", new InboundExceptionHandler()); + } + + + /** + * Returns whether the SSL pipeline is in use. + * + * @return whether the SSL pipeline is in use + */ + public boolean isSsl() { + return sslHandler != null; + } + } + + /** + * Matches an inbound LDAP response message to its operation handle and removes that handle from the response queue. + * Notifies all operation handles when an unsolicited notification arrives. + */ + private final class InboundMessageHandler extends SimpleChannelInboundHandler { + + + @Override + @SuppressWarnings("unchecked") + protected void channelRead0(final ChannelHandlerContext ctx, final Message msg) { + try { + final DefaultOperationHandle handle = pendingResponses.get(msg.getMessageID()); + if (handle != null) { + if (msg instanceof LdapEntry) { + ((SearchRequest) handle.getRequest()).configureBinaryAttributes((LdapEntry) msg); + ((DefaultSearchOperationHandle) handle).entry((LdapEntry) msg); + } else if (msg instanceof SearchResultReference) { + ((DefaultSearchOperationHandle) handle).reference((SearchResultReference) msg); + } else if (msg instanceof Result) { + if (msg instanceof ExtendedResponse) { + ((DefaultExtendedOperationHandle) handle).extended((ExtendedResponse) msg); + } else if (msg instanceof CompareResponse) { + ((DefaultCompareOperationHandle) handle).compare((CompareResponse) msg); + } + if (msg.getControls() != null && msg.getControls().length > 0) { + Stream.of(msg.getControls()).forEach(handle::control); + } + if (((Result) msg).getReferralURLs() != null && ((Result) msg).getReferralURLs().length > 0) { + handle.referral(((Result) msg).getReferralURLs()); + } + handle.result((Result) msg); + } else if (msg instanceof IntermediateResponse) { + handle.intermediate((IntermediateResponse) msg); + } else { + throw new IllegalStateException("Unknown message type: " + msg); + } + } else if (msg instanceof UnsolicitedNotification) { + pendingResponses.notifyOperationHandles((UnsolicitedNotification) msg); + } + } finally { + if (ctx != null) { + ctx.fireUserEventTriggered(MessageStatus.COMPLETE); + } + } + } + } + + /** + * Schedules a connection validator to run based on its strategy. If the validator fails an exception caught is fired + * in the pipeline. + */ + private class ValidatorHandler extends ChannelInboundHandlerAdapter { + + /** + * Connection validator. + */ + private final ConnectionValidator connectionValidator; + + /** + * Future to track execution status. + */ + private ScheduledFuture sf; + + + /** + * Creates a new validator handler. + * + * @param validator to execute + */ + ValidatorHandler(final ConnectionValidator validator) { + connectionValidator = validator; + } + + + @Override + public void channelActive(final ChannelHandlerContext ctx) { + // this implementation could also be done with user events + // while that may be cleaner it does introduce a dependency on the message thread pool which should be avoided + sf = ctx.executor().scheduleAtFixedRate( + () -> { + if (ctx != null && !ctx.executor().isShuttingDown()) { + final AtomicReference result = new AtomicReference<>(); + ctx.executor().submit(() -> connectionValidator.applyAsync(NettyConnection.this, result::set)); + ctx.executor().schedule( + () -> { + final boolean success = result.updateAndGet(b -> b != null && b); + if (!success) { + ctx.fireExceptionCaught( + new LdapException( + ResultCode.SERVER_DOWN, + "Connection validation failed for " + NettyConnection.this)); + } + }, + Duration.ZERO.equals(connectionValidator.getValidateTimeout()) ? + connectionConfig.getResponseTimeout().toMillis() : connectionValidator.getValidateTimeout().toMillis(), + TimeUnit.MILLISECONDS); + } + }, + connectionValidator.getValidatePeriod().toMillis(), + connectionValidator.getValidatePeriod().toMillis(), + TimeUnit.MILLISECONDS); + ctx.fireChannelActive(); + } + + + @Override + public void channelInactive(final ChannelHandlerContext ctx) { + if (sf != null) { + sf.cancel(true); + } + ctx.fireChannelInactive(); + } + } + + + /** + * Sets {@link #inboundException} and closes the channel when an exception occurs. + */ + private final class InboundExceptionHandler extends ChannelInboundHandlerAdapter { + + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { + inboundException = cause; + if (channel != null && !isClosing()) { + channel.close().addListener(new LogFutureListener()); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyConnectionFactoryTransport.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyConnectionFactoryTransport.java new file mode 100644 index 0000000..411b4ef --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyConnectionFactoryTransport.java @@ -0,0 +1,104 @@ + +package org.xbib.net.ldap.transport.netty; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import org.xbib.net.ldap.Connection; +import org.xbib.net.ldap.ConnectionConfig; +import org.xbib.net.ldap.transport.Transport; + +/** + * Creates netty connections with configured event loops. This implementation reuses the same event loops for each + * connection created. + * + */ +public class NettyConnectionFactoryTransport implements Transport { + + /** + * Channel type. + */ + private final Class channelType; + + /** + * Event loop group for I/O, must support the channel type. + */ + private final EventLoopGroup ioWorkerGroup; + + /** + * Event loop group for message handling. + */ + private final EventLoopGroup messageWorkerGroup; + + /** + * Whether to shut down the event loop groups on {@link #close()}. + */ + private boolean shutdownOnClose = true; + + + /** + * Creates a new netty connection factory transport. + * + * @param type of channel + * @param ioGroup event loop group to handle I/O + */ + public NettyConnectionFactoryTransport(final Class type, final EventLoopGroup ioGroup) { + this(type, ioGroup, null); + } + + + /** + * Creates a new netty connection factory transport. + * + * @param type of channel + * @param ioGroup event loop group to handle I/O + * @param messageGroup event loop group to handle inbound messages, can be null + */ + public NettyConnectionFactoryTransport( + final Class type, + final EventLoopGroup ioGroup, + final EventLoopGroup messageGroup) { + channelType = type; + ioWorkerGroup = ioGroup; + messageWorkerGroup = messageGroup; + } + + + /** + * Sets whether to shut down the event loop groups on close. + * + * @param b whether to shut down on close + */ + public void setShutdownOnClose(final boolean b) { + shutdownOnClose = b; + } + + + @Override + public Connection create(final ConnectionConfig cc) { + return new NettyConnection(cc, channelType, ioWorkerGroup, messageWorkerGroup, false); + } + + + @Override + public void close() { + if (shutdownOnClose) { + if (!ioWorkerGroup.isShutdown()) { + NettyUtils.shutdownGracefully(ioWorkerGroup); + } + if (messageWorkerGroup != null && !messageWorkerGroup.isShutdown()) { + NettyUtils.shutdownGracefully(messageWorkerGroup); + } + } + } + + + @Override + public String toString() { + return "[" + + getClass().getName() + "@" + hashCode() + "::" + + "channelType=" + channelType + ", " + + "ioWorkerGroup=" + ioWorkerGroup + ", " + + "messageWorkerGroup=" + messageWorkerGroup + ", " + + "shutdownOnClose=" + shutdownOnClose + "]"; + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyDERBuffer.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyDERBuffer.java new file mode 100644 index 0000000..11119de --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyDERBuffer.java @@ -0,0 +1,111 @@ + +package org.xbib.net.ldap.transport.netty; + +import io.netty.buffer.ByteBuf; +import org.xbib.asn1.DERBuffer; + +/** + * {@link DERBuffer} that uses a {@link ByteBuf}. Since {@link ByteBuf} does not have the concept of limit the writer + * index is used to track the limit. + * + */ +public class NettyDERBuffer implements DERBuffer { + + /** + * Underlying byte buffer. + */ + private final ByteBuf buffer; + + + /** + * Creates a new netty DER buffer. + * + * @param buf existing byte buf + */ + public NettyDERBuffer(final ByteBuf buf) { + this(buf, 0, buf.capacity()); + } + + + /** + * Creates a new netty DER buffer and sets the initial position and limit. + * + * @param buf existing byte buf + * @param pos initial buffer position + * @param lim initial buffer limit + */ + public NettyDERBuffer(final ByteBuf buf, final int pos, final int lim) { + buffer = buf; + buffer.setIndex(pos, lim); + } + + + @Override + public int position() { + return buffer.readerIndex(); + } + + + @Override + public DERBuffer position(final int newPosition) { + buffer.readerIndex(newPosition); + return this; + } + + + @Override + public int limit() { + return buffer.writerIndex(); + } + + + @Override + public int capacity() { + return buffer.capacity(); + } + + + @Override + public DERBuffer limit(final int newLimit) { + buffer.writerIndex(newLimit); + if (buffer.readerIndex() > newLimit) { + buffer.readerIndex(newLimit); + } + return this; + } + + + @Override + public DERBuffer clear() { + buffer.setIndex(0, buffer.capacity()); + return this; + } + + + @Override + public byte get() { + return buffer.readByte(); + } + + + @Override + public DERBuffer get(final byte[] dst) { + buffer.readBytes(dst); + return this; + } + + + @Override + public DERBuffer slice() { + return new NettyDERBuffer(buffer.slice(position(), remaining())); + } + + + @Override + public String toString() { + return getClass().getName() + "@" + hashCode() + "::" + + "pos=" + position() + ", " + + "lim=" + limit() + ", " + + "cap=" + capacity(); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyUtils.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyUtils.java new file mode 100644 index 0000000..bb1168e --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NettyUtils.java @@ -0,0 +1,95 @@ + +package org.xbib.net.ldap.transport.netty; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.ThreadPerTaskExecutor; + +/** + * Provides utility methods for this package. + * + */ +public final class NettyUtils { + + /** + * Time in milliseconds for graceful shutdown quiet period. + */ + private static final long DEFAULT_SHUTDOWN_QUIET_PERIOD = 0; + + /** + * Time in milliseconds for graceful shutdown max wait. + */ + private static final long DEFAULT_SHUTDOWN_MAX_TIMEOUT = 1000; + + /** + * Whether to use NIO even if other transports are available. + */ + private static final boolean USE_NIO = Boolean.parseBoolean( + System.getProperty("org.xbib.net.ldap.transport.netty.useNio", "false")); + + /** + * Default constructor. + */ + private NettyUtils() { + } + + + /** + * Returns the default socket channel type for this platform. + * + * @return socket channel type + */ + public static Class getDefaultSocketChannelType() { + return NioSocketChannel.class; + } + + + /** + * Returns the default event loop group for this platform. Set numThreads to zero to use the netty default. + * + * @param name of the thread pool + * @param numThreads number of threads in the thread pool + * @return event loop group + */ + public static EventLoopGroup createDefaultEventLoopGroup(final String name, final int numThreads) { + return new NioEventLoopGroup( + numThreads, + new ThreadPerTaskExecutor(new DefaultThreadFactory(name, true, Thread.NORM_PRIORITY))); + } + + + /** + * Invokes {@link EventLoopGroup#shutdownGracefully(long, long, TimeUnit)} on the supplied worker group. This method + * blocks for twice the {@link #DEFAULT_SHUTDOWN_MAX_TIMEOUT} waiting for the shutdown to be done. If the future is + * not invoked in that timeframe a warning is logged. + * + * @param workerGroup to shutdown + */ + public static void shutdownGracefully(final EventLoopGroup workerGroup) { + final CountDownLatch shutdownLatch = new CountDownLatch(1); + workerGroup.shutdownGracefully(DEFAULT_SHUTDOWN_QUIET_PERIOD, DEFAULT_SHUTDOWN_MAX_TIMEOUT, TimeUnit.MILLISECONDS) + .addListener(f -> { + shutdownLatch.countDown(); + if (!f.isSuccess()) { + if (f.cause() != null) { + } else { + // + } + } else { + // + } + }); + try { + if (!shutdownLatch.await(DEFAULT_SHUTDOWN_MAX_TIMEOUT * 2, TimeUnit.MILLISECONDS)) { + // + } + } catch (InterruptedException e) { + // + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioConnectionFactoryTransport.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioConnectionFactoryTransport.java new file mode 100644 index 0000000..b3f60a1 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioConnectionFactoryTransport.java @@ -0,0 +1,79 @@ + +package org.xbib.net.ldap.transport.netty; + +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.ThreadPerTaskExecutor; + +/** + * Creates netty connections using an {@link NioEventLoopGroup}. The event loop group is shutdown when the connection + * factory is closed. + * + */ +public class NioConnectionFactoryTransport extends NettyConnectionFactoryTransport { + + + /** + * Creates a new nio connection factory transport. + */ + public NioConnectionFactoryTransport() { + this(0); + } + + + /** + * Creates a new nio connection factory transport. + * + * @param ioThreads number of threads used for I/O in the event loop group + */ + public NioConnectionFactoryTransport(final int ioThreads) { + this(NioConnectionFactoryTransport.class.getSimpleName(), ioThreads); + } + + + /** + * Creates a new nio connection factory transport. + * + * @param name to assign the thread pool + * @param ioThreads number of threads used for I/O in the event loop group + */ + public NioConnectionFactoryTransport(final String name, final int ioThreads) { + super( + NioSocketChannel.class, + new NioEventLoopGroup( + ioThreads, + new ThreadPerTaskExecutor(new DefaultThreadFactory(name, true, Thread.NORM_PRIORITY))), + null); + } + + + /** + * Creates a new nio connection factory transport. + * + * @param ioThreads number of threads used for I/O in the event loop group + * @param messageThreads number of threads for LDAP message handling in the event loop group + */ + public NioConnectionFactoryTransport(final int ioThreads, final int messageThreads) { + this(NioConnectionFactoryTransport.class.getSimpleName(), ioThreads, messageThreads); + } + + + /** + * Creates a new nio connection factory transport. + * + * @param name to assign the thread pool + * @param ioThreads number of threads used for I/O in the event loop group + * @param messageThreads number of threads for LDAP message handling in the event loop group + */ + public NioConnectionFactoryTransport(final String name, final int ioThreads, final int messageThreads) { + super( + NioSocketChannel.class, + new NioEventLoopGroup( + ioThreads, + new ThreadPerTaskExecutor(new DefaultThreadFactory(name + "-io", true, Thread.NORM_PRIORITY))), + new NioEventLoopGroup( + messageThreads, + new ThreadPerTaskExecutor(new DefaultThreadFactory(name + "-messages", true, Thread.NORM_PRIORITY)))); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioConnectionTransport.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioConnectionTransport.java new file mode 100644 index 0000000..091b755 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioConnectionTransport.java @@ -0,0 +1,60 @@ + +package org.xbib.net.ldap.transport.netty; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.ThreadPerTaskExecutor; + +/** + * Creates netty connections using an {@link NioEventLoopGroup}. The event loop group is shutdown when the connection is + * closed. + * + */ +public class NioConnectionTransport extends ConnectionTransport { + + + /** + * Creates a new nio connection transport. + */ + public NioConnectionTransport() { + this(0); + } + + + /** + * Creates a new nio connection transport. + * + * @param ioThreads number of threads used for I/O in the event loop group + */ + public NioConnectionTransport(final int ioThreads) { + super(ioThreads); + } + + + /** + * Creates a new nio connection transport. + * + * @param ioThreads number of threads used for I/O in the event loop group + * @param messageThreads number of threads for LDAP message handling in the event loop group + */ + public NioConnectionTransport(final int ioThreads, final int messageThreads) { + super(ioThreads, messageThreads); + } + + + @Override + protected Class getSocketChannelType() { + return NioSocketChannel.class; + } + + + @Override + protected EventLoopGroup createEventLoopGroup(final String name, final int numThreads) { + return new NioEventLoopGroup( + numThreads, + new ThreadPerTaskExecutor(new DefaultThreadFactory(name, true, Thread.NORM_PRIORITY))); + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioSingletonTransport.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioSingletonTransport.java new file mode 100644 index 0000000..fbfdd49 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/NioSingletonTransport.java @@ -0,0 +1,42 @@ + +package org.xbib.net.ldap.transport.netty; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.ThreadPerTaskExecutor; + +/** + * Creates netty connections using a single, shared {@link NioEventLoopGroup}. This event loop group uses daemon + * threads and does not expect to be shutdown, however it can be manually shutdown using {@link #shutdown()}. + * + */ +public class NioSingletonTransport extends NettyConnectionFactoryTransport { + + /** + * Event group used for all connections . + */ + private static final EventLoopGroup SHARED_WORKER_GROUP = new NioEventLoopGroup( + 0, + new ThreadPerTaskExecutor(new DefaultThreadFactory(NioSingletonTransport.class, true, Thread.NORM_PRIORITY))); + + + /** + * Default constructor. + */ + public NioSingletonTransport() { + super(NioSocketChannel.class, SHARED_WORKER_GROUP); + } + + /** + * Invokes {@link NettyUtils#shutdownGracefully(EventLoopGroup)} on the underlying worker group. + */ + public static void shutdown() { + NettyUtils.shutdownGracefully(SHARED_WORKER_GROUP); + } + + @Override + public void close() { + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/SaslHandler.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/SaslHandler.java new file mode 100644 index 0000000..5c9ab23 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/SaslHandler.java @@ -0,0 +1,188 @@ + +package org.xbib.net.ldap.transport.netty; + +import java.net.SocketAddress; +import java.util.List; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.channel.CoalescingBufferQueue; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.UnsupportedMessageTypeException; +import io.netty.util.ReferenceCountUtil; + +/** + * Netty handler that uses a {@link SaslClient} to wrap and unwrap requests and responses. + * + */ +public class SaslHandler extends ByteToMessageDecoder implements ChannelOutboundHandler { + + + /** + * Underlying SASL client. + */ + private final SaslClient saslClient; + + /** + * To manage requests. + */ + private CoalescingBufferQueue queue; + + + /** + * Creates a new SASL handler. + * + * @param sc SASL client + */ + public SaslHandler(final SaslClient sc) { + saslClient = sc; + } + + + @Override + public void handlerAdded(final ChannelHandlerContext ctx) + throws Exception { + queue = new CoalescingBufferQueue(ctx.channel()); + } + + + @Override + public void handlerRemoved0(final ChannelHandlerContext ctx) + throws Exception { + dispose(); + } + + @Override + protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) + throws Exception { + // CheckStyle:MagicNumber OFF + if (in.readableBytes() <= 4) { + return; + } + // CheckStyle:MagicNumber ON + + final int readerIdx = in.readerIndex(); + final int writerIdx = in.writerIndex(); + final int len = in.readInt(); + if (in.readableBytes() < len) { + in.setIndex(readerIdx, writerIdx); + return; + } + + final byte[] wrappedBytes = new byte[len]; + in.readBytes(wrappedBytes); + final byte[] unwrapped = saslClient.unwrap(wrappedBytes, 0, wrappedBytes.length); + ctx.fireChannelRead(Unpooled.wrappedBuffer(unwrapped)); + } + + + @Override + public void bind(final ChannelHandlerContext ctx, final SocketAddress localAddress, final ChannelPromise promise) + throws Exception { + ctx.bind(localAddress, promise); + } + + + @Override + public void connect( + final ChannelHandlerContext ctx, + final SocketAddress remoteAddress, + final SocketAddress localAddress, + final ChannelPromise promise) + throws Exception { + ctx.connect(remoteAddress, localAddress, promise); + } + + + @Override + public void disconnect(final ChannelHandlerContext ctx, final ChannelPromise promise) + throws Exception { + dispose(); + ctx.close(promise); + } + + + @Override + public void close(final ChannelHandlerContext ctx, final ChannelPromise promise) + throws Exception { + dispose(); + ctx.close(promise); + } + + + /** + * Disposes the SASL client and releases all buffers from the queue. + */ + private void dispose() { + try { + saslClient.dispose(); + } catch (SaslException e) { + // + } + + if (queue != null && !queue.isEmpty()) { + queue.releaseAndFailAll(new ChannelException("SASL client closed")); + } + queue = null; + } + + + @Override + public void deregister(final ChannelHandlerContext ctx, final ChannelPromise promise) + throws Exception { + ctx.deregister(promise); + } + + + @Override + public void read(final ChannelHandlerContext ctx) + throws Exception { + ctx.read(); + } + + + @Override + public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) + throws Exception { + if (!(msg instanceof ByteBuf)) { + final UnsupportedMessageTypeException exception = new UnsupportedMessageTypeException(msg, ByteBuf.class); + ReferenceCountUtil.safeRelease(msg); + promise.setFailure(exception); + } else if (queue == null) { + ReferenceCountUtil.safeRelease(msg); + promise.setFailure(new IllegalStateException("Queue is null, handler has been removed")); + } else { + queue.add((ByteBuf) msg, promise); + } + } + + + @Override + public void flush(final ChannelHandlerContext ctx) + throws Exception { + if (ctx.isRemoved() || queue.isEmpty()) { + return; + } + ByteBuf buf = null; + try { + final ChannelPromise promise = ctx.newPromise(); + final int readableBytes = queue.readableBytes(); + buf = queue.remove(readableBytes, promise); + final byte[] bytes = new byte[readableBytes]; + buf.readBytes(bytes); + final byte[] wrappedBytes = saslClient.wrap(bytes, 0, bytes.length); + final ByteBuf wrappedBuf = Unpooled.buffer(wrappedBytes.length + 4); + wrappedBuf.writeInt(wrappedBytes.length).writeBytes(wrappedBytes); + ctx.writeAndFlush(wrappedBuf, promise); + } finally { + if (buf != null) { + ReferenceCountUtil.safeRelease(buf); + } + } + } +} diff --git a/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/SingletonTransport.java b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/SingletonTransport.java new file mode 100644 index 0000000..ebeb159 --- /dev/null +++ b/net-ldap/src/main/java/org/xbib/net/ldap/transport/netty/SingletonTransport.java @@ -0,0 +1,40 @@ + +package org.xbib.net.ldap.transport.netty; + +import io.netty.channel.EventLoopGroup; + +/** + * Creates netty connections using a single, shared {@link EventLoopGroup} using the best fit event loop group based on + * the operating system. See {@link io.netty.channel.epoll.Epoll#isAvailable()} and {@link + * io.netty.channel.kqueue.KQueue#isAvailable()}. This event loop group uses daemon threads and does not expect to be + * shutdown, however it can be manually shutdown using {@link #shutdown()}. + * + */ +public class SingletonTransport extends NettyConnectionFactoryTransport { + + /** + * Event group used for all connections . + */ + private static final EventLoopGroup SHARED_WORKER_GROUP = NettyUtils.createDefaultEventLoopGroup( + SingletonTransport.class.getSimpleName(), + 0); + + + /** + * Default constructor. + */ + public SingletonTransport() { + super(NettyUtils.getDefaultSocketChannelType(), SHARED_WORKER_GROUP); + } + + /** + * Invokes {@link NettyUtils#shutdownGracefully(EventLoopGroup)} on the underlying worker group. + */ + public static void shutdown() { + NettyUtils.shutdownGracefully(SHARED_WORKER_GROUP); + } + + @Override + public void close() { + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b53a5dc --- /dev/null +++ b/settings.gradle @@ -0,0 +1,40 @@ +pluginManagement { + repositories { + mavenLocal() + mavenCentral { + metadataSources { + mavenPom() + artifact() + ignoreGradleMetadataRedirection() + } + } + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + versionCatalogs { + libs { + version('gradle', '8.7') + version('netty', '4.1.112.Final') + version('netty-tcnative', '2.0.65.Final') + library('netty-codec-http2', 'io.netty', 'netty-codec-http2').versionRef('netty') + library('netty-handler', 'io.netty', 'netty-handler').versionRef('netty') + library('netty-handler-proxy', 'io.netty', 'netty-handler-proxy').versionRef('netty') + library('netty-epoll', 'io.netty', 'netty-transport-native-epoll').versionRef('netty') + library('netty-kqueue', 'io.netty', 'netty-transport-native-kqueue').versionRef('netty') + library('netty-boringssl', 'io.netty', 'netty-tcnative-boringssl-static').versionRef('netty-tcnative') + } + testLibs { + version('junit', '5.10.2') + library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit') + library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').versionRef('junit') + library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit') + library('junit-jupiter-platform-launcher', 'org.junit.platform', 'junit-platform-launcher').version('1.10.1') + library('hamcrest', 'org.hamcrest', 'hamcrest-library').version('2.2') + } + } +} + +include 'asn1' +include 'net-ldap'