001package com.nimbusds.common.ldap;
002
003
004import java.io.FileInputStream;
005import java.io.IOException;
006import java.io.InputStream;
007import java.util.*;
008import javax.servlet.ServletContext;
009import javax.servlet.ServletContextEvent;
010import javax.servlet.ServletContextListener;
011
012import com.nimbusds.common.servlet.ResourceRetriever;
013import com.thetransactioncompany.util.PropertyParseException;
014import com.thetransactioncompany.util.PropertyRetriever;
015import com.unboundid.ldap.listener.InMemoryDirectoryServer;
016import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
017import com.unboundid.ldap.listener.InMemoryListenerConfig;
018import com.unboundid.ldap.sdk.LDAPException;
019import com.unboundid.ldap.sdk.OperationType;
020import com.unboundid.ldap.sdk.schema.Schema;
021import com.unboundid.ldif.LDIFException;
022import com.unboundid.ldif.LDIFReader;
023import org.apache.logging.log4j.LogManager;
024import org.apache.logging.log4j.Logger;
025
026
027/**
028 * Sample in-memory LDAP directory server for demonstration and testing
029 * purposes. Access is limited to read and bind (authenticate) only.
030 *
031 * <p>The directory server is configured by a set of "sampleDirectoryServer.*"
032 * properties which can be overridden with Java system properties, see
033 * {@link Configuration}.
034 *
035 * <p>The sample directory implements {@code ServletContextListener}. This
036 * enables its automatic startup and shutdown in a servlet container (Java web 
037 * server), such as Apache Tomcat. When started from a servlet container the
038 * directory configuration is obtained from a properties file specified by a
039 * context parameter named {@code sampleDirectoryServer.configurationFile}.
040 */
041public class SampleDirectory implements ServletContextListener {
042        
043        
044        /**
045         * The sample directory server configuration.
046         */
047        public static class Configuration {
048        
049                
050                /**
051                 * If {@code true} the sample directory server must be
052                 * enabled.
053                 *
054                 * <p>Property key: sampleDirectoryServer.enable
055                 */
056                public final boolean enable;
057                
058                
059                /**
060                 * The default enable policy.
061                 */
062                public static final boolean DEFAULT_ENABLE = false;
063                
064                
065                /**
066                 * The port number on which the sample directory server
067                 * must accept LDAP client connections.
068                 *
069                 * <p>Property key: sampleDirectoryServer.port
070                 */
071                public final int port;
072                
073                
074                /**
075                 * The default port number.
076                 */
077                public static final int DEFAULT_PORT = 10389;
078                
079                
080                /**
081                 * Specifies the permitted LDAP operations.
082                 *
083                 * <p>Property key: sampleDirectoryServer.operations
084                 */
085                public final Set<OperationType> operations;
086                
087                
088                /**
089                 * The default permitted LDAP operations.
090                 */
091                public static final Set<OperationType> DEFAULT_OPERATIONS = new HashSet<>(
092                        Arrays.asList(OperationType.BIND,
093                                OperationType.COMPARE,
094                                OperationType.SEARCH,
095                                OperationType.EXTENDED));
096                
097                
098                /**
099                 * Specifies an alternative schema for the sample directory +
100                 * server, supplied in a single LDIF file. If {@code null} the 
101                 * default built-in server schema must be used.
102                 *
103                 * <p>Property key: sampleDirectoryServer.schema
104                 */
105                public final String schema;
106                
107                
108                /**
109                 * The base distinguished name (DN) of the directory information
110                 * tree.
111                 *
112                 * <p>Property key: sampleDirectoryServer.baseDN
113                 */
114                public final String baseDN;
115                
116                
117                /**
118                 * The initial directory information tree, supplied in a single
119                 * LDIF file. If {@code null} the directory will be left
120                 * empty.
121                 *
122                 * <p>Property key: sampleDirectoryServer.content
123                 */
124                public final String content;
125                
126                
127                /**
128                 * Creates a new sample directory server configuration from the
129                 * specified properties.
130                 *
131                 * @param props The configuration properties. Must not be 
132                 *              {@code null}.
133                 *
134                 * @throws PropertyParseException On a missing or invalid 
135                 *                                property.
136                 */
137                public Configuration(final Properties props)
138                        throws PropertyParseException {
139                
140                        PropertyRetriever pr = new PropertyRetriever(props, true);
141                        
142                        enable = pr.getOptBoolean("sampleDirectoryServer.enable", DEFAULT_ENABLE);
143                        
144                        if (! enable) {
145                                port = DEFAULT_PORT;
146                                operations = DEFAULT_OPERATIONS;
147                                schema = null;
148                                baseDN = null;
149                                content = null;
150                                return;
151                        }
152                        
153                        // We're okay to read rest of config
154                        
155                        port = pr.getOptInt("sampleDirectoryServer.port", DEFAULT_PORT);
156                        
157                        String s = pr.getOptString("sampleDirectoryServer.operations", null);
158                        
159                        if (s != null && ! s.trim().isEmpty()) {
160                                
161                                String[] tokens = s.split("[\\s,]+");
162                                
163                                Set<OperationType> ops = new HashSet<>();
164                                
165                                for (String t: tokens) {
166                                        try {
167                                                ops.add(OperationType.valueOf(t.toUpperCase()));
168                                        } catch (Exception e) {
169                                                throw new PropertyParseException("Invalid LDAP operation: " + t,
170                                                        "sampleDirectoryServer.operations",
171                                                        s);
172                                        }
173                                }
174                                
175                                operations = Collections.unmodifiableSet(ops);
176                                
177                        } else {
178                                operations = DEFAULT_OPERATIONS;
179                        }
180                        
181                        s = pr.getOptString("sampleDirectoryServer.schema", null);
182                        
183                        if (s == null || s.isEmpty())
184                                schema = null;
185                        else
186                                schema = s;
187                                
188                        baseDN = pr.getString("sampleDirectoryServer.baseDN");
189                        
190                        s = pr.getOptString("sampleDirectoryServer.content", null);
191                        
192                        if (s == null || s.isEmpty())
193                                content = null;
194                        else
195                                content = s;
196                }
197        }
198        
199        
200        /**
201         * The sample in-memory directory server.
202         */
203        private InMemoryDirectoryServer ds = null;
204        
205        
206        /**
207         * The servlet context.
208         */
209        private ServletContext servletContext;
210        
211        
212        /** 
213         * The logger. 
214         */
215        private final Logger log = LogManager.getLogger("MAIN");
216        
217        
218        /**
219         * Starts the sample in-memory directory server.
220         *
221         * @param config The sample directory server configuration. Must not
222         *               be {@code null}.
223         *
224         * @throws LDAPException If the in-memory directory server couldn't be
225         *                       started or its initialisation failed.
226         * @throws IOException   If a schema file was specified and it couldn't
227         *                       be read.
228         * @throws LDIFException If a schema file was specified that is not 
229         *                       valid LDIF.
230         */
231        public void start(final Configuration config)
232                throws LDAPException,
233                       IOException,
234                       LDIFException {
235                
236                if (! config.enable) {
237                
238                        log.info("Sample directory server: disabled");
239                        return;
240                }
241                
242                InMemoryListenerConfig listenerConfig = 
243                        InMemoryListenerConfig.createLDAPConfig("sample-ds", config.port);
244
245                // Get alternative schema, if any
246                Schema schema = null;
247
248                if (config.schema != null) {
249                        InputStream ldifInput;
250                        if (servletContext != null) {
251                                ldifInput = servletContext.getResourceAsStream(config.schema);
252                        } else {
253                                ldifInput = new FileInputStream(config.schema);
254                        }
255                        if (ldifInput == null) {
256                                throw new IOException("Couldn't find schema LDIF file: " + config.schema);
257                        }
258                        LDIFReader ldifReader = new LDIFReader(ldifInput);
259                        schema = new Schema(ldifReader.readEntry());
260                        log.info("Sample directory server: Schema LDIF file: {}", config.schema);
261                }
262
263
264                InMemoryDirectoryServerConfig dsConfig = new InMemoryDirectoryServerConfig(config.baseDN);
265
266                log.info("Sample directory server: Base DN: {}", config.baseDN);
267
268                dsConfig.setSchema(schema);
269                dsConfig.setListenerConfigs(listenerConfig);
270
271                // Set the allowed LDAP operations
272                dsConfig.setAllowedOperationTypes(config.operations);
273
274                // Start server
275                ds = new InMemoryDirectoryServer(dsConfig);
276                
277                // Populate directory with LDIF, if any
278                if (config.content != null) {
279                        InputStream ldifInput;
280                        if (servletContext != null) {
281                                ldifInput = servletContext.getResourceAsStream(config.content);
282                        } else {
283                                ldifInput = new FileInputStream(config.content);
284                        }
285                        if (ldifInput == null) {
286                                throw new IOException("Couldn't find directory content LDIF file: " + config.content);
287                        }
288                        ds.importFromLDIF(true, new LDIFReader(ldifInput));
289                        ldifInput.close();
290                        
291                        log.info("Sample directory server: Populated from LDIF file {}", config.content);
292                }
293
294                // Start listening on selected port
295                ds.startListening();
296
297                log.info("Sample directory server: Started on port {}", ds.getListenPort());
298        }
299        
300        
301        /**
302         * Stops the sample in-memory directory server (if previously started).
303         * Information and status messages are logged at INFO level.
304         */
305        public void stop() {
306        
307                if (ds == null)
308                        return;
309                
310                // Clean all connections and stop server
311                ds.shutDown(true);
312                
313                log.info("Sample directory server: Shut down");
314        }
315        
316        
317        /**
318         * Handler for servlet context startup events. Launches the sample
319         * in-memory directory server (if enabled per configuration). Exceptions
320         * are logged at ERROR level, information and status messages at INFO
321         * level.
322         *
323         * <p>The sample directory server configuration is retrieved from a
324         * properties file which location is specified by a servlet context
325         * parameter named {@code sampleDirectory.configurationFile}.
326         *
327         * @param sce A servlet context event.
328         */
329        @Override
330        public void contextInitialized(final ServletContextEvent sce) {
331
332                servletContext = sce.getServletContext();
333                
334                // Read configuration
335                Configuration config;
336                try {
337                        Properties props = ResourceRetriever.getProperties(servletContext,
338                                                                           "sampleDirectory.configurationFile",
339                                                                           log);
340                
341                        config = new Configuration(props);
342
343                } catch (Exception e) {
344                        log.error("Couldn't configure sample directory server: {}", e.getMessage());
345                        return;
346                }
347                
348                
349                // Start server
350                try {
351                        start(config);
352                } catch (LDAPException e) {
353                        log.error("Couldn't start sample directory server: {}", e.getMessage());
354                } catch (IOException | LDIFException e) {
355                        log.error("Couldn't read schema file: {}", e.getMessage());
356                }
357        }
358
359
360        /**
361         * Handler for servlet context shutdown events. Stops the sample
362         * in-memory directory server (if previously started).
363         *
364         * @param sce A servlet context event.
365         */
366        @Override
367        public void contextDestroyed(final ServletContextEvent sce) {
368                stop();
369        }
370}
371