| /* |
| * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package test; |
| |
| import java.io.*; |
| import java.nio.file.*; |
| import java.math.BigInteger; |
| import java.net.*; |
| import java.util.*; |
| import java.util.regex.*; |
| |
| /* |
| * A dummy LDAP server. |
| * |
| * Loads a sequence of LDAP messages from a capture file into its cache. |
| * It listens for LDAP requests, finds a match in its cache and sends the |
| * corresponding LDAP responses. |
| * |
| * The capture file contains an LDAP protocol exchange in the hexadecimal |
| * dump format emitted by sun.misc.HexDumpEncoder: |
| * |
| * xxxx: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff ................ |
| * |
| * Typically, LDAP protocol exchange is generated by running the LDAP client |
| * application program against a real LDAP server and setting the JNDI/LDAP |
| * environment property: com.sun.jndi.ldap.trace.ber to activate LDAP message |
| * tracing. |
| */ |
| public class LDAPServer { |
| |
| /* |
| * A cache of LDAP requests and responses. |
| * Messages with the same ID are stored in a list. |
| * The first element in the list is the LDAP request, |
| * the remaining elements are the LDAP responses. |
| */ |
| private final Map<Integer,List<byte[]>> cache = new HashMap<>(); |
| |
| public LDAPServer(ServerSocket serverSocket, String filename) |
| throws Exception { |
| |
| System.out.println("LDAPServer: Loading LDAP cache from: " + filename); |
| loadCaptureFile(filename); |
| |
| System.out.println("LDAPServer: listening on port " + |
| serverSocket.getLocalPort()); |
| |
| try (Socket clientSocket = serverSocket.accept(); |
| OutputStream out = clientSocket.getOutputStream(); |
| InputStream in = clientSocket.getInputStream();) { |
| |
| byte[] inBuffer = new byte[8192]; |
| int count; |
| |
| while ((count = in.read(inBuffer)) > 0) { |
| byte[] request = Arrays.copyOf(inBuffer, count); |
| int[] ids = getIDs(request); |
| int messageID = ids[0]; |
| String operation = getOperation(ids[1]); |
| System.out.println("\nLDAPServer: received LDAP " + operation + |
| " [message ID " + messageID + "]"); |
| |
| List<byte[]> encodings = cache.get(messageID); |
| if (encodings == null || |
| (!Arrays.equals(request, encodings.get(0)))) { |
| throw new Exception( |
| "LDAPServer: ERROR: received an LDAP " + operation + |
| " (ID=" + messageID + ") not present in cache"); |
| } |
| |
| for (int i = 1; i < encodings.size(); i++) { |
| // skip the request (at index 0) |
| byte[] response = encodings.get(i); |
| out.write(response, 0, response.length); |
| ids = getIDs(response); |
| System.out.println("\nLDAPServer: Sent LDAP " + |
| getOperation(ids[1]) + " [message ID " + ids[0] + "]"); |
| } |
| } |
| } catch (IOException e) { |
| System.out.println("LDAPServer: ERROR: " + e); |
| throw e; |
| } |
| |
| System.out.println("\n[LDAP server exited normally]"); |
| } |
| |
| /* |
| * Load a capture file containing an LDAP protocol exchange in the |
| * hexadecimal dump format emitted by sun.misc.HexDumpEncoder: |
| * |
| * xxxx: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff ................ |
| */ |
| private void loadCaptureFile(String filename) throws IOException { |
| StringBuilder hexString = new StringBuilder(); |
| String pattern = "(....): (..) (..) (..) (..) (..) (..) (..) (..) (..) (..) (..) (..) (..) (..) (..) (..).*"; |
| |
| try (Scanner fileScanner = new Scanner(Paths.get(filename))) { |
| while (fileScanner.hasNextLine()){ |
| |
| try (Scanner lineScanner = |
| new Scanner(fileScanner.nextLine())) { |
| if (lineScanner.findInLine(pattern) == null) { |
| continue; |
| } |
| MatchResult result = lineScanner.match(); |
| for (int i = 1; i <= result.groupCount(); i++) { |
| String digits = result.group(i); |
| if (digits.length() == 4) { |
| if (digits.equals("0000")) { // start-of-message |
| if (hexString.length() > 0) { |
| addToCache(hexString.toString()); |
| hexString = new StringBuilder(); |
| } |
| } |
| continue; |
| } else if (digits.equals(" ")) { // short message |
| continue; |
| } |
| hexString.append(digits); |
| } |
| } |
| } |
| } |
| addToCache(hexString.toString()); |
| } |
| |
| /* |
| * Add an LDAP encoding to the cache (by messageID key). |
| */ |
| private void addToCache(String hexString) throws IOException { |
| byte[] encoding = parseHexBinary(hexString); |
| int[] ids = getIDs(encoding); |
| int messageID = ids[0]; |
| List<byte[]> encodings = cache.get(messageID); |
| if (encodings == null) { |
| encodings = new ArrayList<>(); |
| } |
| System.out.println(" adding LDAP " + getOperation(ids[1]) + |
| " with message ID " + messageID + " to the cache"); |
| encodings.add(encoding); |
| cache.put(messageID, encodings); |
| } |
| |
| /* |
| * Extracts the message ID and operation ID from an LDAP protocol encoding |
| * and returns them in a 2-element array of integers. |
| */ |
| private static int[] getIDs(byte[] encoding) throws IOException { |
| if (encoding[0] != 0x30) { |
| throw new IOException("Error: bad LDAP encoding in capture file: " + |
| "expected ASN.1 SEQUENCE tag (0x30), encountered " + |
| encoding[0]); |
| } |
| |
| int index = 2; |
| if ((encoding[1] & 0x80) == 0x80) { |
| index += (encoding[1] & 0x0F); |
| } |
| |
| if (encoding[index] != 0x02) { |
| throw new IOException("Error: bad LDAP encoding in capture file: " + |
| "expected ASN.1 INTEGER tag (0x02), encountered " + |
| encoding[index]); |
| } |
| int length = encoding[index + 1]; |
| index += 2; |
| int messageID = |
| new BigInteger(1, |
| Arrays.copyOfRange(encoding, index, index + length)).intValue(); |
| index += length; |
| int operationID = encoding[index]; |
| |
| return new int[]{messageID, operationID}; |
| } |
| |
| /* |
| * Maps an LDAP operation ID to a string description |
| */ |
| private static String getOperation(int operationID) { |
| switch (operationID) { |
| case 0x60: |
| return "BindRequest"; // [APPLICATION 0] |
| case 0x61: |
| return "BindResponse"; // [APPLICATION 1] |
| case 0x42: |
| return "UnbindRequest"; // [APPLICATION 2] |
| case 0x63: |
| return "SearchRequest"; // [APPLICATION 3] |
| case 0x64: |
| return "SearchResultEntry"; // [APPLICATION 4] |
| case 0x65: |
| return "SearchResultDone"; // [APPLICATION 5] |
| case 0x66: |
| return "ModifyRequest"; // [APPLICATION 6] |
| case 0x67: |
| return "ModifyResponse"; // [APPLICATION 7] |
| case 0x68: |
| return "AddRequest"; // [APPLICATION 8] |
| case 0x69: |
| return "AddResponse"; // [APPLICATION 9] |
| case 0x4A: |
| return "DeleteRequest"; // [APPLICATION 10] |
| case 0x6B: |
| return "DeleteResponse"; // [APPLICATION 11] |
| case 0x6C: |
| return "ModifyDNRequest"; // [APPLICATION 12] |
| case 0x6D: |
| return "ModifyDNResponse"; // [APPLICATION 13] |
| case 0x6E: |
| return "CompareRequest"; // [APPLICATION 14] |
| case 0x6F: |
| return "CompareResponse"; // [APPLICATION 15] |
| case 0x50: |
| return "AbandonRequest"; // [APPLICATION 16] |
| case 0x73: |
| return "SearchResultReference"; // [APPLICATION 19] |
| case 0x77: |
| return "ExtendedRequest"; // [APPLICATION 23] |
| case 0x78: |
| return "ExtendedResponse"; // [APPLICATION 24] |
| case 0x79: |
| return "IntermediateResponse"; // [APPLICATION 25] |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| public static byte[] parseHexBinary(String s) { |
| |
| final int len = s.length(); |
| |
| // "111" is not a valid hex encoding. |
| if (len % 2 != 0) { |
| throw new IllegalArgumentException("hexBinary needs to be even-length: " + s); |
| } |
| |
| byte[] out = new byte[len / 2]; |
| |
| for (int i = 0; i < len; i += 2) { |
| int h = hexToBin(s.charAt(i)); |
| int l = hexToBin(s.charAt(i + 1)); |
| if (h == -1 || l == -1) { |
| throw new IllegalArgumentException("contains illegal character for hexBinary: " + s); |
| } |
| |
| out[i / 2] = (byte) (h * 16 + l); |
| } |
| |
| return out; |
| } |
| |
| private static int hexToBin(char ch) { |
| if ('0' <= ch && ch <= '9') { |
| return ch - '0'; |
| } |
| if ('A' <= ch && ch <= 'F') { |
| return ch - 'A' + 10; |
| } |
| if ('a' <= ch && ch <= 'f') { |
| return ch - 'a' + 10; |
| } |
| return -1; |
| } |
| private static final char[] hexCode = "0123456789ABCDEF".toCharArray(); |
| |
| public static String printHexBinary(byte[] data) { |
| StringBuilder r = new StringBuilder(data.length * 2); |
| for (byte b : data) { |
| r.append(hexCode[(b >> 4) & 0xF]); |
| r.append(hexCode[(b & 0xF)]); |
| } |
| return r.toString(); |
| } |
| } |