1 /* 2 * Copyright (C) 2010, 2017 Google Inc. and others 3 * 4 * This program and the accompanying materials are made available under the 5 * terms of the Eclipse Distribution License v. 1.0 which is available at 6 * https://www.eclipse.org/org/documents/edl-v10.php. 7 * 8 * SPDX-License-Identifier: BSD-3-Clause 9 */ 10 11 package org.eclipse.jgit.junit.http; 12 13 import static org.junit.Assert.assertFalse; 14 import static org.junit.Assert.assertTrue; 15 16 import java.io.File; 17 import java.io.IOException; 18 import java.net.InetAddress; 19 import java.net.URI; 20 import java.net.URISyntaxException; 21 import java.net.UnknownHostException; 22 import java.nio.file.Files; 23 import java.util.ArrayList; 24 import java.util.List; 25 import java.util.Locale; 26 import java.util.Map; 27 import java.util.concurrent.ConcurrentHashMap; 28 29 import org.eclipse.jetty.http.HttpVersion; 30 import org.eclipse.jetty.security.AbstractLoginService; 31 import org.eclipse.jetty.security.Authenticator; 32 import org.eclipse.jetty.security.ConstraintMapping; 33 import org.eclipse.jetty.security.ConstraintSecurityHandler; 34 import org.eclipse.jetty.security.authentication.BasicAuthenticator; 35 import org.eclipse.jetty.server.Connector; 36 import org.eclipse.jetty.server.HttpConfiguration; 37 import org.eclipse.jetty.server.HttpConnectionFactory; 38 import org.eclipse.jetty.server.Server; 39 import org.eclipse.jetty.server.ServerConnector; 40 import org.eclipse.jetty.server.SslConnectionFactory; 41 import org.eclipse.jetty.server.handler.ContextHandlerCollection; 42 import org.eclipse.jetty.servlet.ServletContextHandler; 43 import org.eclipse.jetty.util.security.Constraint; 44 import org.eclipse.jetty.util.security.Password; 45 import org.eclipse.jetty.util.ssl.SslContextFactory; 46 import org.eclipse.jgit.transport.URIish; 47 48 /** 49 * Tiny web application server for unit testing. 50 * <p> 51 * Tests should start the server in their {@code setUp()} method and stop the 52 * server in their {@code tearDown()} method. Only while started the server's 53 * URL and/or port number can be obtained. 54 */ 55 public class AppServer { 56 /** Realm name for the secure access areas. */ 57 public static final String realm = "Secure Area"; 58 59 /** Username for secured access areas. */ 60 public static final String username = "agitter"; 61 62 /** Password for {@link #username} in secured access areas. */ 63 public static final String password = "letmein"; 64 65 /** SSL keystore password; must have at least 6 characters. */ 66 private static final String keyPassword = "mykeys"; 67 68 /** Role for authentication. */ 69 private static final String authRole = "can-access"; 70 71 static { 72 // Install a logger that throws warning messages. 73 // 74 final String prop = "org.eclipse.jetty.util.log.class"; System.setProperty(prop, RecordingLogger.class.getName())75 System.setProperty(prop, RecordingLogger.class.getName()); 76 } 77 78 private final Server server; 79 80 private final HttpConfiguration config; 81 82 private final ServerConnector connector; 83 84 private final HttpConfiguration secureConfig; 85 86 private final ServerConnector secureConnector; 87 88 private final ContextHandlerCollection contexts; 89 90 private final TestRequestLog log; 91 92 private List<File> filesToDelete = new ArrayList<>(); 93 94 /** 95 * Constructor for <code>AppServer</code>. 96 */ AppServer()97 public AppServer() { 98 this(0, -1); 99 } 100 101 /** 102 * Constructor for <code>AppServer</code>. 103 * 104 * @param port 105 * the http port number; may be zero to allocate a port 106 * dynamically 107 * @since 4.2 108 */ AppServer(int port)109 public AppServer(int port) { 110 this(port, -1); 111 } 112 113 /** 114 * Constructor for <code>AppServer</code>. 115 * 116 * @param port 117 * for http, may be zero to allocate a port dynamically 118 * @param sslPort 119 * for https,may be zero to allocate a port dynamically. If 120 * negative, the server will be set up without https support. 121 * @since 4.9 122 */ AppServer(int port, int sslPort)123 public AppServer(int port, int sslPort) { 124 server = new Server(); 125 126 config = new HttpConfiguration(); 127 config.setSecureScheme("https"); 128 config.setSecurePort(0); 129 config.setOutputBufferSize(32768); 130 131 connector = new ServerConnector(server, 132 new HttpConnectionFactory(config)); 133 connector.setPort(port); 134 String ip; 135 String hostName; 136 try { 137 final InetAddress me = InetAddress.getByName("localhost"); 138 ip = me.getHostAddress(); 139 connector.setHost(ip); 140 hostName = InetAddress.getLocalHost().getCanonicalHostName(); 141 } catch (UnknownHostException e) { 142 throw new RuntimeException("Cannot find localhost", e); 143 } 144 145 if (sslPort >= 0) { 146 SslContextFactory sslContextFactory = createTestSslContextFactory( 147 hostName); 148 secureConfig = new HttpConfiguration(config); 149 secureConnector = new ServerConnector(server, 150 new SslConnectionFactory(sslContextFactory, 151 HttpVersion.HTTP_1_1.asString()), 152 new HttpConnectionFactory(secureConfig)); 153 secureConnector.setPort(sslPort); 154 secureConnector.setHost(ip); 155 } else { 156 secureConfig = null; 157 secureConnector = null; 158 } 159 160 contexts = new ContextHandlerCollection(); 161 162 log = new TestRequestLog(); 163 log.setHandler(contexts); 164 165 if (secureConnector == null) { 166 server.setConnectors(new Connector[] { connector }); 167 } else { 168 server.setConnectors( 169 new Connector[] { connector, secureConnector }); 170 } 171 server.setHandler(log); 172 } 173 createTestSslContextFactory(String hostName)174 private SslContextFactory createTestSslContextFactory(String hostName) { 175 SslContextFactory.Client factory = new SslContextFactory.Client(true); 176 177 String dName = "CN=,OU=,O=,ST=,L=,C="; 178 179 try { 180 File tmpDir = Files.createTempDirectory("jks").toFile(); 181 tmpDir.deleteOnExit(); 182 makePrivate(tmpDir); 183 File keyStore = new File(tmpDir, "keystore.jks"); 184 Runtime.getRuntime().exec( 185 new String[] { 186 "keytool", // 187 "-keystore", keyStore.getAbsolutePath(), // 188 "-storepass", keyPassword, 189 "-alias", hostName, // 190 "-genkeypair", // 191 "-keyalg", "RSA", // 192 "-keypass", keyPassword, // 193 "-dname", dName, // 194 "-validity", "2" // 195 }).waitFor(); 196 keyStore.deleteOnExit(); 197 makePrivate(keyStore); 198 filesToDelete.add(keyStore); 199 filesToDelete.add(tmpDir); 200 factory.setKeyStorePath(keyStore.getAbsolutePath()); 201 factory.setKeyStorePassword(keyPassword); 202 factory.setKeyManagerPassword(keyPassword); 203 factory.setTrustStorePath(keyStore.getAbsolutePath()); 204 factory.setTrustStorePassword(keyPassword); 205 } catch (InterruptedException | IOException e) { 206 throw new RuntimeException("Cannot create ssl key/certificate", e); 207 } 208 return factory; 209 } 210 makePrivate(File file)211 private void makePrivate(File file) { 212 file.setReadable(false); 213 file.setWritable(false); 214 file.setExecutable(false); 215 file.setReadable(true, true); 216 file.setWritable(true, true); 217 if (file.isDirectory()) { 218 file.setExecutable(true, true); 219 } 220 } 221 222 /** 223 * Create a new servlet context within the server. 224 * <p> 225 * This method should be invoked before the server is started, once for each 226 * context the caller wants to register. 227 * 228 * @param path 229 * path of the context; use "/" for the root context if binding 230 * to the root is desired. 231 * @return the context to add servlets into. 232 */ addContext(String path)233 public ServletContextHandler addContext(String path) { 234 assertNotYetSetUp(); 235 if ("".equals(path)) 236 path = "/"; 237 238 ServletContextHandler ctx = new ServletContextHandler(); 239 ctx.setContextPath(path); 240 contexts.addHandler(ctx); 241 242 return ctx; 243 } 244 245 /** 246 * Configure basic authentication. 247 * 248 * @param ctx 249 * @param methods 250 * @return servlet context handler 251 */ authBasic(ServletContextHandler ctx, String... methods)252 public ServletContextHandler authBasic(ServletContextHandler ctx, 253 String... methods) { 254 assertNotYetSetUp(); 255 auth(ctx, new BasicAuthenticator(), methods); 256 return ctx; 257 } 258 259 static class TestMappedLoginService extends AbstractLoginService { 260 private String role; 261 262 protected final Map<String, UserPrincipal> users = new ConcurrentHashMap<>(); 263 TestMappedLoginService(String role)264 TestMappedLoginService(String role) { 265 this.role = role; 266 } 267 268 @Override doStart()269 protected void doStart() throws Exception { 270 UserPrincipal p = new UserPrincipal(username, 271 new Password(password)); 272 users.put(username, p); 273 super.doStart(); 274 } 275 276 @Override loadRoleInfo(UserPrincipal user)277 protected String[] loadRoleInfo(UserPrincipal user) { 278 if (users.get(user.getName()) == null) { 279 return null; 280 } 281 return new String[] { role }; 282 } 283 284 @Override loadUserInfo(String user)285 protected UserPrincipal loadUserInfo(String user) { 286 return users.get(user); 287 } 288 } 289 createConstraintMapping()290 private ConstraintMapping createConstraintMapping() { 291 ConstraintMapping cm = new ConstraintMapping(); 292 cm.setConstraint(new Constraint()); 293 cm.getConstraint().setAuthenticate(true); 294 cm.getConstraint().setDataConstraint(Constraint.DC_NONE); 295 cm.getConstraint().setRoles(new String[] { authRole }); 296 cm.setPathSpec("/*"); 297 return cm; 298 } 299 auth(ServletContextHandler ctx, Authenticator authType, String... methods)300 private void auth(ServletContextHandler ctx, Authenticator authType, 301 String... methods) { 302 AbstractLoginService users = new TestMappedLoginService(authRole); 303 List<ConstraintMapping> mappings = new ArrayList<>(); 304 if (methods == null || methods.length == 0) { 305 mappings.add(createConstraintMapping()); 306 } else { 307 for (String method : methods) { 308 ConstraintMapping cm = createConstraintMapping(); 309 cm.setMethod(method.toUpperCase(Locale.ROOT)); 310 mappings.add(cm); 311 } 312 } 313 314 ConstraintSecurityHandler sec = new ConstraintSecurityHandler(); 315 sec.setRealmName(realm); 316 sec.setAuthenticator(authType); 317 sec.setLoginService(users); 318 sec.setConstraintMappings( 319 mappings.toArray(new ConstraintMapping[0])); 320 sec.setHandler(ctx); 321 322 contexts.removeHandler(ctx); 323 contexts.addHandler(sec); 324 } 325 326 /** 327 * Start the server on a random local port. 328 * 329 * @throws Exception 330 * the server cannot be started, testing is not possible. 331 */ setUp()332 public void setUp() throws Exception { 333 RecordingLogger.clear(); 334 log.clear(); 335 server.start(); 336 config.setSecurePort(getSecurePort()); 337 if (secureConfig != null) { 338 secureConfig.setSecurePort(getSecurePort()); 339 } 340 } 341 342 /** 343 * Shutdown the server. 344 * 345 * @throws Exception 346 * the server refuses to halt, or wasn't running. 347 */ tearDown()348 public void tearDown() throws Exception { 349 RecordingLogger.clear(); 350 log.clear(); 351 server.stop(); 352 for (File f : filesToDelete) { 353 f.delete(); 354 } 355 filesToDelete.clear(); 356 } 357 358 /** 359 * Get the URI to reference this server. 360 * <p> 361 * The returned URI includes the proper host name and port number, but does 362 * not contain a path. 363 * 364 * @return URI to reference this server's root context. 365 */ getURI()366 public URI getURI() { 367 assertAlreadySetUp(); 368 String host = connector.getHost(); 369 if (host.contains(":") && !host.startsWith("[")) 370 host = "[" + host + "]"; 371 final String uri = "http://" + host + ":" + getPort(); 372 try { 373 return new URI(uri); 374 } catch (URISyntaxException e) { 375 throw new RuntimeException("Unexpected URI error on " + uri, e); 376 } 377 } 378 379 /** 380 * Get port. 381 * 382 * @return the local port number the server is listening on. 383 */ getPort()384 public int getPort() { 385 assertAlreadySetUp(); 386 return connector.getLocalPort(); 387 } 388 389 /** 390 * Get secure port. 391 * 392 * @return the HTTPS port or -1 if not configured. 393 */ getSecurePort()394 public int getSecurePort() { 395 assertAlreadySetUp(); 396 return secureConnector != null ? secureConnector.getLocalPort() : -1; 397 } 398 399 /** 400 * Get requests. 401 * 402 * @return all requests since the server was started. 403 */ getRequests()404 public List<AccessEvent> getRequests() { 405 return new ArrayList<>(log.getEvents()); 406 } 407 408 /** 409 * Get requests. 410 * 411 * @param base 412 * base URI used to access the server. 413 * @param path 414 * the path to locate requests for, relative to {@code base}. 415 * @return all requests which match the given path. 416 */ getRequests(URIish base, String path)417 public List<AccessEvent> getRequests(URIish base, String path) { 418 return getRequests(HttpTestCase.join(base, path)); 419 } 420 421 /** 422 * Get requests. 423 * 424 * @param path 425 * the path to locate requests for. 426 * @return all requests which match the given path. 427 */ getRequests(String path)428 public List<AccessEvent> getRequests(String path) { 429 ArrayList<AccessEvent> r = new ArrayList<>(); 430 for (AccessEvent event : log.getEvents()) { 431 if (event.getPath().equals(path)) { 432 r.add(event); 433 } 434 } 435 return r; 436 } 437 assertNotYetSetUp()438 private void assertNotYetSetUp() { 439 assertFalse("server is not running", server.isRunning()); 440 } 441 assertAlreadySetUp()442 private void assertAlreadySetUp() { 443 assertTrue("server is running", server.isRunning()); 444 } 445 } 446