Line data Source code
1 : // Copyright (C) 2009 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.pgm.http.jetty;
16 :
17 : import static java.util.concurrent.TimeUnit.MILLISECONDS;
18 : import static java.util.concurrent.TimeUnit.SECONDS;
19 :
20 : import com.google.common.annotations.VisibleForTesting;
21 : import com.google.common.base.Strings;
22 : import com.google.gerrit.extensions.client.AuthType;
23 : import com.google.gerrit.extensions.events.LifecycleListener;
24 : import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
25 : import com.google.gerrit.server.config.GerritServerConfig;
26 : import com.google.gerrit.server.config.SitePaths;
27 : import com.google.gerrit.server.config.ThreadSettingsConfig;
28 : import com.google.inject.Inject;
29 : import com.google.inject.Injector;
30 : import com.google.inject.Singleton;
31 : import com.google.inject.servlet.GuiceFilter;
32 : import com.google.inject.servlet.GuiceServletContextListener;
33 : import java.lang.management.ManagementFactory;
34 : import java.net.URI;
35 : import java.net.URISyntaxException;
36 : import java.nio.file.Files;
37 : import java.nio.file.Path;
38 : import java.util.ArrayList;
39 : import java.util.EnumSet;
40 : import java.util.HashMap;
41 : import java.util.HashSet;
42 : import java.util.List;
43 : import java.util.Map;
44 : import java.util.Set;
45 : import java.util.concurrent.TimeUnit;
46 : import java.util.concurrent.atomic.AtomicLong;
47 : import javax.servlet.DispatcherType;
48 : import javax.servlet.Filter;
49 : import javax.servlet.http.HttpSessionEvent;
50 : import javax.servlet.http.HttpSessionListener;
51 : import org.eclipse.jetty.http.HttpScheme;
52 : import org.eclipse.jetty.io.ConnectionStatistics;
53 : import org.eclipse.jetty.jmx.MBeanContainer;
54 : import org.eclipse.jetty.server.Connector;
55 : import org.eclipse.jetty.server.ForwardedRequestCustomizer;
56 : import org.eclipse.jetty.server.Handler;
57 : import org.eclipse.jetty.server.HttpConfiguration;
58 : import org.eclipse.jetty.server.HttpConnectionFactory;
59 : import org.eclipse.jetty.server.SecureRequestCustomizer;
60 : import org.eclipse.jetty.server.Server;
61 : import org.eclipse.jetty.server.ServerConnector;
62 : import org.eclipse.jetty.server.SslConnectionFactory;
63 : import org.eclipse.jetty.server.handler.ContextHandler;
64 : import org.eclipse.jetty.server.handler.ContextHandlerCollection;
65 : import org.eclipse.jetty.server.handler.RequestLogHandler;
66 : import org.eclipse.jetty.server.handler.StatisticsHandler;
67 : import org.eclipse.jetty.server.session.SessionHandler;
68 : import org.eclipse.jetty.servlet.DefaultServlet;
69 : import org.eclipse.jetty.servlet.FilterHolder;
70 : import org.eclipse.jetty.servlet.ServletContextHandler;
71 : import org.eclipse.jetty.servlet.ServletHolder;
72 : import org.eclipse.jetty.util.BlockingArrayQueue;
73 : import org.eclipse.jetty.util.log.Log;
74 : import org.eclipse.jetty.util.ssl.SslContextFactory;
75 : import org.eclipse.jetty.util.thread.QueuedThreadPool;
76 : import org.eclipse.jgit.lib.Config;
77 :
78 : @Singleton
79 : public class JettyServer {
80 : static class Lifecycle implements LifecycleListener {
81 : private final JettyServer server;
82 : private final Config cfg;
83 :
84 : @Inject
85 99 : Lifecycle(JettyServer server, @GerritServerConfig Config cfg) {
86 99 : this.server = server;
87 99 : this.cfg = cfg;
88 99 : }
89 :
90 : @Override
91 : public void start() {
92 : try {
93 99 : String origUrl = cfg.getString("httpd", null, "listenUrl");
94 99 : boolean rewrite = !Strings.isNullOrEmpty(origUrl) && origUrl.endsWith(":0/");
95 99 : server.httpd.start();
96 99 : if (rewrite) {
97 99 : Connector con = server.httpd.getConnectors()[0];
98 99 : if (con instanceof ServerConnector) {
99 : @SuppressWarnings("resource")
100 99 : ServerConnector serverCon = (ServerConnector) con;
101 99 : String host = serverCon.getHost();
102 99 : int port = serverCon.getLocalPort();
103 99 : String url = String.format("http://%s:%d", host, port);
104 99 : cfg.setString("gerrit", null, "canonicalWebUrl", url);
105 99 : cfg.setString("httpd", null, "listenUrl", url);
106 : }
107 : }
108 0 : } catch (Exception e) {
109 0 : throw new IllegalStateException("Cannot start HTTP daemon", e);
110 99 : }
111 99 : }
112 :
113 : @Override
114 : public void stop() {
115 : try {
116 99 : server.httpd.stop();
117 99 : server.httpd.join();
118 0 : } catch (Exception e) {
119 0 : throw new IllegalStateException("Cannot stop HTTP daemon", e);
120 99 : }
121 99 : }
122 : }
123 :
124 : static class Metrics {
125 : private final QueuedThreadPool threadPool;
126 : private ConnectionStatistics connStats;
127 :
128 99 : Metrics(QueuedThreadPool threadPool, ConnectionStatistics connStats) {
129 99 : this.threadPool = threadPool;
130 99 : this.connStats = connStats;
131 99 : }
132 :
133 : public int getIdleThreads() {
134 15 : return threadPool.getIdleThreads();
135 : }
136 :
137 : public int getBusyThreads() {
138 15 : return threadPool.getBusyThreads();
139 : }
140 :
141 : public int getReservedThreads() {
142 15 : return threadPool.getReservedThreads();
143 : }
144 :
145 : public int getMinThreads() {
146 15 : return threadPool.getMinThreads();
147 : }
148 :
149 : public int getMaxThreads() {
150 15 : return threadPool.getMaxThreads();
151 : }
152 :
153 : public int getThreads() {
154 15 : return threadPool.getThreads();
155 : }
156 :
157 : public int getQueueSize() {
158 15 : return threadPool.getQueueSize();
159 : }
160 :
161 : public boolean isLowOnThreads() {
162 15 : return threadPool.isLowOnThreads();
163 : }
164 :
165 : public long getConnections() {
166 15 : return connStats.getConnections();
167 : }
168 :
169 : public long getConnectionsTotal() {
170 15 : return connStats.getConnectionsTotal();
171 : }
172 :
173 : public long getConnectionDurationMax() {
174 15 : return connStats.getConnectionDurationMax();
175 : }
176 :
177 : public double getConnectionDurationMean() {
178 15 : return connStats.getConnectionDurationMean();
179 : }
180 :
181 : public double getConnectionDurationStdDev() {
182 15 : return connStats.getConnectionDurationStdDev();
183 : }
184 :
185 : public long getReceivedMessages() {
186 15 : return connStats.getReceivedMessages();
187 : }
188 :
189 : public long getSentMessages() {
190 15 : return connStats.getSentMessages();
191 : }
192 :
193 : public long getReceivedBytes() {
194 15 : return connStats.getReceivedBytes();
195 : }
196 :
197 : public long getSentBytes() {
198 15 : return connStats.getSentBytes();
199 : }
200 : }
201 :
202 : private final SitePaths site;
203 : private final Server httpd;
204 : private final Metrics metrics;
205 : private boolean reverseProxy;
206 : private ConnectionStatistics connStats;
207 : private final SessionHandler sessionHandler;
208 : private final AtomicLong sessionsCounter;
209 :
210 : @Inject
211 : JettyServer(
212 : @GerritServerConfig Config cfg,
213 : ThreadSettingsConfig threadSettingsConfig,
214 : SitePaths site,
215 : JettyEnv env,
216 99 : HttpLogFactory httpLogFactory) {
217 99 : this.site = site;
218 :
219 99 : QueuedThreadPool pool = threadPool(cfg, threadSettingsConfig);
220 99 : httpd = new Server(pool);
221 99 : httpd.setConnectors(listen(httpd, cfg));
222 99 : connStats = new ConnectionStatistics();
223 99 : for (Connector connector : httpd.getConnectors()) {
224 99 : connector.addBean(connStats);
225 : }
226 99 : metrics = new Metrics(pool, connStats);
227 99 : sessionHandler = new SessionHandler();
228 99 : sessionsCounter = new AtomicLong();
229 :
230 : /* Code used for testing purposes for making assertions
231 : * on the number of active HTTP sessions.
232 : */
233 99 : sessionHandler.addEventListener(
234 99 : new HttpSessionListener() {
235 :
236 : @Override
237 : public void sessionDestroyed(HttpSessionEvent se) {
238 0 : sessionsCounter.decrementAndGet();
239 0 : }
240 :
241 : @Override
242 : public void sessionCreated(HttpSessionEvent se) {
243 0 : sessionsCounter.incrementAndGet();
244 0 : }
245 : });
246 :
247 99 : Handler app = makeContext(env, cfg, sessionHandler);
248 99 : if (cfg.getBoolean("httpd", "requestLog", !reverseProxy)) {
249 15 : RequestLogHandler handler = new RequestLogHandler();
250 15 : handler.setRequestLog(httpLogFactory.get());
251 15 : handler.setHandler(app);
252 15 : app = handler;
253 : }
254 99 : if (cfg.getBoolean("httpd", "registerMBeans", false)) {
255 0 : MBeanContainer mbean = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
256 0 : httpd.addEventListener(mbean);
257 0 : httpd.addBean(Log.getRootLogger());
258 0 : httpd.addBean(mbean);
259 : }
260 :
261 99 : long gracefulStopTimeout =
262 99 : cfg.getTimeUnit("httpd", null, "gracefulStopTimeout", 0L, TimeUnit.MILLISECONDS);
263 99 : if (gracefulStopTimeout > 0) {
264 0 : StatisticsHandler statsHandler = new StatisticsHandler();
265 0 : statsHandler.setHandler(app);
266 0 : app = statsHandler;
267 0 : httpd.setStopTimeout(gracefulStopTimeout);
268 : }
269 :
270 99 : httpd.setHandler(app);
271 99 : httpd.setStopAtShutdown(false);
272 99 : }
273 :
274 : @VisibleForTesting
275 : public long numActiveSessions() {
276 2 : return sessionsCounter.longValue();
277 : }
278 :
279 : Metrics getMetrics() {
280 99 : return metrics;
281 : }
282 :
283 : private Connector[] listen(Server server, Config cfg) {
284 : // OpenID and certain web-based single-sign-on products can cause
285 : // some very long headers, especially in the Referer header. We
286 : // need to use a larger default header size to ensure we have
287 : // the space required.
288 : //
289 99 : final int requestHeaderSize = cfg.getInt("httpd", "requestheadersize", 16386);
290 99 : final URI[] listenUrls = listenURLs(cfg);
291 99 : final boolean reuseAddress = cfg.getBoolean("httpd", "reuseaddress", true);
292 99 : final int acceptors = cfg.getInt("httpd", "acceptorThreads", 2);
293 99 : final AuthType authType = cfg.getEnum("auth", null, "type", AuthType.OPENID);
294 :
295 99 : reverseProxy = isReverseProxied(listenUrls);
296 99 : final Connector[] connectors = new Connector[listenUrls.length];
297 99 : for (int idx = 0; idx < listenUrls.length; idx++) {
298 99 : final URI u = listenUrls[idx];
299 : final int defaultPort;
300 : final ServerConnector c;
301 99 : HttpConfiguration config = defaultConfig(requestHeaderSize);
302 :
303 99 : if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType) && !"https".equals(u.getScheme())) {
304 0 : throw new IllegalArgumentException(
305 : "Protocol '"
306 0 : + u.getScheme()
307 : + "' "
308 : + " not supported in httpd.listenurl '"
309 : + u
310 : + "' when auth.type = '"
311 0 : + AuthType.CLIENT_SSL_CERT_LDAP.name()
312 : + "'; only 'https' is supported");
313 : }
314 :
315 99 : if ("http".equals(u.getScheme())) {
316 99 : defaultPort = 80;
317 99 : c = newServerConnector(server, acceptors, config);
318 :
319 1 : } else if ("https".equals(u.getScheme())) {
320 0 : SslContextFactory.Server ssl = new SslContextFactory.Server();
321 0 : final Path keystore = getFile(cfg, "sslkeystore", "etc/keystore");
322 0 : String password = cfg.getString("httpd", null, "sslkeypassword");
323 0 : if (password == null) {
324 0 : password = "gerrit";
325 : }
326 0 : ssl.setKeyStorePath(keystore.toAbsolutePath().toString());
327 0 : ssl.setTrustStorePath(keystore.toAbsolutePath().toString());
328 0 : ssl.setKeyStorePassword(password);
329 0 : ssl.setTrustStorePassword(password);
330 :
331 0 : if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) {
332 0 : ssl.setNeedClientAuth(true);
333 :
334 0 : Path crl = getFile(cfg, "sslCrl", "etc/crl.pem");
335 0 : if (Files.exists(crl)) {
336 0 : ssl.setCrlPath(crl.toAbsolutePath().toString());
337 0 : ssl.setValidatePeerCerts(true);
338 : }
339 : }
340 :
341 0 : defaultPort = 443;
342 :
343 0 : config.addCustomizer(new SecureRequestCustomizer());
344 0 : c =
345 : new ServerConnector(
346 : server,
347 : null,
348 : null,
349 : null,
350 : 0,
351 : acceptors,
352 : new SslConnectionFactory(ssl, "http/1.1"),
353 : new HttpConnectionFactory(config));
354 :
355 1 : } else if ("proxy-http".equals(u.getScheme())) {
356 0 : defaultPort = 8080;
357 0 : config.addCustomizer(new ForwardedRequestCustomizer());
358 0 : c = newServerConnector(server, acceptors, config);
359 :
360 1 : } else if ("proxy-https".equals(u.getScheme())) {
361 1 : defaultPort = 8080;
362 1 : config.addCustomizer(new ForwardedRequestCustomizer());
363 1 : config.addCustomizer(
364 : (connector, channelConfig, request) -> {
365 1 : request.setScheme(HttpScheme.HTTPS.asString());
366 1 : request.setSecure(true);
367 1 : });
368 1 : c = newServerConnector(server, acceptors, config);
369 :
370 : } else {
371 0 : throw new IllegalArgumentException(
372 : "Protocol '"
373 0 : + u.getScheme()
374 : + "' "
375 : + " not supported in httpd.listenurl '"
376 : + u
377 : + "';"
378 : + " only 'http', 'https', 'proxy-http, 'proxy-https'"
379 : + " are supported");
380 : }
381 :
382 : try {
383 99 : if (u.getHost() == null
384 0 : && (u.getAuthority().equals("*") //
385 0 : || u.getAuthority().startsWith("*:"))) {
386 : // Bind to all local addresses. Port wasn't parsed right by URI
387 : // due to the illegal host of "*" so replace with a legal name
388 : // and parse the URI.
389 : //
390 0 : final URI r = new URI(u.toString().replace('*', 'A')).parseServerAuthority();
391 0 : c.setHost(null);
392 0 : c.setPort(0 < r.getPort() ? r.getPort() : defaultPort);
393 0 : } else {
394 99 : final URI r = u.parseServerAuthority();
395 99 : c.setHost(r.getHost());
396 99 : c.setPort(0 <= r.getPort() ? r.getPort() : defaultPort);
397 : }
398 0 : } catch (URISyntaxException e) {
399 0 : throw new IllegalArgumentException("Invalid httpd.listenurl " + u, e);
400 99 : }
401 99 : c.setInheritChannel(cfg.getBoolean("httpd", "inheritChannel", false));
402 99 : c.setReuseAddress(reuseAddress);
403 99 : c.setIdleTimeout(cfg.getTimeUnit("httpd", null, "idleTimeout", 30000L, MILLISECONDS));
404 99 : connectors[idx] = c;
405 : }
406 99 : return connectors;
407 : }
408 :
409 : private static ServerConnector newServerConnector(
410 : Server server, int acceptors, HttpConfiguration config) {
411 99 : return new ServerConnector(
412 : server, null, null, null, 0, acceptors, new HttpConnectionFactory(config));
413 : }
414 :
415 : private HttpConfiguration defaultConfig(int requestHeaderSize) {
416 99 : HttpConfiguration config = new HttpConfiguration();
417 99 : config.setRequestHeaderSize(requestHeaderSize);
418 99 : config.setSendServerVersion(false);
419 99 : config.setSendDateHeader(true);
420 99 : return config;
421 : }
422 :
423 : static boolean isReverseProxied(URI[] listenUrls) {
424 99 : for (URI u : listenUrls) {
425 99 : if ("http".equals(u.getScheme()) || "https".equals(u.getScheme())) {
426 99 : return false;
427 : }
428 : }
429 1 : return true;
430 : }
431 :
432 : static URI[] listenURLs(Config cfg) {
433 99 : String[] urls = cfg.getStringList("httpd", null, "listenurl");
434 99 : if (urls.length == 0) {
435 0 : urls = new String[] {"http://*:8080/"};
436 : }
437 :
438 99 : final URI[] r = new URI[urls.length];
439 99 : for (int i = 0; i < r.length; i++) {
440 99 : final String s = urls[i];
441 : try {
442 99 : r[i] = new URI(s);
443 0 : } catch (URISyntaxException e) {
444 0 : throw new IllegalArgumentException("Invalid httpd.listenurl " + s, e);
445 99 : }
446 : }
447 99 : return r;
448 : }
449 :
450 : private Path getFile(Config cfg, String name, String def) {
451 0 : String path = cfg.getString("httpd", null, name);
452 0 : if (path == null || path.length() == 0) {
453 0 : path = def;
454 : }
455 0 : return site.resolve(path);
456 : }
457 :
458 : private QueuedThreadPool threadPool(Config cfg, ThreadSettingsConfig threadSettingsConfig) {
459 99 : int maxThreads = threadSettingsConfig.getHttpdMaxThreads();
460 99 : int minThreads = cfg.getInt("httpd", null, "minthreads", 5);
461 99 : int maxQueued = cfg.getInt("httpd", null, "maxqueued", 200);
462 99 : int idleTimeout = (int) MILLISECONDS.convert(60, SECONDS);
463 99 : int maxCapacity = maxQueued == 0 ? Integer.MAX_VALUE : Math.max(minThreads, maxQueued);
464 99 : QueuedThreadPool pool =
465 : new QueuedThreadPool(
466 : maxThreads,
467 : minThreads,
468 : idleTimeout,
469 : new BlockingArrayQueue<>(
470 : minThreads, // capacity,
471 : minThreads, // growBy,
472 : maxCapacity // maxCapacity
473 : ));
474 99 : pool.setName("HTTP");
475 99 : return pool;
476 : }
477 :
478 : private Handler makeContext(JettyEnv env, Config cfg, SessionHandler sessionHandler) {
479 99 : final Set<String> paths = new HashSet<>();
480 99 : for (URI u : listenURLs(cfg)) {
481 99 : String p = u.getPath();
482 99 : if (p == null || p.isEmpty()) {
483 2 : p = "/";
484 : }
485 99 : while (1 < p.length() && p.endsWith("/")) {
486 0 : p = p.substring(0, p.length() - 1);
487 : }
488 99 : paths.add(p);
489 : }
490 :
491 99 : final List<ContextHandler> all = new ArrayList<>();
492 99 : for (String path : paths) {
493 99 : all.add(makeContext(path, env, cfg, sessionHandler));
494 99 : }
495 :
496 99 : if (all.size() == 1) {
497 : // If we only have one context path in our web space, return it
498 : // without any wrapping so Jetty has less work to do per-request.
499 : //
500 99 : return all.get(0);
501 : }
502 : // We have more than one path served out of this container so
503 : // combine them in a handler which supports dispatching to the
504 : // individual contexts.
505 : //
506 0 : final ContextHandlerCollection r = new ContextHandlerCollection();
507 0 : r.setHandlers(all.toArray(new Handler[0]));
508 0 : return r;
509 : }
510 :
511 : private ContextHandler makeContext(
512 : final String contextPath, JettyEnv env, Config cfg, SessionHandler sessionHandler) {
513 99 : final ServletContextHandler app = new ServletContextHandler();
514 :
515 : // This enables the use of sessions in Jetty, feature available
516 : // for Gerrit plug-ins to enable user-level sessions.
517 : //
518 99 : app.setSessionHandler(sessionHandler);
519 99 : app.setErrorHandler(new HiddenErrorHandler());
520 :
521 : // This is the path we are accessed by clients within our domain.
522 : //
523 99 : app.setContextPath(contextPath);
524 :
525 : // HTTP front-end filters to be used as surrogate of Apache HTTP
526 : // reverse-proxy filtering.
527 : // It is meant to be used as simpler tiny deployment of custom-made
528 : // security enforcement (Security tokens, IP-based security filtering, others)
529 99 : String[] filterClassNames = cfg.getStringList("httpd", null, "filterClass");
530 99 : for (String filterClassName : filterClassNames) {
531 : try {
532 : @SuppressWarnings("unchecked")
533 1 : Class<? extends Filter> filterClass =
534 1 : (Class<? extends Filter>) Class.forName(filterClassName);
535 1 : Filter filter = env.webInjector.getInstance(filterClass);
536 :
537 1 : Map<String, String> initParams = new HashMap<>();
538 1 : Set<String> initParamKeys = cfg.getNames("filterClass", filterClassName, true);
539 1 : initParamKeys.forEach(
540 : paramKey -> {
541 1 : String paramValue = cfg.getString("filterClass", filterClassName, paramKey);
542 1 : initParams.put(paramKey, paramValue);
543 1 : });
544 :
545 1 : FilterHolder filterHolder = new FilterHolder(filter);
546 1 : if (initParams.size() > 0) {
547 1 : filterHolder.setInitParameters(initParams);
548 : }
549 1 : app.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
550 0 : } catch (Exception e) {
551 0 : throw new IllegalArgumentException(
552 : "Unable to instantiate front-end HTTP Filter " + filterClassName, e);
553 1 : }
554 : }
555 :
556 : // Perform the same binding as our web.xml would do, but instead
557 : // of using the listener to create the injector pass the one we
558 : // already have built.
559 : //
560 99 : GuiceFilter filter = env.webInjector.getInstance(GuiceFilter.class);
561 99 : app.addFilter(
562 99 : new FilterHolder(filter), "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
563 99 : app.addEventListener(
564 99 : new GuiceServletContextListener() {
565 : @Override
566 : protected Injector getInjector() {
567 99 : return env.webInjector;
568 : }
569 : });
570 :
571 : // Jetty requires at least one servlet be bound before it will
572 : // bother running the filter above. Since the filter has all
573 : // of our URLs except the static resources, the only servlet
574 : // we need to bind is the default static resource servlet from
575 : // the Jetty container.
576 : //
577 99 : final ServletHolder ds = app.addServlet(DefaultServlet.class, "/");
578 99 : ds.setInitParameter("dirAllowed", "false");
579 99 : ds.setInitParameter("redirectWelcome", "false");
580 99 : ds.setInitParameter("useFileMappedBuffer", "false");
581 99 : ds.setInitParameter("gzip", "true");
582 :
583 99 : app.setWelcomeFiles(new String[0]);
584 99 : return app;
585 : }
586 : }
|