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

io.jooby.internal.netty.NettyHandler Maven / Gradle / Ivy

The newest version!
/*
 * Jooby https://jooby.io
 * Apache License Version 2.0 https://jooby.io/LICENSE.txt
 * Copyright 2014 Edgar Espina
 */
package io.jooby.internal.netty;

import static io.jooby.internal.netty.SlowPathChecks.*;
import static io.netty.handler.codec.http.HttpUtil.isTransferEncodingChunked;

import java.nio.charset.StandardCharsets;

import org.slf4j.Logger;

import io.jooby.*;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.HttpPostStandardRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AsciiString;

public class NettyHandler extends ChannelInboundHandlerAdapter {
  private static final AsciiString server = AsciiString.cached("N");
  private final NettyDateService serverDate;
  private final Jooby app;
  private final Router router;
  private final int bufferSize;
  private final boolean defaultHeaders;
  private final HttpDataFactory factory;
  private final long maxRequestSize;
  private long contentLength;
  private long chunkSize;
  private boolean http2;
  private NettyContext context;

  public NettyHandler(
      NettyDateService dateService,
      Jooby app,
      long maxRequestSize,
      int bufferSize,
      HttpDataFactory factory,
      boolean defaultHeaders,
      boolean http2) {
    this.serverDate = dateService;
    this.app = app;
    this.router = app.getRouter();
    this.maxRequestSize = maxRequestSize;
    this.factory = factory;
    this.bufferSize = bufferSize;
    this.defaultHeaders = defaultHeaders;
    this.http2 = http2;
  }

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) {
    if (isHttpRequest(msg)) {
      var req = (HttpRequest) msg;

      context = new NettyContext(ctx, req, app, pathOnly(req.uri()), bufferSize, http2);

      if (defaultHeaders) {
        context.setHeaders.set(HttpHeaderNames.DATE, serverDate.date());
        context.setHeaders.set(HttpHeaderNames.SERVER, server);
      }
      context.setHeaders.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);

      if (req.method().equals(HttpMethod.GET)) {
        router.match(context).execute(context);
      } else {
        // possibly body:
        contentLength = contentLength(req);
        if (contentLength > 0 || isTransferEncodingChunked(req)) {
          context.decoder = newDecoder(req, factory);
        } else {
          // no body, move on
          router.match(context).execute(context);
        }
      }
    } else if (isLastHttpContent(msg)) {
      var chunk = (HttpContent) msg;
      try {
        // when decoder == null, chunk is always a LastHttpContent.EMPTY, ignore it
        if (context.decoder != null) {
          offer(context, chunk);
          Router.Match route = router.match(context);
          resetDecoderState(context, !route.matches());
          route.execute(context);
        }
      } finally {
        release(chunk);
      }
    } else if (isHttpContent(msg)) {
      var chunk = (HttpContent) msg;
      try {
        // when decoder == null, chunk is always a LastHttpContent.EMPTY, ignore it
        if (context.decoder != null) {
          chunkSize += chunk.content().readableBytes();
          if (chunkSize > maxRequestSize) {
            resetDecoderState(context, true);
            router.match(context).execute(context, Route.REQUEST_ENTITY_TOO_LARGE);
            return;
          }
          offer(context, chunk);
        }
      } finally {
        // must be released
        release(chunk);
      }
    } else if (isWebSocketFrame(msg)) {
      if (context.webSocket != null) {
        context.webSocket.handleFrame((WebSocketFrame) msg);
      }
    }
  }

  private void release(HttpContent ref) {
    if (ref.refCnt() > 0) {
      ref.release();
    }
  }

  @Override
  public void channelReadComplete(ChannelHandlerContext ctx) {
    if (context != null) {
      context.flush();
    }
  }

  @Override
  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
    if (evt instanceof IdleStateEvent) {
      NettyWebSocket ws = ctx.channel().attr(NettyWebSocket.WS).getAndSet(null);
      if (ws != null) {
        ws.close(WebSocketCloseStatus.GOING_AWAY);
      }
    }
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    try {
      Logger log = app.getLog();
      if (Server.connectionLost(cause)) {
        if (log.isDebugEnabled()) {
          if (context == null) {
            log.debug("execution resulted in connection lost", cause);
          } else {
            log.debug("{} {}", context.getMethod(), context.getRequestPath(), cause);
          }
        }
      } else {
        if (context == null) {
          log.error("execution resulted in exception", cause);
        } else {
          if (app.isStopped()) {
            log.debug("execution resulted in exception while application was shutting down", cause);
          } else {
            context.sendError(cause);
          }
        }
      }
    } finally {
      ctx.close();
    }
  }

  private void offer(NettyContext context, HttpContent chunk) {
    try {
      context.decoder.offer(chunk);
    } catch (HttpPostRequestDecoder.ErrorDataDecoderException
        | HttpPostRequestDecoder.TooLongFormFieldException
        | HttpPostRequestDecoder.TooManyFormFieldsException x) {
      resetDecoderState(context, true);
      context.sendError(x, StatusCode.BAD_REQUEST);
    }
  }

  private void resetDecoderState(NettyContext context, boolean destroy) {
    chunkSize = 0;
    contentLength = -1;
    if (destroy && context.decoder != null) {
      var decoder = context.decoder;
      context.decoder = null;
      decoder.destroy();
    }
  }

  private static InterfaceHttpPostRequestDecoder newDecoder(
      HttpRequest request, HttpDataFactory factory) {
    String contentType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
    if (contentType != null) {
      String lowerContentType = contentType.toLowerCase();
      if (lowerContentType.startsWith(MediaType.MULTIPART_FORMDATA)) {
        return new HttpPostMultipartRequestDecoder(factory, request, StandardCharsets.UTF_8);
      } else if (lowerContentType.startsWith(MediaType.FORM_URLENCODED)) {
        return new HttpPostStandardRequestDecoder(factory, request, StandardCharsets.UTF_8);
      }
    }
    return new HttpRawPostRequestDecoder(factory, request);
  }

  static String pathOnly(String uri) {
    int len = uri.indexOf('?');
    return len > 0 ? uri.substring(0, len) : uri;
  }

  private static long contentLength(HttpRequest req) {
    String value = req.headers().get(HttpHeaderNames.CONTENT_LENGTH);
    if (value == null) {
      return -1;
    }
    try {
      return Long.parseLong(value);
    } catch (NumberFormatException x) {
      return -1;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy