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