xref: /JGit/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java (revision 0f442d70836ee292ed916605448f806cc7d1fe78)
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