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

ratpack.http.client.internal.RequestActionSupport Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc-1
Show newest version
/*
 * Copyright 2016 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 ratpack.http.client.internal;

import com.google.common.net.HostAndPort;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.pool.ChannelPool;
import io.netty.handler.codec.PrematureChannelClosureException;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.ReadTimeoutHandler;
import ratpack.exec.Downstream;
import ratpack.exec.Execution;
import ratpack.exec.Upstream;
import ratpack.func.Action;
import ratpack.func.Factory;
import ratpack.func.Function;
import ratpack.http.Headers;
import ratpack.http.Status;
import ratpack.http.client.ReceivedResponse;
import ratpack.http.client.RequestSpec;
import ratpack.http.internal.*;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import static ratpack.util.Exceptions.uncheck;

abstract class RequestActionSupport implements Upstream {

  private static final Pattern ABSOLUTE_PATTERN = Pattern.compile("^https?://.*");
  private static final String SSL_HANDLER_NAME = "ssl";
  private static final String CLIENT_CODEC_HANDLER_NAME = "clientCodec";
  private static final String READ_TIMEOUT_HANDLER_NAME = "readTimeout";
  private static final String REDIRECT_HANDLER_NAME = "redirect";
  private static final String DECOMPRESS_HANDLER_NAME = "decompressor";

  protected final HttpClientInternal client;
  protected final RequestConfig requestConfig;
  protected final Execution execution;

  private final HttpChannelKey channelKey;
  private ChannelPool channelPool;
  private final int redirectCount;
  private final Action requestConfigurer;

  private boolean fired;
  private boolean disposed;

  RequestActionSupport(URI uri, HttpClientInternal client, int redirectCount, Execution execution, Action requestConfigurer) {
    this.requestConfigurer = requestConfigurer;
    this.requestConfig = uncheck(() -> RequestConfig.of(uri, client, requestConfigurer));
    this.client = client;
    this.execution = execution;
    this.redirectCount = redirectCount;
    this.channelKey = new HttpChannelKey(requestConfig.uri, requestConfig.connectTimeout, execution);
    this.channelPool = client.getChannelPoolMap().get(channelKey);
  }

  protected abstract void addResponseHandlers(ChannelPipeline p, Downstream downstream);

  @Override
  public void connect(final Downstream downstream) throws Exception {
    channelPool.acquire().addListener(f1 -> {
      if (!f1.isSuccess()) {
        error(downstream, f1.cause());
      } else {
        Channel channel = (Channel) f1.getNow();
        channel.config().setAutoRead(true);
        addCommonResponseHandlers(channel.pipeline(), downstream);

        String fullPath = getFullPath(requestConfig.uri);
        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, requestConfig.method.getNettyMethod(), fullPath, requestConfig.body);
        if (requestConfig.headers.get(HttpHeaderConstants.HOST) == null) {
          HostAndPort hostAndPort = HostAndPort.fromParts(channelKey.host, channelKey.port);
          requestConfig.headers.set(HttpHeaderConstants.HOST, hostAndPort.toString());
        }
        if (client.getPoolSize() == 0) {
          requestConfig.headers.set(HttpHeaderConstants.CONNECTION, HttpHeaderValues.CLOSE);
        }
        int contentLength = request.content().readableBytes();
        if (contentLength > 0) {
          requestConfig.headers.set(HttpHeaderConstants.CONTENT_LENGTH, Integer.toString(contentLength));
        }

        HttpHeaders requestHeaders = request.headers();
        requestHeaders.set(requestConfig.headers.getNettyHeaders());

        ChannelFuture writeFuture = channel.writeAndFlush(request);
        writeFuture.addListener(f2 -> {
          if (!writeFuture.isSuccess()) {
            error(downstream, writeFuture.cause());
          }
        });
      }
    });
  }

  protected void forceDispose(ChannelPipeline channelPipeline) {
    dispose(channelPipeline, true);
  }

  protected void dispose(ChannelPipeline channelPipeline, HttpResponse response) {
    dispose(channelPipeline, !HttpUtil.isKeepAlive(response));
  }

  protected final void dispose(ChannelPipeline channelPipeline, boolean forceClose) {
    if (!disposed) {
      disposed = true;
      doDispose(channelPipeline, forceClose);
    }
  }

  protected void doDispose(ChannelPipeline channelPipeline, boolean forceClose) {
    channelPipeline.remove(READ_TIMEOUT_HANDLER_NAME);
    channelPipeline.remove(REDIRECT_HANDLER_NAME);
    if (channelPipeline.get(DECOMPRESS_HANDLER_NAME) != null) {
      channelPipeline.remove(DECOMPRESS_HANDLER_NAME);
    }

    if (forceClose) {
      channelPipeline.channel().close();
    }

    channelPool.release(channelPipeline.channel());
  }

  private void addCommonResponseHandlers(ChannelPipeline p, Downstream downstream) throws Exception {
    if (channelKey.ssl) {
      //this is added once because netty is not able to properly replace this handler on
      //pooled channels from request to request. Because a pool is unique to a uri,
      //doing this works, as subsequent requests would be passing in the same certs.
      addHandlerOnce(p, SSL_HANDLER_NAME, () -> {
        SSLEngine sslEngine;
        if (requestConfig.sslContext != null) {
          sslEngine = requestConfig.sslContext.createSSLEngine();
        } else {
          sslEngine = SSLContext.getDefault().createSSLEngine();
        }
        sslEngine.setUseClientMode(true);
        return new SslHandler(sslEngine);
      });
    }

    //this is added once because it is the same across all requests.
    addHandlerOnce(p, CLIENT_CODEC_HANDLER_NAME, HttpClientCodec::new);

    p.addLast(READ_TIMEOUT_HANDLER_NAME, new ReadTimeoutHandler(requestConfig.readTimeout.toNanos(), TimeUnit.NANOSECONDS));

    p.addLast(REDIRECT_HANDLER_NAME, new SimpleChannelInboundHandler(false) {
      boolean readComplete;
      boolean redirected;

      @Override
      public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        if (!ctx.isRemoved() && !readComplete) {
          error(downstream, new PrematureChannelClosureException("Server " + requestConfig.uri + " closed the connection prematurely"));
        }
        super.channelInactive(ctx);
      }

      @Override
      public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        if (cause instanceof ReadTimeoutException) {
          if (ctx.isRemoved()) {
            forceDispose(ctx.pipeline());
          } else {
            error(downstream, cause);
          }
        } else {
          super.exceptionCaught(ctx, cause);
        }
      }

      @Override
      protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        if (msg instanceof HttpResponse) {
          readComplete = true;
          final HttpResponse response = (HttpResponse) msg;
          int maxRedirects = requestConfig.maxRedirects;
          int status = response.status().code();
          String locationValue = response.headers().getAsString(HttpHeaderConstants.LOCATION);

          Action redirectConfigurer = RequestActionSupport.this.requestConfigurer;
          if (isRedirect(status) && redirectCount < maxRedirects && locationValue != null) {
            final Function> onRedirect = requestConfig.onRedirect;
            if (onRedirect != null) {
              final Action onRedirectResult = onRedirect.apply(toReceivedResponse(response));
              if (onRedirectResult == null) {
                redirectConfigurer = null;
              } else {
                redirectConfigurer = redirectConfigurer.append(onRedirectResult);
              }
            }

            if (redirectConfigurer != null) {
              Action redirectRequestConfig = s -> {
                if (status == 301 || status == 302) {
                  s.get();
                }
              };
              redirectRequestConfig = redirectRequestConfig.append(redirectConfigurer);

              URI locationUrl;
              if (ABSOLUTE_PATTERN.matcher(locationValue).matches()) {
                locationUrl = new URI(locationValue);
              } else {
                locationUrl = new URI(channelKey.ssl ? "https" : "http", null, channelKey.host, channelKey.port, locationValue, null, null);
              }

              onRedirect(locationUrl, redirectCount + 1, redirectRequestConfig).connect(downstream);

              redirected = true;
              dispose(ctx.pipeline(), response);
            }
          }
        }

        if (!redirected) {
          ctx.fireChannelRead(msg);
        }
      }
    });

    if (requestConfig.decompressResponse) {
      p.addLast(DECOMPRESS_HANDLER_NAME, new HttpContentDecompressor());
    }

    addResponseHandlers(p, downstream);
  }

  protected abstract Upstream onRedirect(URI locationUrl, int redirectCount, Action redirectRequestConfig);

  protected void success(Downstream downstream, T value) {
    if (!fired) {
      fired = true;
      downstream.success(value);
    }
  }

  protected void error(Downstream downstream, Throwable error) {
    if (!fired) {
      fired = true;
      downstream.error(error);
    }
  }

  private ReceivedResponse toReceivedResponse(HttpResponse msg) {
    return toReceivedResponse(msg, Unpooled.EMPTY_BUFFER);
  }

  private void addHandlerOnce(ChannelPipeline p, String name, Factory channelHandler) throws Exception {
    if (p.get(name) == null) {
      p.addLast(name, channelHandler.create());
    }
  }

  protected ReceivedResponse toReceivedResponse(HttpResponse msg, ByteBuf responseBuffer) {
    final Headers headers = new NettyHeadersBackedHeaders(msg.headers());
    String contentType = headers.get(HttpHeaderConstants.CONTENT_TYPE);
    final ByteBufBackedTypedData typedData = new ByteBufBackedTypedData(responseBuffer, DefaultMediaType.get(contentType));
    final Status status = new DefaultStatus(msg.status());
    return new DefaultReceivedResponse(status, headers, typedData);
  }

  private static boolean isRedirect(int code) {
    return code == 301 || code == 302 || code == 303 || code == 307;
  }

  private static String getFullPath(URI uri) {
    String path = uri.getRawPath();
    String query = uri.getRawQuery();
    String fragment = uri.getRawFragment();

    if (query == null && fragment == null) {
      return path;
    } else {
      StringBuilder sb = new StringBuilder(path);
      if (query != null) {
        sb.append("?").append(query);
      }
      if (fragment != null) {
        sb.append("#").append(fragment);
      }
      return sb.toString();
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy