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