1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package test;
  25 
  26 import java.io.*;
  27 import java.nio.file.*;
  28 import java.math.BigInteger;
  29 import java.net.*;
  30 import java.util.*;
  31 import java.util.regex.*;
  32 import javax.xml.bind.DatatypeConverter;
  33 
  34 /*
  35  * A dummy LDAP server.
  36  *
  37  * Loads a sequence of LDAP messages from a capture file into its cache.
  38  * It listens for LDAP requests, finds a match in its cache and sends the
  39  * corresponding LDAP responses.
  40  *
  41  * The capture file contains an LDAP protocol exchange in the hexadecimal
  42  * dump format emitted by sun.misc.HexDumpEncoder:
  43  *
  44  * xxxx: 00 11 22 33 44 55 66 77   88 99 aa bb cc dd ee ff  ................
  45  *
  46  * Typically, LDAP protocol exchange is generated by running the LDAP client
  47  * application program against a real LDAP server and setting the JNDI/LDAP
  48  * environment property: com.sun.jndi.ldap.trace.ber to activate LDAP message
  49  * tracing.
  50  */
  51 public class LDAPServer {
  52 
  53     /*
  54      * A cache of LDAP requests and responses.
  55      * Messages with the same ID are stored in a list.
  56      * The first element in the list is the LDAP request,
  57      * the remaining elements are the LDAP responses.
  58      */
  59     private final Map<Integer,List<byte[]>> cache = new HashMap<>();
  60 
  61     public LDAPServer(ServerSocket serverSocket, String filename)
  62         throws Exception {
  63 
  64         System.out.println("LDAPServer: Loading LDAP cache from: " + filename);
  65         loadCaptureFile(filename);
  66 
  67         System.out.println("LDAPServer: listening on port " +
  68             serverSocket.getLocalPort());
  69 
  70         try (Socket clientSocket = serverSocket.accept();
  71             OutputStream out = clientSocket.getOutputStream();
  72             InputStream in = clientSocket.getInputStream();) {
  73 
  74             byte[] inBuffer = new byte[8192];
  75             int count;
  76 
  77             while ((count = in.read(inBuffer)) > 0) {
  78                 byte[] request = Arrays.copyOf(inBuffer, count);
  79                 int[] ids = getIDs(request);
  80                 int messageID = ids[0];
  81                 String operation = getOperation(ids[1]);
  82                 System.out.println("\nLDAPServer: received LDAP " + operation +
  83                     "  [message ID " + messageID + "]");
  84 
  85                 List<byte[]> encodings = cache.get(messageID);
  86                 if (encodings == null ||
  87                     (!Arrays.equals(request, encodings.get(0)))) {
  88                     throw new Exception(
  89                         "LDAPServer: ERROR: received an LDAP " + operation +
  90                         " (ID=" + messageID + ") not present in cache");
  91                 }
  92 
  93                 for (int i = 1; i < encodings.size(); i++) {
  94                     // skip the request (at index 0)
  95                     byte[] response = encodings.get(i);
  96                     out.write(response, 0, response.length);
  97                     ids = getIDs(response);
  98                     System.out.println("\nLDAPServer: Sent LDAP " +
  99                         getOperation(ids[1]) + "  [message ID " + ids[0] + "]");
 100                 }
 101             }
 102         } catch (IOException e) {
 103             System.out.println("LDAPServer: ERROR: " + e);
 104             throw e;
 105         }
 106 
 107         System.out.println("\n[LDAP server exited normally]");
 108     }
 109 
 110     /*
 111      * Load a capture file containing an LDAP protocol exchange in the
 112      * hexadecimal dump format emitted by sun.misc.HexDumpEncoder:
 113      *
 114      * xxxx: 00 11 22 33 44 55 66 77   88 99 aa bb cc dd ee ff  ................
 115      */
 116     private void loadCaptureFile(String filename) throws IOException {
 117         StringBuilder hexString = new StringBuilder();
 118         String pattern = "(....): (..) (..) (..) (..) (..) (..) (..) (..)   (..) (..) (..) (..) (..) (..) (..) (..).*";
 119 
 120         try (Scanner fileScanner =  new Scanner(Paths.get(filename))) {
 121             while (fileScanner.hasNextLine()){
 122 
 123                 try (Scanner lineScanner =
 124                     new Scanner(fileScanner.nextLine())) {
 125                     if (lineScanner.findInLine(pattern) == null) {
 126                         continue;
 127                     }
 128                     MatchResult result = lineScanner.match();
 129                     for (int i = 1; i <= result.groupCount(); i++) {
 130                         String digits = result.group(i);
 131                         if (digits.length() == 4) {
 132                             if (digits.equals("0000")) { // start-of-message
 133                                 if (hexString.length() > 0) {
 134                                     addToCache(hexString.toString());
 135                                     hexString = new StringBuilder();
 136                                 }
 137                             }
 138                             continue;
 139                         } else if (digits.equals("  ")) { // short message
 140                             continue;
 141                         }
 142                         hexString.append(digits);
 143                     }
 144                 }
 145             }
 146         }
 147         addToCache(hexString.toString());
 148     }
 149 
 150     /*
 151      * Add an LDAP encoding to the cache (by messageID key).
 152      */
 153     private void addToCache(String hexString) throws IOException {
 154         byte[] encoding = DatatypeConverter.parseHexBinary(hexString);
 155         int[] ids = getIDs(encoding);
 156         int messageID = ids[0];
 157         List<byte[]> encodings = cache.get(messageID);
 158         if (encodings == null) {
 159             encodings = new ArrayList<>();
 160         }
 161         System.out.println("    adding LDAP " + getOperation(ids[1]) +
 162             " with message ID " + messageID + " to the cache");
 163         encodings.add(encoding);
 164         cache.put(messageID, encodings);
 165     }
 166 
 167     /*
 168      * Extracts the message ID and operation ID from an LDAP protocol encoding
 169      * and returns them in a 2-element array of integers.
 170      */
 171     private static int[] getIDs(byte[] encoding) throws IOException {
 172         if (encoding[0] != 0x30) {
 173             throw new IOException("Error: bad LDAP encoding in capture file: " +
 174                 "expected ASN.1 SEQUENCE tag (0x30), encountered " +
 175                 encoding[0]);
 176         }
 177 
 178         int index = 2;
 179         if ((encoding[1] & 0x80) == 0x80) {
 180             index += (encoding[1] & 0x0F);
 181         }
 182 
 183         if (encoding[index] != 0x02) {
 184             throw new IOException("Error: bad LDAP encoding in capture file: " +
 185                 "expected ASN.1 INTEGER tag (0x02), encountered " +
 186                 encoding[index]);
 187         }
 188         int length = encoding[index + 1];
 189         index += 2;
 190         int messageID =
 191             new BigInteger(1,
 192                 Arrays.copyOfRange(encoding, index, index + length)).intValue();
 193         index += length;
 194         int operationID = encoding[index];
 195 
 196         return new int[]{messageID, operationID};
 197     }
 198 
 199     /*
 200      * Maps an LDAP operation ID to a string description
 201      */
 202     private static String getOperation(int operationID) {
 203         switch (operationID) {
 204         case 0x60:
 205             return "BindRequest";       // [APPLICATION 0]
 206         case 0x61:
 207             return "BindResponse";      // [APPLICATION 1]
 208         case 0x42:
 209             return "UnbindRequest";     // [APPLICATION 2]
 210         case 0x63:
 211             return "SearchRequest";     // [APPLICATION 3]
 212         case 0x64:
 213             return "SearchResultEntry"; // [APPLICATION 4]
 214         case 0x65:
 215             return "SearchResultDone";  // [APPLICATION 5]
 216         case 0x66:
 217             return "ModifyRequest";     // [APPLICATION 6]
 218         case 0x67:
 219             return "ModifyResponse";    // [APPLICATION 7]
 220         case 0x68:
 221             return "AddRequest";        // [APPLICATION 8]
 222         case 0x69:
 223             return "AddResponse";       // [APPLICATION 9]
 224         case 0x4A:
 225             return "DeleteRequest";     // [APPLICATION 10]
 226         case 0x6B:
 227             return "DeleteResponse";    // [APPLICATION 11]
 228         case 0x6C:
 229             return "ModifyDNRequest";   // [APPLICATION 12]
 230         case 0x6D:
 231             return "ModifyDNResponse";  // [APPLICATION 13]
 232         case 0x6E:
 233             return "CompareRequest";    // [APPLICATION 14]
 234         case 0x6F:
 235             return "CompareResponse";   // [APPLICATION 15]
 236         case 0x50:
 237             return "AbandonRequest";    // [APPLICATION 16]
 238         case 0x73:
 239             return "SearchResultReference";  // [APPLICATION 19]
 240         case 0x77:
 241             return "ExtendedRequest";   // [APPLICATION 23]
 242         case 0x78:
 243             return "ExtendedResponse";  // [APPLICATION 24]
 244         case 0x79:
 245             return "IntermediateResponse";  // [APPLICATION 25]
 246         default:
 247             return "Unknown";
 248         }
 249     }
 250 }