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