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