/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; import static com.linecorp.armeria.common.http.HttpSessionProtocols.H1; import static com.linecorp.armeria.common.http.HttpSessionProtocols.H1C; import static com.linecorp.armeria.common.http.HttpSessionProtocols.H2; import static com.linecorp.armeria.common.http.HttpSessionProtocols.H2C; import static java.util.Objects.requireNonNull; import java.util.concurrent.ScheduledFuture; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.SessionProtocolNegotiationException; import com.linecorp.armeria.client.http.HttpResponseDecoder.HttpResponseWrapper; import com.linecorp.armeria.common.ClosedSessionException; import com.linecorp.armeria.common.SessionProtocol; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.util.Exceptions; import com.linecorp.armeria.internal.InboundTrafficController; import com.linecorp.armeria.internal.http.Http1ObjectEncoder; import com.linecorp.armeria.internal.http.Http2ObjectEncoder; import com.linecorp.armeria.internal.http.HttpObjectEncoder; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.socket.ChannelInputShutdownReadComplete; import io.netty.handler.codec.http2.Http2ConnectionHandler; import io.netty.handler.codec.http2.Http2ConnectionPrefaceWrittenEvent; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.ssl.SslCloseCompletionEvent; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Promise; final class HttpSessionHandler extends ChannelDuplexHandler implements HttpSession { private static final Logger logger = LoggerFactory.getLogger(HttpSessionHandler.class); /** * 2^29 - We could have used 2^30 but this should be large enough. */ private static final int MAX_NUM_REQUESTS_SENT = 536870912; private final HttpSessionChannelFactory channelFactory; private final Channel channel; private final Promise<Channel> sessionPromise; private final ScheduledFuture<?> sessionTimeoutFuture; /** * Whether the current channel is active or not. */ private volatile boolean active; /** * The current negotiated {@link SessionProtocol}. */ private SessionProtocol protocol; private HttpResponseDecoder responseDecoder; private HttpObjectEncoder requestEncoder; /** * The number of requests sent. Disconnects when it reaches at {@link #MAX_NUM_REQUESTS_SENT}. */ private int numRequestsSent; /** * {@code true} if the protocol upgrade to HTTP/2 has failed. * If set to {@code true}, another connection attempt will follow. */ private boolean needsRetryWithH1C; HttpSessionHandler(HttpSessionChannelFactory channelFactory, Channel channel, Promise<Channel> sessionPromise, ScheduledFuture<?> sessionTimeoutFuture) { this.channelFactory = requireNonNull(channelFactory, "channelFactory"); this.channel = requireNonNull(channel, "channel"); this.sessionPromise = requireNonNull(sessionPromise, "sessionPromise"); this.sessionTimeoutFuture = requireNonNull(sessionTimeoutFuture, "sessionTimeoutFuture"); } @Override public SessionProtocol protocol() { return protocol; } @Override public InboundTrafficController inboundTrafficController() { return responseDecoder.inboundTrafficController(); } @Override public boolean hasUnfinishedResponses() { return responseDecoder.hasUnfinishedResponses(); } @Override public boolean isActive() { return active; } @Override public boolean invoke(ClientRequestContext ctx, HttpRequest req, DecodedHttpResponse res) { if (!res.isOpen()) { // The response has been closed even before its request is sent. req.abort(); return true; } final long writeTimeoutMillis = ctx.writeTimeoutMillis(); final long responseTimeoutMillis = ctx.responseTimeoutMillis(); final long maxContentLength = ctx.maxResponseLength(); final int numRequestsSent = ++this.numRequestsSent; final HttpResponseWrapper wrappedRes = responseDecoder.addResponse(numRequestsSent, req, res, ctx.logBuilder(), responseTimeoutMillis, maxContentLength); req.subscribe( new HttpRequestSubscriber(channel, requestEncoder, numRequestsSent, req, wrappedRes, ctx, writeTimeoutMillis), channel.eventLoop()); if (numRequestsSent >= MAX_NUM_REQUESTS_SENT) { responseDecoder.disconnectWhenFinished(); return false; } else { return true; } } @Override public void retryWithH1C() { needsRetryWithH1C = true; } @Override public void deactivate() { active = false; } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { active = ctx.channel().isActive(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { active = true; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof Http2Settings) { // Expected } else { try { final String typeInfo; if (msg instanceof ByteBuf) { typeInfo = msg + " HexDump: " + ByteBufUtil.hexDump((ByteBuf) msg); } else { typeInfo = String.valueOf(msg); } throw new IllegalStateException("unexpected message type: " + typeInfo); } finally { ReferenceCountUtil.release(msg); } } } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof SessionProtocol) { assert protocol == null; assert responseDecoder == null; sessionTimeoutFuture.cancel(false); // Set the current protocol and its associated WaitsHolder implementation. final SessionProtocol protocol = (SessionProtocol) evt; this.protocol = protocol; if (protocol == H1 || protocol == H1C) { requestEncoder = new Http1ObjectEncoder(false, protocol.isTls()); responseDecoder = ctx.pipeline().get(Http1ResponseDecoder.class); } else if (protocol == H2 || protocol == H2C) { final Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class); requestEncoder = new Http2ObjectEncoder(handler.encoder()); responseDecoder = ctx.pipeline().get(Http2ClientConnectionHandler.class).responseDecoder(); } else { throw new Error(); // Should never reach here. } if (!sessionPromise.trySuccess(ctx.channel())) { // Session creation has been failed already; close the connection. ctx.close(); } return; } if (evt instanceof SessionProtocolNegotiationException) { sessionTimeoutFuture.cancel(false); sessionPromise.tryFailure((SessionProtocolNegotiationException) evt); ctx.close(); return; } if (evt instanceof Http2ConnectionPrefaceWrittenEvent || evt instanceof SslCloseCompletionEvent || evt instanceof ChannelInputShutdownReadComplete) { // Expected events return; } logger.warn("{} Unexpected user event: {}", ctx.channel(), evt); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { active = false; // Protocol upgrade has failed, but needs to retry. if (needsRetryWithH1C) { assert responseDecoder == null || !responseDecoder.hasUnfinishedResponses(); sessionTimeoutFuture.cancel(false); channelFactory.connect(ctx.channel().remoteAddress(), H1C, sessionPromise); } else { // Fail all pending responses. failUnfinishedResponses(ClosedSessionException.get()); // Cancel the timeout and reject the sessionPromise just in case the connection has been closed // even before the session protocol negotiation is done. sessionTimeoutFuture.cancel(false); sessionPromise.tryFailure(ClosedSessionException.get()); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { Exceptions.logIfUnexpected(logger, ctx.channel(), protocol(), cause); if (ctx.channel().isActive()) { ctx.close(); } } private void failUnfinishedResponses(Throwable e) { final HttpResponseDecoder responseDecoder = this.responseDecoder; if (responseDecoder == null) { return; } responseDecoder.failUnfinishedResponses(e); } }