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
24package test;
25
26import java.io.*;
27import java.nio.file.*;
28import java.math.BigInteger;
29import java.net.*;
30import java.util.*;
31import java.util.regex.*;
32import 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 */
51public 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}
251