View Javadoc

1   /*
2    * CDDL HEADER START
3    *
4    * The contents of this file are subject to the terms of the
5    * Common Development and Distribution License, Version 1.0 only
6    * (the "License").  You may not use this file except in compliance
7    * with the License.
8    *
9    * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
10   * or http://forgerock.org/license/CDDLv1.0.html.
11   * See the License for the specific language governing permissions
12   * and limitations under the License.
13   *
14   * When distributing Covered Code, include this CDDL HEADER in each
15   * file and include the License file at legal-notices/CDDLv1_0.txt.
16   * If applicable, add the following below this CDDL HEADER, with the
17   * fields enclosed by brackets "[]" replaced with your own identifying
18   * information:
19   *      Portions Copyright [yyyy] [name of copyright owner]
20   *
21   * CDDL HEADER END
22   *
23   *
24   *      Copyright 2009-2010 Sun Microsystems, Inc.
25   *      Portions copyright 2011-2012 ForgeRock AS
26   */
27  
28  package org.forgerock.opendj.examples;
29  
30  import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
31  
32  import java.io.FileInputStream;
33  import java.io.FileNotFoundException;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.util.NavigableMap;
37  import java.util.concurrent.ConcurrentSkipListMap;
38  import java.util.concurrent.locks.ReentrantReadWriteLock;
39  
40  import javax.net.ssl.SSLContext;
41  
42  import org.forgerock.opendj.ldap.CancelledResultException;
43  import org.forgerock.opendj.ldap.Connections;
44  import org.forgerock.opendj.ldap.DN;
45  import org.forgerock.opendj.ldap.Entry;
46  import org.forgerock.opendj.ldap.ErrorResultException;
47  import org.forgerock.opendj.ldap.Filter;
48  import org.forgerock.opendj.ldap.IntermediateResponseHandler;
49  import org.forgerock.opendj.ldap.KeyManagers;
50  import org.forgerock.opendj.ldap.LDAPClientContext;
51  import org.forgerock.opendj.ldap.LDAPListener;
52  import org.forgerock.opendj.ldap.LDAPListenerOptions;
53  import org.forgerock.opendj.ldap.LinkedHashMapEntry;
54  import org.forgerock.opendj.ldap.Matcher;
55  import org.forgerock.opendj.ldap.Modification;
56  import org.forgerock.opendj.ldap.ModificationType;
57  import org.forgerock.opendj.ldap.RequestContext;
58  import org.forgerock.opendj.ldap.RequestHandler;
59  import org.forgerock.opendj.ldap.ResultCode;
60  import org.forgerock.opendj.ldap.ResultHandler;
61  import org.forgerock.opendj.ldap.SSLContextBuilder;
62  import org.forgerock.opendj.ldap.SearchResultHandler;
63  import org.forgerock.opendj.ldap.SearchScope;
64  import org.forgerock.opendj.ldap.ServerConnection;
65  import org.forgerock.opendj.ldap.ServerConnectionFactory;
66  import org.forgerock.opendj.ldap.TrustManagers;
67  import org.forgerock.opendj.ldap.requests.AddRequest;
68  import org.forgerock.opendj.ldap.requests.BindRequest;
69  import org.forgerock.opendj.ldap.requests.CompareRequest;
70  import org.forgerock.opendj.ldap.requests.DeleteRequest;
71  import org.forgerock.opendj.ldap.requests.ExtendedRequest;
72  import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
73  import org.forgerock.opendj.ldap.requests.ModifyRequest;
74  import org.forgerock.opendj.ldap.requests.SearchRequest;
75  import org.forgerock.opendj.ldap.responses.BindResult;
76  import org.forgerock.opendj.ldap.responses.CompareResult;
77  import org.forgerock.opendj.ldap.responses.ExtendedResult;
78  import org.forgerock.opendj.ldap.responses.Responses;
79  import org.forgerock.opendj.ldap.responses.Result;
80  import org.forgerock.opendj.ldif.LDIFEntryReader;
81  
82  /**
83   * An LDAP directory server which exposes data contained in an LDIF file. This
84   * is implementation is very simple and is only intended as an example:
85   * <ul>
86   * <li>It does not support SSL connections
87   * <li>It does not support StartTLS
88   * <li>It does not support Abandon or Cancel requests
89   * <li>Very basic authentication and authorization support.
90   * </ul>
91   * This example takes the following command line parameters:
92   *
93   * <pre>
94   *  &lt;listenAddress> &lt;listenPort> [&lt;ldifFile>]
95   * </pre>
96   */
97  public final class Server {
98      private static final class MemoryBackend implements RequestHandler<RequestContext> {
99          private final ConcurrentSkipListMap<DN, Entry> entries;
100         private final ReentrantReadWriteLock entryLock = new ReentrantReadWriteLock();
101 
102         private MemoryBackend(final ConcurrentSkipListMap<DN, Entry> entries) {
103             this.entries = entries;
104         }
105 
106         /**
107          * {@inheritDoc}
108          */
109         @Override
110         public void handleAdd(final RequestContext requestContext, final AddRequest request,
111                 final IntermediateResponseHandler intermediateResponseHandler,
112                 final ResultHandler<? super Result> resultHandler) {
113             // TODO: controls.
114             entryLock.writeLock().lock();
115             try {
116                 DN dn = request.getName();
117                 if (entries.containsKey(dn)) {
118                     resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
119                             ResultCode.ENTRY_ALREADY_EXISTS, "The entry " + dn.toString()
120                                     + " already exists"));
121                 }
122 
123                 DN parent = dn.parent();
124                 if (!entries.containsKey(parent)) {
125                     resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
126                             ResultCode.NO_SUCH_OBJECT, "The parent entry " + parent.toString()
127                                     + " does not exist"));
128                 } else {
129                     entries.put(dn, request);
130                     resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
131                 }
132             } finally {
133                 entryLock.writeLock().unlock();
134             }
135         }
136 
137         /**
138          * {@inheritDoc}
139          */
140         @Override
141         public void handleBind(final RequestContext requestContext, final int version,
142                 final BindRequest request,
143                 final IntermediateResponseHandler intermediateResponseHandler,
144                 final ResultHandler<? super BindResult> resultHandler) {
145             if (request.getAuthenticationType() != ((byte) 0x80)) {
146                 // TODO: SASL authentication not implemented.
147                 resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
148                         "non-SIMPLE authentication not supported: "
149                                 + request.getAuthenticationType()));
150             } else {
151                 // TODO: always succeed.
152                 resultHandler.handleResult(Responses.newBindResult(ResultCode.SUCCESS));
153             }
154         }
155 
156         /**
157          * {@inheritDoc}
158          */
159         @Override
160         public void handleCompare(final RequestContext requestContext,
161                 final CompareRequest request,
162                 final IntermediateResponseHandler intermediateResponseHandler,
163                 final ResultHandler<? super CompareResult> resultHandler) {
164             // TODO:
165         }
166 
167         /**
168          * {@inheritDoc}
169          */
170         @Override
171         public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
172                 final IntermediateResponseHandler intermediateResponseHandler,
173                 final ResultHandler<? super Result> resultHandler) {
174             // TODO: controls.
175             entryLock.writeLock().lock();
176             try {
177                 // TODO: check for children.
178                 DN dn = request.getName();
179                 if (!entries.containsKey(dn)) {
180                     resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
181                             ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
182                                     + " does not exist"));
183                 } else {
184                     entries.remove(dn);
185                     resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
186                 }
187             } finally {
188                 entryLock.writeLock().unlock();
189             }
190         }
191 
192         /**
193          * {@inheritDoc}
194          */
195         @Override
196         public <R extends ExtendedResult> void handleExtendedRequest(
197                 final RequestContext requestContext, final ExtendedRequest<R> request,
198                 final IntermediateResponseHandler intermediateResponseHandler,
199                 final ResultHandler<? super R> resultHandler) {
200             // TODO: not implemented.
201             resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
202                     "Extended request operation not supported"));
203         }
204 
205         /**
206          * {@inheritDoc}
207          */
208         @Override
209         public void handleModify(final RequestContext requestContext, final ModifyRequest request,
210                 final IntermediateResponseHandler intermediateResponseHandler,
211                 final ResultHandler<? super Result> resultHandler) {
212             // TODO: controls.
213             // TODO: read lock is not really enough since concurrent updates may
214             // still occur to the same entry.
215             entryLock.readLock().lock();
216             try {
217                 DN dn = request.getName();
218                 Entry entry = entries.get(dn);
219                 if (entry == null) {
220                     resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
221                             ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
222                                     + " does not exist"));
223                 }
224 
225                 Entry newEntry = new LinkedHashMapEntry(entry);
226                 for (Modification mod : request.getModifications()) {
227                     ModificationType modType = mod.getModificationType();
228                     if (modType.equals(ModificationType.ADD)) {
229                         // TODO: Reject empty attribute and duplicate values.
230                         newEntry.addAttribute(mod.getAttribute(), null);
231                     } else if (modType.equals(ModificationType.DELETE)) {
232                         // TODO: Reject missing values.
233                         newEntry.removeAttribute(mod.getAttribute(), null);
234                     } else if (modType.equals(ModificationType.REPLACE)) {
235                         newEntry.replaceAttribute(mod.getAttribute());
236                     } else {
237                         resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
238                                 "Modify request contains an unsupported modification type"));
239                         return;
240                     }
241                 }
242 
243                 entries.put(dn, newEntry);
244                 resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
245             } finally {
246                 entryLock.readLock().unlock();
247             }
248         }
249 
250         /**
251          * {@inheritDoc}
252          */
253         @Override
254         public void handleModifyDN(final RequestContext requestContext,
255                 final ModifyDNRequest request,
256                 final IntermediateResponseHandler intermediateResponseHandler,
257                 final ResultHandler<? super Result> resultHandler) {
258             // TODO: not implemented.
259             resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
260                     "ModifyDN request operation not supported"));
261         }
262 
263         /**
264          * {@inheritDoc}
265          */
266         @Override
267         public void handleSearch(final RequestContext requestContext, final SearchRequest request,
268                 final IntermediateResponseHandler intermediateResponseHandler,
269                 final SearchResultHandler resultHandler) {
270             // TODO: controls, limits, etc.
271             entryLock.readLock().lock();
272             try {
273                 DN dn = request.getName();
274                 Entry baseEntry = entries.get(dn);
275                 if (baseEntry == null) {
276                     resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
277                             ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
278                                     + " does not exist"));
279                     return;
280                 }
281 
282                 SearchScope scope = request.getScope();
283                 Filter filter = request.getFilter();
284                 Matcher matcher = filter.matcher();
285 
286                 if (scope.equals(SearchScope.BASE_OBJECT)) {
287                     if (matcher.matches(baseEntry).toBoolean()) {
288                         sendEntry(request, resultHandler, baseEntry);
289                     }
290                 } else if (scope.equals(SearchScope.SINGLE_LEVEL)) {
291                     NavigableMap<DN, Entry> subtree = entries.tailMap(dn, false);
292                     for (Entry entry : subtree.values()) {
293                         // Check for cancellation.
294                         requestContext.checkIfCancelled(false);
295 
296                         DN childDN = entry.getName();
297                         if (childDN.isChildOf(dn)) {
298                             if (!matcher.matches(entry).toBoolean()) {
299                                 continue;
300                             }
301 
302                             if (!sendEntry(request, resultHandler, entry)) {
303                                 // Caller has asked to stop sending results.
304                                 break;
305                             }
306                         } else if (!childDN.isSubordinateOrEqualTo(dn)) {
307                             // The remaining entries will be out of scope.
308                             break;
309                         }
310                     }
311                 } else if (scope.equals(SearchScope.WHOLE_SUBTREE)) {
312                     NavigableMap<DN, Entry> subtree = entries.tailMap(dn);
313                     for (Entry entry : subtree.values()) {
314                         // Check for cancellation.
315                         requestContext.checkIfCancelled(false);
316 
317                         DN childDN = entry.getName();
318                         if (childDN.isSubordinateOrEqualTo(dn)) {
319                             if (!matcher.matches(entry).toBoolean()) {
320                                 continue;
321                             }
322 
323                             if (!sendEntry(request, resultHandler, entry)) {
324                                 // Caller has asked to stop sending results.
325                                 break;
326                             }
327                         } else {
328                             // The remaining entries will be out of scope.
329                             break;
330                         }
331                     }
332                 } else {
333                     resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
334                             "Search request contains an unsupported search scope"));
335                     return;
336                 }
337 
338                 resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
339             } catch (CancelledResultException e) {
340                 resultHandler.handleErrorResult(e);
341             } finally {
342                 entryLock.readLock().unlock();
343             }
344         }
345 
346         private boolean sendEntry(SearchRequest request, SearchResultHandler resultHandler,
347                 Entry entry) {
348             // TODO: check filter, strip attributes.
349             return resultHandler.handleEntry(Responses.newSearchResultEntry(entry));
350         }
351     }
352 
353     /**
354      * Main method.
355      *
356      * @param args
357      *            The command line arguments: listen address, listen port,
358      *            ldifFile
359      */
360     public static void main(final String[] args) {
361         if (args.length != 3 && args.length != 6) {
362             System.err.println("Usage: listenAddress listenPort ldifFile "
363                     + "[keyStoreFile keyStorePassword certNickname]");
364             System.exit(1);
365         }
366 
367         // Parse command line arguments.
368         final String localAddress = args[0];
369         final int localPort = Integer.parseInt(args[1]);
370         final String ldifFileName = args[2];
371         final String keyStoreFileName = (args.length == 6) ? args[3] : null;
372         final String keyStorePassword = (args.length == 6) ? args[4] : null;
373         final String certNickname = (args.length == 6) ? args[5] : null;
374 
375         // Create the memory backend.
376         final ConcurrentSkipListMap<DN, Entry> entries = readEntriesFromLDIF(ldifFileName);
377         final MemoryBackend backend = new MemoryBackend(entries);
378 
379         // Create a server connection adapter.
380         final ServerConnectionFactory<LDAPClientContext, Integer> connectionHandler =
381                 Connections.newServerConnectionFactory(backend);
382 
383         // Create listener.
384         LDAPListener listener = null;
385         try {
386             final LDAPListenerOptions options = new LDAPListenerOptions().setBacklog(4096);
387 
388             if (keyStoreFileName != null) {
389                 // Configure SSL/TLS and enable it when connections are
390                 // accepted.
391                 final SSLContext sslContext =
392                         new SSLContextBuilder().setKeyManager(
393                                 KeyManagers.useSingleCertificate(certNickname, KeyManagers
394                                         .useKeyStoreFile(keyStoreFileName, keyStorePassword
395                                                 .toCharArray(), null))).setTrustManager(
396                                 TrustManagers.trustAll()).getSSLContext();
397 
398                 ServerConnectionFactory<LDAPClientContext, Integer> sslWrapper =
399                         new ServerConnectionFactory<LDAPClientContext, Integer>() {
400 
401                             public ServerConnection<Integer> handleAccept(
402                                     LDAPClientContext clientContext) throws ErrorResultException {
403                                 clientContext.enableTLS(sslContext, null, null, false, false);
404                                 return connectionHandler.handleAccept(clientContext);
405                             }
406                         };
407 
408                 listener = new LDAPListener(localAddress, localPort, sslWrapper, options);
409             } else {
410                 // No SSL.
411                 listener = new LDAPListener(localAddress, localPort, connectionHandler, options);
412             }
413             System.out.println("Press any key to stop the server...");
414             System.in.read();
415         } catch (final Exception e) {
416             System.out.println("Error listening on " + localAddress + ":" + localPort);
417             e.printStackTrace();
418         } finally {
419             if (listener != null) {
420                 listener.close();
421             }
422         }
423     }
424 
425     /**
426      * Reads the entries from the named LDIF file.
427      *
428      * @param ldifFileName
429      *            The name of the LDIF file.
430      * @return The entries.
431      */
432     private static ConcurrentSkipListMap<DN, Entry> readEntriesFromLDIF(final String ldifFileName) {
433         final ConcurrentSkipListMap<DN, Entry> entries;
434         // Read the LDIF.
435         InputStream ldif;
436         try {
437             ldif = new FileInputStream(ldifFileName);
438         } catch (final FileNotFoundException e) {
439             System.err.println(e.getMessage());
440             System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
441             return null; // Satisfy compiler.
442         }
443 
444         entries = new ConcurrentSkipListMap<DN, Entry>();
445         final LDIFEntryReader reader = new LDIFEntryReader(ldif);
446         try {
447             while (reader.hasNext()) {
448                 Entry entry = reader.readEntry();
449                 entries.put(entry.getName(), entry);
450             }
451         } catch (final IOException e) {
452             System.err.println(e.getMessage());
453             System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
454             return null; // Satisfy compiler.
455         } finally {
456             try {
457                 reader.close();
458             } catch (final IOException ignored) {
459                 // Ignore.
460             }
461         }
462 
463         // Quickly sanity check that every entry (except root entries) have a
464         // parent.
465         boolean isValid = true;
466         for (DN dn : entries.keySet()) {
467             if (dn.size() > 1) {
468                 DN parent = dn.parent();
469                 if (!entries.containsKey(parent)) {
470                     System.err
471                             .println("The entry \"" + dn.toString() + "\" does not have a parent");
472                     isValid = false;
473                 }
474             }
475         }
476         if (!isValid) {
477             System.exit(1);
478         }
479         return entries;
480     }
481 
482     private Server() {
483         // Not used.
484     }
485 }