/* * Copyright 2011-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.glowroot.ui; import java.io.File; import java.net.InetSocketAddress; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import javax.annotation.Nullable; import com.google.common.base.Supplier; import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.internal.logging.InternalLoggerFactory; import io.netty.util.internal.logging.Slf4JLoggerFactory; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.google.common.base.Preconditions.checkNotNull; import static java.util.concurrent.TimeUnit.SECONDS; class HttpServer { private static final Logger logger = LoggerFactory.getLogger(HttpServer.class); // log startup messages using logger name "org.glowroot" private static final Logger startupLogger = LoggerFactory.getLogger("org.glowroot"); private final ServerBootstrap bootstrap; private final HttpServerHandler handler; private final EventLoopGroup bossGroup; private final EventLoopGroup workerGroup; private final String bindAddress; private final File certificateDir; private volatile @Nullable SslContext sslContext; private volatile @MonotonicNonNull Channel serverChannel; private volatile @MonotonicNonNull Integer port; HttpServer(String bindAddress, boolean https, Supplier<String> contextPathSupplier, int numWorkerThreads, CommonHandler commonHandler, File certificateDir) throws Exception { InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE); ThreadFactory bossThreadFactory = new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("Glowroot-Http-Boss") .build(); ThreadFactory workerThreadFactory = new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("Glowroot-Http-Worker-%d") .build(); bossGroup = new NioEventLoopGroup(1, bossThreadFactory); workerGroup = new NioEventLoopGroup(numWorkerThreads, workerThreadFactory); final HttpServerHandler handler = new HttpServerHandler(contextPathSupplier, commonHandler); if (https) { sslContext = SslContextBuilder .forServer(new File(certificateDir, "certificate.pem"), new File(certificateDir, "private.pem")) .build(); } this.certificateDir = certificateDir; bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); SslContext sslContextLocal = sslContext; if (sslContextLocal != null) { p.addLast(sslContextLocal.newHandler(ch.alloc())); } // bumping maxInitialLineLength (first arg below) from default 4096 to 32768 // in order to handle long urls on /jvm/gauges view // bumping maxHeaderSize (second arg below) from default 8192 to 32768 for // same reason due to "Referer" header once url becomes huge // leaving maxChunkSize (third arg below) at default 8192 p.addLast(new HttpServerCodec(32768, 32768, 8192)); p.addLast(new HttpObjectAggregator(1048576)); p.addLast(new ConditionalHttpContentCompressor()); p.addLast(new ChunkedWriteHandler()); p.addLast(handler); } }); this.handler = handler; this.bindAddress = bindAddress; } void bindEventually(int port) { try { serverChannel = bootstrap.bind(new InetSocketAddress(bindAddress, port)).sync().channel(); onBindSuccess(); } catch (Exception e) { // FailedChannelFuture.sync() is using UNSAFE to re-throw checked exceptions logger.debug(e.getMessage(), e); startupLogger.error("Error binding to {}:{}, the UI will not be available" + " (will keep trying to bind): {}", bindAddress, port, e.getMessage()); ThreadFactory threadFactory = new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("Glowroot-Init-Bind") .build(); Executors.newSingleThreadExecutor(threadFactory).execute(new BindEventually(port)); } } @RequiresNonNull("serverChannel") private void onBindSuccess() { port = ((InetSocketAddress) serverChannel.localAddress()).getPort(); if (bindAddress.equals("127.0.0.1")) { startupLogger.info("UI listening on {}:{} (to access the UI from remote machines," + " change the bind address to 0.0.0.0, either in the Glowroot UI under" + " Configuration > Web or directly in the admin.json file, and then restart" + " JVM to take effect)", bindAddress, port); } else { startupLogger.info("UI listening on {}:{}", bindAddress, port); } } String getBindAddress() { return bindAddress; } @Nullable Integer getPort() { return port; } boolean getHttps() { return sslContext != null; } void changePort(int newPort) throws PortChangeFailedException { checkNotNull(serverChannel); // need to call from separate thread, since netty throws exception if I/O thread (serving // http request) calls awaitUninterruptibly(), which is called by bind() below Channel previousServerChannel = serverChannel; ChangePort changePort = new ChangePort(newPort); ThreadFactory threadFactory = new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("Glowroot-Temporary-Thread") .build(); ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory); try { // calling get() will wait until ChangePort is complete and will re-throw any exceptions // thrown by ChangePort executor.submit(changePort).get(); } catch (Exception e) { throw new PortChangeFailedException(e); } finally { executor.shutdown(); } previousServerChannel.close(); handler.closeAllButCurrent(); } void changeProtocol(boolean ssl) throws Exception { if (ssl) { sslContext = SslContextBuilder .forServer(new File(certificateDir, "certificate.pem"), new File(certificateDir, "private.pem")) .build(); } else { sslContext = null; } handler.closeAllButCurrent(); } // used by tests and by central ui void close(boolean waitForChannelClose) { logger.debug("close(): stopping http server"); if (serverChannel != null) { if (waitForChannelClose) { serverChannel.close().awaitUninterruptibly(); } else { serverChannel.close().awaitUninterruptibly(1, SECONDS); } } bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); handler.close(waitForChannelClose); logger.debug("close(): http server stopped"); } private class BindEventually implements Runnable { private final int port; private BindEventually(int port) { this.port = port; } @Override public void run() { long backoffMillis = 100; while (true) { try { serverChannel = bootstrap.bind(new InetSocketAddress(bindAddress, port)).sync() .channel(); onBindSuccess(); return; } catch (Exception e) { // FailedChannelFuture.sync() is using UNSAFE to re-throw checked exceptions logger.debug(e.getMessage(), e); try { Thread.sleep(backoffMillis); } catch (InterruptedException f) { Thread.interrupted(); return; } backoffMillis = Math.min(backoffMillis * 2, 10000); } } } } private class ChangePort implements Callable</*@Nullable*/ Void> { private final int newPort; ChangePort(int newPort) { this.newPort = newPort; } @Override public @Nullable Void call() throws InterruptedException { InetSocketAddress localAddress = new InetSocketAddress(bindAddress, newPort); serverChannel = bootstrap.bind(localAddress).sync().channel(); port = newPort; return null; } } @SuppressWarnings("serial") static class PortChangeFailedException extends Exception { private PortChangeFailedException(Exception cause) { super(cause); } } }