001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.doc; 018 019import static org.forgerock.guava.common.base.Strings.isNullOrEmpty; 020import org.forgerock.json.JsonValue; 021import org.glassfish.grizzly.http.Cookie; 022import org.glassfish.grizzly.http.Method; 023import org.glassfish.grizzly.http.server.HttpHandler; 024import org.glassfish.grizzly.http.server.HttpServer; 025import org.glassfish.grizzly.http.server.NetworkListener; 026import org.glassfish.grizzly.http.server.Request; 027import org.glassfish.grizzly.http.server.Response; 028import org.glassfish.grizzly.ssl.SSLContextConfigurator; 029import org.glassfish.grizzly.ssl.SSLEngineConfigurator; 030 031import java.io.ByteArrayOutputStream; 032import java.io.IOException; 033import java.io.InputStream; 034import java.util.ArrayList; 035import java.util.LinkedHashMap; 036import java.util.List; 037import java.util.Properties; 038import java.util.Scanner; 039import java.util.logging.Level; 040import java.util.logging.Logger; 041 042/** 043 * Simple servlet allowing user-agents to get a home page, 044 * and to post form-based login to access a protected profile page. 045 */ 046public final class SampleServer { 047 048 private static final String EOL = System.getProperty("line.separator"); 049 private static final Logger LOGGER = Logger.getLogger(SampleServer.class.getName()); 050 private static final int DEFAULT_PORT = 8081; 051 private static final int DEFAULT_SSL_PORT = 8444; 052 /** Name of fake OpenAM cookie. */ 053 private static final String IPLANET_DIRECTORY_PRO_COOKIE = "iPlanetDirectoryPro"; 054 /** URL used to contact OpenAM. */ 055 private static String openamUrl; 056 057 /** 058 * Start an HTTP server. 059 * 060 * @param args Optionally specify a free port number and free SSL port number. 061 * Defaults: 8081 (HTTP), 8444 (HTTPS). 062 * Also optionally specify a URL used to contact OpenAM. 063 * Default: http://openam.example.com:8088/openam/oauth2 064 */ 065 public static void main(String[] args) { 066 final String usage = "Optionally specify HTTP and HTTPS port numbers. " 067 + "Defaults: " + DEFAULT_PORT + ", " + DEFAULT_SSL_PORT + ".\n" 068 + "Also optionally specify a URL to contact OpenAM. " 069 + "Default: " + getOpenamUrl(); 070 int port = DEFAULT_PORT; 071 int sslPort = DEFAULT_SSL_PORT; 072 073 if (args.length > 3) { 074 System.out.println(usage); 075 System.exit(-1); 076 } 077 078 if (args.length > 0) { 079 port = Integer.parseInt(args[0]); 080 if (args.length >= 2) { 081 sslPort = Integer.parseInt(args[1]); 082 } 083 if (args.length == 3) { 084 openamUrl = args[2]; 085 } 086 } 087 // If the OpenAM URL is not set, set it now. 088 openamUrl = getOpenamUrl(); 089 090 LOGGER.setLevel(Level.INFO); 091 runServer(port, sslPort); 092 } 093 094 /** 095 * Returns the URL used to contact OpenAM, 096 * setting it to the default if it is not already set. 097 * @return The URL used to contact OpenAM. 098 */ 099 private static String getOpenamUrl() { 100 if (isNullOrEmpty(openamUrl)) { 101 return "http://openam.example.com:8088/openam/oauth2"; 102 } else { 103 return openamUrl; 104 } 105 } 106 107 /** 108 * Run the HTTP server, listening on the chosen port. 109 * <p> 110 * On HTTP GET the server returns a home page with a login form. 111 * <p> 112 * On HTTP PUT with valid credentials, the server returns a profile page. 113 * 114 * @param port Port on which the server listens for HTTP 115 * @param sslPort Port on which the server listens for HTTPS 116 */ 117 static void runServer(int port, int sslPort) { 118 start(port, sslPort, true); 119 } 120 121 /** 122 * Run the HTTP server, listening on the chosen port. 123 * <p> 124 * Use stop() to shut the server down. 125 * 126 * @param port Port on which the server listens for HTTP 127 * @return The HttpServer that is running if letRun is true 128 */ 129 static HttpServer start(final int port, final int sslPort) { 130 return start(port, sslPort, false); 131 } 132 133 /** 134 * Run the HTTP server, listening on the chosen port. 135 * 136 * @param port Port on which the server listens for HTTP 137 * @param sslPort Port on which the server listens for HTTPS 138 * @param waitForCtrlC If true, only stop the server when the user enters Ctrl+C 139 * @return The HttpServer that is running if letRun is true 140 */ 141 static HttpServer start(final int port, final int sslPort, final boolean waitForCtrlC) { 142 143 final HttpServer httpServer = new HttpServer(); 144 System.out.println("Preparing to listen for HTTP on port " + port + "."); 145 httpServer.addListener(new NetworkListener("HTTP", "0.0.0.0", port)); 146 SSLEngineConfigurator sslEngineConfigurator = createSslConfiguration(); 147 if (sslEngineConfigurator != null) { 148 System.out.println("Preparing to listen for HTTPS on port " + sslPort + "."); 149 System.out.println("The server will use a self-signed certificate not known to browsers."); 150 System.out.println("When using HTTPS with curl for example, try --insecure."); 151 httpServer.addListener(new NetworkListener("HTTPS", "0.0.0.0", sslPort)); 152 httpServer.getListener("HTTPS").setSSLEngineConfig(sslEngineConfigurator); 153 httpServer.getListener("HTTPS").setSecure(true); 154 } 155 httpServer.getServerConfiguration().addHttpHandler(new SampleHandler()); 156 157 System.out.println("Using OpenAM URL: " + getOpenamUrl() + "."); 158 159 if (waitForCtrlC) { 160 Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { 161 @Override 162 public void run() { 163 httpServer.shutdownNow(); 164 } 165 }, "shutDownHook")); 166 } 167 168 try { 169 System.out.println("Starting server..."); 170 httpServer.start(); 171 if (waitForCtrlC) { 172 System.out.println("Press Ctrl+C to stop the server."); 173 Thread.currentThread().join(); 174 } 175 } catch (Exception e) { 176 LOGGER.info(e.getMessage()); 177 } 178 179 return httpServer; 180 } 181 182 /** 183 * Stop the HTTP Server started with waitForCtrlC set to false. 184 * 185 * @param httpServer The server to stop 186 */ 187 static void stop(final HttpServer httpServer) { 188 httpServer.shutdownNow(); 189 } 190 191 /** 192 * Returns an SSL configuration that uses {@code keystore.jks}. 193 * 194 * <br> 195 * 196 * The key store has one key pair with a self-signed cert, {@code test}. 197 * The key store was set up with the following commands: 198 * 199 * <pre> 200 * keytool \ 201 * -genkey \ 202 * -alias test \ 203 * -keyalg rsa \ 204 * -dname "cn=Doc Sample Server,dc=openig,dc=example,dc=com" \ 205 * -keystore keystore.jks \ 206 * -storepass changeit \ 207 * -keypass changeit 208 * 209 * keytool \ 210 * -selfcert \ 211 * -alias test \ 212 * -validity 7300 \ 213 * -keystore keystore.jks \ 214 * -storepass changeit \ 215 * -keypass changeit 216 * </pre> 217 * 218 * @return An SSL configuration that uses {@code keystore.jks}, 219 * or null if no valid SSL configuration could be set up. 220 */ 221 private static SSLEngineConfigurator createSslConfiguration() { 222 SSLContextConfigurator sslContextConfigurator = new SSLContextConfigurator(); 223 try { 224 // Cannot read the key store as a file inside a jar, 225 // so get the key store as a byte array. 226 sslContextConfigurator.setKeyStoreBytes(getKeyStore()); 227 sslContextConfigurator.setKeyStorePass("changeit"); 228 sslContextConfigurator.setKeyPass("changeit"); 229 } catch (IOException e) { 230 LOGGER.info("Failed to load key store when setting up HTTPS."); 231 e.printStackTrace(); 232 return null; 233 } 234 235 if (sslContextConfigurator.validateConfiguration(true)) { 236 SSLEngineConfigurator sslEngineConfigurator = 237 new SSLEngineConfigurator(sslContextConfigurator.createSSLContext()); 238 sslEngineConfigurator.setClientMode(false); 239 sslEngineConfigurator.setNeedClientAuth(false); 240 sslEngineConfigurator.setWantClientAuth(false); 241 return sslEngineConfigurator; 242 } else { 243 LOGGER.info("Failed to build a valid HTTPS configuration."); 244 return null; 245 } 246 } 247 248 /** 249 * Returns {@code keystore.jks} as a byte array. 250 * @return {@code keystore.jks} as a byte array. 251 * @throws IOException Failed to read {@code keystore.jks}. 252 */ 253 private static byte[] getKeyStore() throws IOException { 254 InputStream inputStream = SampleServer.class.getResourceAsStream("/keystore.jks"); 255 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 256 int read; 257 byte[] data = new byte[4096]; 258 while ((read = inputStream.read(data, 0, data.length)) != -1) { 259 buffer.write(data, 0, read); 260 } 261 return buffer.toByteArray(); 262 } 263 264 /** 265 * Handler for HTTP GET and HTTP PUT requests. 266 */ 267 static class SampleHandler extends HttpHandler { 268 269 @Override 270 public void service(Request request, Response response) throws Exception { 271 if (request.getHttpHandlerPath().equalsIgnoreCase("/login")) { 272 response.addCookie(new Cookie("login-cookie", "chocolate-chip")); 273 } 274 275 if (request.getHttpHandlerPath().startsWith("/uma")) { 276 // Provides access to UMA example client files. 277 // If there is a need to serve more files in the future, 278 // consider a CLStaticHttpHandler. 279 final String file = request.getHttpHandlerPath(); 280 String content = SampleServer.getResourceAsString(file); 281 if (isNullOrEmpty(content)) { 282 content = SampleServer.getResourceAsString("/uma/index.html"); 283 } 284 285 if (file.endsWith("css")) { 286 response.setContentType("text/css"); 287 } else if (file.endsWith("js")) { 288 response.setContentType("application/javascript"); 289 } else { 290 response.setContentType("text/html"); 291 } 292 293 response.setStatus(200, "OK"); 294 response.setContentLength(content.length()); 295 response.getWriter().write(content); 296 return; 297 } 298 299 if (request.getHttpHandlerPath().equalsIgnoreCase("/.well-known/webfinger")) { 300 // See http://tools.ietf.org/html/rfc7033 301 // This is not fully compliant. Just enough for an OIDC discovery example. 302 response.setContentType("application/jrd+json"); 303 response.setCharacterEncoding("UTF-8"); 304 305 String subject = request.getParameter("resource"); 306 if (isNullOrEmpty(subject)) { 307 JsonValue error = new JsonValue(new LinkedHashMap<String, Object>()); 308 error.put("error", "Request must include a resource parameter."); 309 response.setStatus(400, "Bad Request"); 310 response.setContentLength(error.toString().length()); 311 response.getWriter().write(error.toString()); 312 return; 313 } 314 315 LinkedHashMap<String, Object> links = new LinkedHashMap<>(); 316 // Just enough for an OIDC discovery example. 317 links.put("rel", "http://openid.net/specs/connect/1.0/issuer"); 318 links.put("href", getOpenamUrl()); 319 List<LinkedHashMap<String, Object>> list = new ArrayList<>(); 320 list.add(links); 321 JsonValue json = new JsonValue(new LinkedHashMap<String, Object>()); 322 json.put("subject", subject); 323 json.put("links", list); 324 325 response.setStatus(200, "OK"); 326 response.setHeader("Access-Control-Allow-Origin", "*"); 327 response.setContentLength(json.toString().length()); 328 response.getWriter().write(json.toString()); 329 return; 330 } 331 332 // When a fake OpenAM cookie is presented, simulate OpenAM's response. 333 // If the cookie smells real, continue processing. 334 for (Cookie cookie : request.getCookies()) { 335 if (cookie.getName().equalsIgnoreCase(IPLANET_DIRECTORY_PRO_COOKIE)) { 336 String[] credentials = cookie.getValue().split(":"); 337 if (credentials.length == 2) { 338 simulateOpenAMResponse(credentials[0], credentials[1], response); 339 return; 340 } 341 } 342 } 343 344 if (Method.GET == request.getMethod()) { 345 String homePage = getResourceAsString("/home.html"); 346 347 response.setContentType("text/html"); 348 response.setStatus(200, "OK"); 349 response.setContentLength(homePage.length()); 350 response.getWriter().write(homePage); 351 } 352 353 if (Method.POST == request.getMethod()) { 354 String username; 355 String password; 356 357 // Allow use of IDToken1 (username) and IDToken2 (password) 358 // to simulate the behavior of the OpenAM classic UI login page. 359 username = request.getParameter("IDToken1"); 360 password = request.getParameter("IDToken2"); 361 if (username != null && password != null) { 362 simulateOpenAMResponse(username, password, response); 363 return; 364 } 365 366 // Accept username and password as headers for testing. 367 if (notNullOrEmpty(request.getHeader("username"))) { 368 username = request.getHeader("username"); 369 } 370 if (notNullOrEmpty(request.getHeader("password"))) { 371 password = request.getHeader("password"); 372 } 373 374 // Accept username and password as parameters 375 // in the query string or as form-encoded data. 376 if (notNullOrEmpty(request.getParameter("username"))) { 377 username = request.getParameter("username"); 378 } 379 if (notNullOrEmpty(request.getParameter("password"))) { 380 password = request.getParameter("password"); 381 } 382 383 if (username == null || password == null) { 384 final String authRequired = "Authorization Required"; 385 response.setStatus(401, authRequired); 386 response.setContentLength(authRequired.length() + EOL.length()); 387 response.getWriter().write(authRequired + EOL); 388 return; 389 } 390 391 if (credentialsAreValid(username, password)) { 392 393 // Replace profile page placeholders and respond. 394 final StringBuilder headers = new StringBuilder(); 395 for (String name : request.getHeaderNames()) { 396 for (String header : request.getHeaders(name)) { 397 headers.append(name) 398 .append(": ") 399 .append(header) 400 .append("<br>"); 401 } 402 } 403 404 String profilePage = getResourceAsString("/profile.html") 405 .replaceAll(EOL, "####") 406 .replaceAll("USERNAME", username) 407 .replace("METHOD", request.getMethod().getMethodString()) 408 .replace("REQUEST_URI", request.getDecodedRequestURI()) 409 .replace("HEADERS", headers.toString()) 410 .replaceAll("####", EOL); 411 412 response.setContentType("text/html"); 413 response.setStatus(200, "OK"); 414 response.setContentLength(profilePage.length()); 415 response.getWriter().write(profilePage); 416 417 } else { 418 final String forbidden = "Forbidden"; 419 response.setStatus(403, forbidden); 420 response.setContentLength(forbidden.length() + EOL.length()); 421 response.getWriter().write(forbidden + EOL); 422 } 423 } 424 } 425 426 /** 427 * Returns true if the String to test is null nor empty. 428 * @param s The String to test. 429 * @return true if the String to test is null nor empty. 430 */ 431 private static boolean isNullOrEmpty(final String s) { 432 return s == null || s.isEmpty(); 433 } 434 435 /** 436 * Returns true if the String to test is neither null nor empty. 437 * @param s The String to test. 438 * @return true if the String to test is neither null nor empty. 439 */ 440 private static boolean notNullOrEmpty(final String s) { 441 return s != null && !s.isEmpty(); 442 } 443 } 444 445 /** 446 * Simulates a response from OpenAM. 447 * 448 * <br> 449 * 450 * If the username and password are valid, sets response status to 200 OK 451 * and writes a text message and fake SSO Token cookie in the response. 452 * The fake cookie holds the credentials. 453 * 454 * Otherwise, sets response status to 403 Forbidden 455 * and writes a failure text message in the response. 456 * 457 * @param username A username such as {@code demo} 458 * @param password A password such as {@code changeit} 459 * @param response The response to the request 460 * @throws IOException Failed when checking credentials 461 */ 462 private static void simulateOpenAMResponse(final String username, 463 final String password, 464 final Response response) throws IOException { 465 String message; 466 if (credentialsAreValid(username, password)) { 467 message = "Welcome, " + username + "!" + EOL; 468 Cookie cookie = new Cookie(IPLANET_DIRECTORY_PRO_COOKIE, username + ":" + password); 469 cookie.setPath("/"); 470 response.addCookie(cookie); 471 response.setStatus(200, "OK"); 472 } else { 473 String user = isNullOrEmpty(username) ? "" : username + ", "; 474 message = "Too bad, " + user + "you failed to authenticate." + EOL; 475 response.setStatus(403, "Forbidden"); 476 } 477 response.setContentType("text/plain"); 478 response.setContentLength(message.length()); 479 response.getWriter().write(message); 480 } 481 482 /** 483 * Check whether username and password credentials are valid. 484 * 485 * @param username A username such as {@code demo} 486 * @param password A password such as {@code changeit} 487 * 488 * @return True if the username matches the password in credentials.properties 489 * @throws IOException Could not read credentials.properties 490 */ 491 static synchronized boolean credentialsAreValid( 492 final String username, final String password) 493 throws IOException { 494 495 if (isNullOrEmpty(username) || isNullOrEmpty(password)) { 496 return false; 497 } 498 499 boolean result = false; 500 501 Properties credentials = new Properties(); 502 InputStream in = SampleHandler.class.getResourceAsStream("/credentials.properties"); 503 credentials.load(in); 504 505 final String pwd = credentials.getProperty(username); 506 if (pwd != null) { 507 result = pwd.equals(password); 508 } 509 510 in.close(); 511 512 return result; 513 } 514 515 /** 516 * Read the contents of a resource file into a string. 517 * 518 * @param resource Path to resource file 519 * @return String holding the content of the resource file 520 */ 521 static synchronized String getResourceAsString(final String resource) { 522 523 StringBuilder content = new StringBuilder(); 524 InputStream inputStream = SampleHandler.class.getResourceAsStream(resource); 525 526 Scanner scanner = null; 527 try { 528 scanner = new Scanner(inputStream); 529 while (scanner.hasNextLine()) { 530 content.append(scanner.nextLine()).append(EOL); 531 } 532 } finally { 533 if (scanner != null) { 534 scanner.close(); 535 } 536 } 537 538 return content.toString(); 539 } 540 541 /** 542 * Not used. 543 */ 544 private SampleServer() { 545 } 546}