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