All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xipki.http.server.HttpServer Maven / Gradle / Ivy

The newest version!
/*
 *
 * Copyright (c) 2013 - 2018 Lijun Liao
 *
 * 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.xipki.http.server;

import java.io.Closeable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import javax.net.ssl.SSLSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.http.servlet.HttpServlet;
import org.xipki.http.servlet.ServletURI;
import org.xipki.http.servlet.SslReverseProxyMode;
import org.xipki.util.Args;
import org.xipki.util.LogUtil;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.CharsetUtil;

/**
 * TODO.
 * @author Lijun Liao
 * @since 2.2.0
 */

public final class HttpServer implements Closeable {

  private class NettyHttpServerInitializer extends ChannelInitializer {

    public NettyHttpServerInitializer() {
    }

    @Override
    public void initChannel(SocketChannel ch) {
      ChannelPipeline pipeline = ch.pipeline();
      if (sslContext != null) {
        pipeline.addLast("ssl", sslContext.newHandler(ch.alloc()));
      }

      final int maxContentLength;
      if (maxRequestBodySize == Integer.MAX_VALUE) {
        maxContentLength = 1024 * 1024; // 1M
      } else {
        maxContentLength = 1024 + maxRequestBodySize;
      }

      pipeline
        .addLast("code", new HttpServerCodec())
        .addLast("aggregator", new HttpObjectAggregator(maxContentLength))
        .addLast(new ChunkedWriteHandler())
        .addLast("serverHandler", new NettyHttpServerHandler());
    }
  }

  private class NettyHttpServerHandler extends SimpleChannelInboundHandler {

    private NettyHttpServerHandler() {
      super(true);
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request)
            throws Exception {
      if (!request.decoderResult().isSuccess()) {
        sendError(ctx, HttpResponseStatus.BAD_REQUEST);
        return;
      }

      String rawPath = request.uri();
      if (rawPath.length() > maxUriPathSize) {
        sendError(ctx, HttpResponseStatus.REQUEST_URI_TOO_LONG);
        return;
      }

      Object[] objs = servletListener.getServlet(rawPath);
      if (objs == null) {
        sendError(ctx, HttpResponseStatus.NOT_FOUND);
        return;
      }

      ServletURI servletUri = (ServletURI) objs[0];
      HttpServlet servlet = (HttpServlet) objs[1];

      if (rawPath.length() > servlet.getMaxUriSize()) {
        sendError(ctx, HttpResponseStatus.REQUEST_URI_TOO_LONG);
        return;
      }

      if (request.content().readableBytes()
          > Math.min(maxRequestBodySize, servlet.getMaxRequestSize())) {
        sendError(ctx, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
        return;
      }

      SSLSession sslSession = null;

      if (servlet.needsTlsSessionInfo() && sslContext != null) {
        SslHandler handler = (SslHandler) ctx.channel().pipeline().get("ssl");
        if (handler != null) {
          sslSession = handler.engine().getSession();
        }
      }

      FullHttpResponse response;
      try {
        response = servlet.service(request, servletUri, sslSession, sslReverseProxyMode);
      } catch (Exception ex) {
        logException("exception raised while processing request", ex);
        sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        return;
      }

      boolean keepAlive = true;
      int status = response.status().code();
      if (status < 200 | status > 299) {
        keepAlive = false;
      }

      ChannelFuture cf = ctx.writeAndFlush(response);
      if (!keepAlive) {
        cf.addListener(ChannelFutureListener.CLOSE);
      }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
      if (ctx.channel().isActive()) {
        sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
      }
    }

    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
      ByteBuf content = Unpooled.copiedBuffer("Failure: " + status + "\r\n",
          CharsetUtil.UTF_8);
      FullHttpResponse response = new DefaultFullHttpResponse(
          HttpVersion.HTTP_1_1, status, content);
      response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");

      // Close the connection as soon as the error message is sent.
      ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private void logException(String msg, Exception ex) {
      LOG.warn("{} - {}: {}", msg, ex.getClass().getName(), ex.getMessage());
      LOG.debug(msg, ex);
      return;
    }
  }

  private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class);

  private static Boolean epollAvailable;

  private static Boolean kqueueAvailable;

  private final int port;

  private final SslContext sslContext;

  private final int numThreads;

  private int maxRequestBodySize = Integer.MAX_VALUE;

  private int maxUriPathSize = Integer.MAX_VALUE;

  private ServletListener servletListener;

  private EventLoopGroup bossGroup;

  private EventLoopGroup workerGroup;

  private SslReverseProxyMode sslReverseProxyMode = SslReverseProxyMode.NONE;

  public void setSslReverseProxyMode(SslReverseProxyMode mode) {
    this.sslReverseProxyMode = (mode == null) ? SslReverseProxyMode.NONE : mode;
  }

  public HttpServer(SslContext sslContext, int port, int numThreads) {
    this.sslContext = sslContext;
    this.port = port;
    this.numThreads = (numThreads > 0) ? numThreads : Runtime.getRuntime().availableProcessors();
  }

  public void setMaxUriPathSize(int maxUriPathSize) {
    this.maxUriPathSize = Args.min(maxUriPathSize, "maxUriPathSize", 0);
  }

  public void setMaxRequestBodySize(int maxRequestBodySize) {
    this.maxRequestBodySize = Args.min(maxRequestBodySize, "maxRequestBodySize", 0);
  }

  public void setServletListener(ServletListener servletListener) {
    this.servletListener = servletListener;
  }

  static {
    String os = System.getProperty("os.name").toLowerCase();
    ClassLoader loader = HttpServer.class.getClassLoader();
    if (os.contains("linux")) {
      try {
        Class checkClazz = clazz("io.netty.channel.epoll.Epoll", false, loader);
        Method mt = checkClazz.getMethod("isAvailable");
        Object obj = mt.invoke(null);

        if (obj instanceof Boolean) {
          epollAvailable = (Boolean) obj;
        }
      } catch (Throwable th) {
        if (th instanceof ClassNotFoundException) {
          LOG.info("epoll linux is not in classpath");
        } else {
          LOG.warn("could not use Epoll transport: {}", th.getMessage());
          LOG.debug("could not use Epoll transport", th);
        }
      }
    } else if (os.contains("mac os") || os.contains("os x")) {
      try {
        Class checkClazz = clazz("io.netty.channel.epoll.kqueue.KQueue", false, loader);
        Method mt = checkClazz.getMethod("isAvailable");
        Object obj = mt.invoke(null);
        if (obj instanceof Boolean) {
          kqueueAvailable = (Boolean) obj;
        }
      } catch (Exception ex) {
        LOG.warn("could not use KQueue transport: {}", ex.getMessage());
        LOG.debug("could not use KQueue transport", ex);
      }
    }
  }

  @SuppressWarnings("unchecked")
  public void start() {
    int numProcessors = Runtime.getRuntime().availableProcessors();
    Class channelClass = null;
    int bossGroupThreads = numProcessors == 1 ? 1  : (numProcessors + 1) / 2;

    ClassLoader loader = getClass().getClassLoader();
    if (epollAvailable != null && epollAvailable.booleanValue()) {
      try {
        channelClass = (Class)
            clazz("io.netty.channel.epoll.EpollServerSocketChannel", false, loader);

        Class clazz = clazz("io.netty.channel.epoll.EpollEventLoopGroup", true, loader);
        Constructor constructor = clazz.getConstructor(int.class);
        this.bossGroup = (EventLoopGroup) constructor.newInstance(bossGroupThreads);
        this.workerGroup = (EventLoopGroup) constructor.newInstance(numThreads);
        LOG.info("use Epoll Transport");
      } catch (Throwable th) {
        if (th instanceof ClassNotFoundException) {
          LOG.info("epoll linux is not in classpath");
        } else {
          LogUtil.warn(LOG, th, "could not use Epoll transport");
        }
        channelClass = null;
        this.bossGroup = null;
        this.workerGroup = null;
      }
    } else if (kqueueAvailable != null && kqueueAvailable.booleanValue()) {
      try {
        channelClass = (Class)
            clazz("io.netty.channel.kqueue.KQueueServerSocketChannel", false, loader);

        Class clazz = clazz("io.netty.channel.kqueue.KQueueEventLoopGroup", true, loader);
        Constructor constructor = clazz.getConstructor(int.class);
        this.bossGroup = (EventLoopGroup) constructor.newInstance(bossGroupThreads);
        this.workerGroup = (EventLoopGroup) constructor.newInstance(numThreads);
        LOG.info("Use KQueue Transport");
      } catch (Exception ex) {
        LOG.warn("could not use KQueue transport: {}", ex.getMessage());
        LOG.debug("could not use KQueue transport", ex);
        channelClass = null;
        this.bossGroup = null;
        this.workerGroup = null;
      }
    }

    if (this.bossGroup == null) {
      channelClass = NioServerSocketChannel.class;
      this.bossGroup = new NioEventLoopGroup(bossGroupThreads);
      this.workerGroup = new NioEventLoopGroup(numThreads);
    }

    ServerBootstrap bootstrap = new ServerBootstrap();
    bootstrap.group(bossGroup, workerGroup)
      .channel(channelClass)
      .handler(new LoggingHandler())
      .childHandler(new NettyHttpServerInitializer());

    bootstrap.bind(port).syncUninterruptibly();
    LOG.info("HTTP server is listening on port {}", port);
  }

  @Deprecated
  public void shutdown() {
    close();
  }

  @Override
  public void close() {
    bossGroup.shutdownGracefully();
    bossGroup = null;
    workerGroup.shutdownGracefully();
    workerGroup = null;
  }

  private static Class clazz(String clazzName, boolean initialize, ClassLoader clazzLoader)
      throws ClassNotFoundException {
    return Class.forName(clazzName, initialize, clazzLoader);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy