/* * Copyright 2016 Netflix, Inc. * * 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 io.reactivex.netty.protocol.http.internal; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.RecyclableArrayList; import io.reactivex.netty.channel.AppendTransformerEvent; import io.reactivex.netty.channel.ConnectionInputSubscriberEvent; import io.reactivex.netty.channel.ConnectionInputSubscriberReplaceEvent; import io.reactivex.netty.channel.SubscriberToChannelFutureBridge; import io.reactivex.netty.channel.WriteTransformations; import io.reactivex.netty.client.ClientConnectionToChannelBridge.ConnectionReuseEvent; import io.reactivex.netty.events.Clock; import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge.State.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Producer; import rx.Subscriber; import rx.functions.Action0; import rx.subscriptions.Subscriptions; import java.nio.channels.ClosedChannelException; import static io.netty.handler.codec.http.HttpHeaderNames.*; import static io.netty.handler.codec.http.HttpHeaderValues.*; public abstract class AbstractHttpConnectionBridge<C> extends ChannelDuplexHandler { private static final Logger logger = LoggerFactory.getLogger(AbstractHttpConnectionBridge.class); public static final AttributeKey<Boolean> CONNECTION_UPGRADED = AttributeKey.valueOf("rxnetty_http_upgraded_connection"); @SuppressWarnings("ThrowableInstanceNeverThrown") private static final IllegalStateException ONLY_ONE_CONTENT_INPUT_SUB_ALLOWED = new IllegalStateException("Only one subscriber allowed for HTTP content."); @SuppressWarnings("ThrowableInstanceNeverThrown") private static final IllegalStateException LAZY_CONTENT_INPUT_SUB = new IllegalStateException("Channel is set to auto-read but the subscription was lazy."); @SuppressWarnings("ThrowableInstanceNeverThrown") private static final IllegalStateException CONTENT_ARRIVED_WITH_NO_SUB = new IllegalStateException("HTTP Content received but no subscriber was registered."); @SuppressWarnings("ThrowableInstanceNeverThrown") private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); static { ONLY_ONE_CONTENT_INPUT_SUB_ALLOWED.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); LAZY_CONTENT_INPUT_SUB.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); CONTENT_ARRIVED_WITH_NO_SUB.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); } protected ConnectionInputSubscriber connectionInputSubscriber; private final UnsafeEmptySubscriber<C> emptyContentSubscriber; private final WriteTransformations transformations; private long headerWriteStartTimeNanos; protected AbstractHttpConnectionBridge() { emptyContentSubscriber = new UnsafeEmptySubscriber<>("Error while waiting for HTTP content."); transformations = new WriteTransformations(); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { boolean skipNextHandlers = false; if (isOutboundHeader(msg)) { /*Reset on every header write, when we support pipelining, this should be a queue.*/ headerWriteStartTimeNanos = Clock.newStartTimeNanos(); HttpMessage httpMsg = (HttpMessage) msg; if (!HttpUtil.isContentLengthSet(httpMsg) && !HttpVersion.HTTP_1_0.equals(httpMsg.protocolVersion())) { // If there is no content length we need to specify the transfer encoding as chunked as we always // send data in multiple HttpContent. // On the other hand, if someone wants to not have chunked encoding, adding content-length will work // as expected. httpMsg.headers().set(TRANSFER_ENCODING, CHUNKED); } beforeOutboundHeaderWrite(httpMsg, promise, headerWriteStartTimeNanos); } else if (msg instanceof LastHttpContent) { onOutboundLastContentWrite((LastHttpContent) msg, promise, headerWriteStartTimeNanos); } else if (transformations.acceptMessage(msg)) { RecyclableArrayList out = RecyclableArrayList.newInstance(); try { transformations.transform(msg, ctx.alloc(), out); } finally { final int sizeMinusOne = out.size() - 1; if (sizeMinusOne == 0) { ctx.write(out.get(0), promise); } else if (sizeMinusOne > 0) { ChannelPromise voidPromise = ctx.voidPromise(); boolean isVoidPromise = promise == voidPromise; for (int i = 0; i < sizeMinusOne; i ++) { ChannelPromise p; if (isVoidPromise) { p = voidPromise; } else { p = ctx.newPromise(); } ctx.write(out.get(i), p); } ctx.write(out.get(sizeMinusOne), promise); } out.recycle(); skipNextHandlers = true; } } if (!skipNextHandlers) { super.write(ctx, msg, promise); } } protected abstract void beforeOutboundHeaderWrite(HttpMessage httpMsg, ChannelPromise promise, long startTimeNanos); protected abstract void onOutboundLastContentWrite(LastHttpContent msg, ChannelPromise promise, long headerWriteStartTimeNanos); @Override public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception { Object eventToPropagateFurther = evt; Boolean connUpgradedAttr = ctx.channel().attr(CONNECTION_UPGRADED).get(); boolean connUpgraded = null != connUpgradedAttr ? connUpgradedAttr : false; if (evt instanceof ConnectionInputSubscriberEvent) { @SuppressWarnings({ "unchecked", "rawtypes" }) ConnectionInputSubscriberEvent orig = (ConnectionInputSubscriberEvent) evt; if (!connUpgraded) { /*Local copy to refer from the channel close listener. As the instance level copy can change*/ @SuppressWarnings("unchecked") final ConnectionInputSubscriber _connectionInputSubscriber = newConnectionInputSubscriber(orig, ctx.channel()); connectionInputSubscriber = _connectionInputSubscriber; final SubscriberToChannelFutureBridge l = new SubscriberToChannelFutureBridge() { @Override protected void doOnSuccess(ChannelFuture future) { onChannelClose(_connectionInputSubscriber); } @Override protected void doOnFailure(ChannelFuture future, Throwable cause) { onChannelClose(_connectionInputSubscriber); } }; l.bridge(ctx.channel().closeFuture(), _connectionInputSubscriber); @SuppressWarnings({ "unchecked", "rawtypes" }) ConnectionInputSubscriberEvent newEvent = new ConnectionInputSubscriberEvent(_connectionInputSubscriber ); eventToPropagateFurther = newEvent; } else { if (null != connectionInputSubscriber) { connectionInputSubscriber.state.stage = Stage.Upgraded; } @SuppressWarnings({ "unchecked", "rawtypes" }) ConnectionInputSubscriberReplaceEvent replaceEvt = new ConnectionInputSubscriberReplaceEvent<>(orig); eventToPropagateFurther = replaceEvt; } } else if (evt instanceof HttpContentSubscriberEvent) { newHttpContentSubscriber(evt, connectionInputSubscriber); } else if (evt instanceof AppendTransformerEvent) { transformations.appendTransformer(((AppendTransformerEvent)evt).getTransformer()); } else if (evt instanceof ConnectionReuseEvent) { transformations.resetTransformations(); } super.userEventTriggered(ctx, eventToPropagateFurther); } protected ConnectionInputSubscriber newConnectionInputSubscriber(ConnectionInputSubscriberEvent<?, ?> orig, Channel channel) { ConnectionInputSubscriber toReturn = new ConnectionInputSubscriber(orig.getSubscriber(), channel); toReturn.state.headerSub.add(Subscriptions.create(toReturn)); return toReturn; } protected final void onChannelClose(ConnectionInputSubscriber connectionInputSubscriber) { /* * If any of the subscribers(header or content) are still subscribed and the channel is closed, it is an * error. If they are unsubscribed, this will be a no-op. */ connectionInputSubscriber.onError(CLOSED_CHANNEL_EXCEPTION); } protected void onClosedBeforeReceiveComplete(Channel channel) { // No Op. Override to add behavior } protected void resetSubscriptionState(final ConnectionInputSubscriber connectionInputSubscriber) { connectionInputSubscriber.resetSubscribers(); } protected abstract boolean isInboundHeader(Object nextItem); protected abstract boolean isOutboundHeader(Object nextItem); protected abstract Object newHttpObject(Object nextItem, Channel channel); protected abstract void onContentReceived(); protected abstract void onContentReceiveComplete(long receiveStartTimeNanos); protected void onNewContentSubscriber(ConnectionInputSubscriber inputSubscriber, Subscriber<? super C> newSub) { // No Op. } protected long getHeaderWriteStartTimeNanos() { return headerWriteStartTimeNanos; } private void processNextItemInEventloop(Object nextItem, ConnectionInputSubscriber connectionInputSubscriber) { final State state = connectionInputSubscriber.state; final Channel channel = connectionInputSubscriber.channel; if (isInboundHeader(nextItem)) { state.headerReceived(); Object newHttpObject = newHttpObject(nextItem, channel); connectionInputSubscriber.nextHeader(newHttpObject); /*Why not complete the header sub? It may be listening to multiple responses (pipelining)*/ checkEagerSubscriptionIfConfigured(channel, state); final HttpObject httpObject = (HttpObject) nextItem; if (httpObject.decoderResult().isFailure()) { connectionInputSubscriber.onError(httpObject.decoderResult().cause()); channel.close();// Netty rejects all data after decode failure, so closing connection // Issue: https://github.com/netty/netty/issues/3362 } } if (nextItem instanceof HttpContent) { onContentReceived(); ByteBuf content = ((ByteBufHolder) nextItem).content(); if (nextItem instanceof LastHttpContent) { /* * Since, LastHttpContent is always received, even if the pipeline does not emit ByteBuf, if * ByteBuf with the LastHttpContent is empty, only trailing headers are emitted. Otherwise, * the content type should be a ByteBuf. */ if (content.isReadable()) { connectionInputSubscriber.nextContent(content); } else { /*Since, the content buffer, was not sent, release it*/ ReferenceCountUtil.release(content); } state.contentComplete(); connectionInputSubscriber.contentComplete(); onContentReceiveComplete(state.headerReceivedTimeNanos); } else { connectionInputSubscriber.nextContent(content); } } else if(!isInboundHeader(nextItem)){ connectionInputSubscriber.nextContent(nextItem); } } private void newHttpContentSubscriber(final Object evt, final ConnectionInputSubscriber inputSubscriber) { @SuppressWarnings("unchecked") HttpContentSubscriberEvent<C> contentSubscriberEvent = (HttpContentSubscriberEvent<C>) evt; Subscriber<? super C> newSub = contentSubscriberEvent.getSubscriber(); Throwable errorToRaise = null; if (null == inputSubscriber) { errorToRaise = new NullPointerException("Null Connection input subscriber."); } else { final State state = inputSubscriber.state; if (state.raiseErrorOnInputSubscription()) { errorToRaise = state.raiseErrorOnInputSubscription; } else if (isValidToEmit(state.contentSub)) { /*Allow only one concurrent input subscriber but allow concatenated subscribers*/ if (!newSub.isUnsubscribed()) { errorToRaise = ONLY_ONE_CONTENT_INPUT_SUB_ALLOWED; } } else if (state.stage == Stage.HeaderReceived) { inputSubscriber.setupContentSubscriber(newSub); onNewContentSubscriber(inputSubscriber, newSub); } else { errorToRaise = new IllegalStateException("Content subscription received without request start."); } } if (null != errorToRaise && isValidToEmit(newSub)) { newSub.onError(errorToRaise); } } private void checkEagerSubscriptionIfConfigured(Channel channel, final State state) { if (channel.config().isAutoRead()) { if (null == state.contentSub) { // If the channel is set to auto-read and there is no eager subscription then, we should raise errors // when a subscriber arrives. state.raiseErrorOnInputSubscription = LAZY_CONTENT_INPUT_SUB; state.contentSub = emptyContentSubscriber; } } } private static boolean isValidToEmit(Subscriber<?> subscriber) { return null != subscriber && !subscriber.isUnsubscribed(); } /** * All state for this handler. At any point we need to invoke any method outside of this handler, this state should * be stored in a local variable and used after the external call finishes. Failure to do so will cause race * conditions in us using different state before and after the method call specifically if the external call ends * up generating a user generated event and triggering {@link #userEventTriggered(ChannelHandlerContext, Object)} * which in turn changes this state. * * Issue: https://github.com/Netflix/RxNetty/issues/129 */ protected static final class State { /*Visible for testing*/enum Stage { /*Strictly in the order in which the transitions would happen*/ Created, HeaderReceived, ContentComplete, Upgraded } protected IllegalStateException raiseErrorOnInputSubscription; @SuppressWarnings("rawtypes") private Subscriber headerSub; @SuppressWarnings("rawtypes") private Subscriber contentSub; private long headerReceivedTimeNanos; private volatile Stage stage = Stage.Created; /*Visible for testing*/void headerReceived() { headerReceivedTimeNanos = Clock.newStartTimeNanos(); stage = Stage.HeaderReceived; } private void contentComplete() { stage = Stage.ContentComplete; } public boolean raiseErrorOnInputSubscription() { return null != raiseErrorOnInputSubscription; } public boolean startButNotCompleted() { return stage == Stage.HeaderReceived; } public boolean receiveStarted() { return stage.ordinal() > Stage.Created.ordinal(); } /*Visible for testing*/Subscriber<?> getHeaderSub() { return headerSub; } /*Visible for testing*/Subscriber<?> getContentSub() { return contentSub; } } protected class ConnectionInputSubscriber extends Subscriber<Object> implements Action0, Runnable { private final Channel channel; private final State state; private Producer producer; @SuppressWarnings("rawtypes") private ConnectionInputSubscriber(Subscriber subscriber, Channel channel) { state = new State(); this.channel = channel; state.headerSub = subscriber; } @Override public void onCompleted() { // This means channel input has completed if (state.startButNotCompleted()) { onError(CLOSED_CHANNEL_EXCEPTION); } else { completeAllSubs(); } } @Override public void onError(Throwable e) { // This means channel input has got an error & hence no other notifications will arrive. errorAllSubs(e); if (state.startButNotCompleted()) { onClosedBeforeReceiveComplete(channel); } } @Override public void onNext(final Object next) { if (channel.eventLoop().inEventLoop()) { processNextItemInEventloop(next, this); } else { channel.eventLoop().execute(new Runnable() { @Override public void run() { processNextItemInEventloop(next, ConnectionInputSubscriber.this); } }); } } @Override public void setProducer(Producer producer) { this.producer = producer; state.headerSub.setProducer(producer); /*Content & trailer producers are set on subscription*/ } public Channel getChannel() { return channel; } public void resetSubscribers() { completeAllSubs(); } private void completeAllSubs() { if (isValidToEmit(state.headerSub)) { state.headerSub.onCompleted(); } if (isValidToEmit(state.contentSub)) { state.contentSub.onCompleted(); } } private void errorAllSubs(Throwable throwable) { if (isValidToEmit(state.headerSub)) { state.headerSub.onError(throwable); } if (isValidToEmit(state.contentSub)) { state.contentSub.onError(throwable); } } @SuppressWarnings("unchecked") private void nextContent(final Object nextObject) { if (isValidToEmit(state.contentSub)) { state.contentSub.onNext(nextObject); } else { contentArrivedWhenSubscriberNotValid(); if (logger.isWarnEnabled()) { logger.warn("Data received on channel, but no subscriber registered. Discarding data. Message class: " + nextObject.getClass().getName() + ", channel: " + channel); } ReferenceCountUtil.release(nextObject); } } @SuppressWarnings("unchecked") private void nextHeader(final Object nextObject) { if (isValidToEmit(state.headerSub)) { state.headerSub.onNext(nextObject); } } private void setupContentSubscriber(Subscriber<? super C> newSub) { assert channel.eventLoop().inEventLoop(); state.contentSub = newSub; state.contentSub.add(Subscriptions.create(this)); state.contentSub.setProducer(producer); /*Content demand matches upstream demand*/ } public void contentComplete() { assert channel.eventLoop().inEventLoop(); if (isValidToEmit(state.contentSub)) { state.contentSub.onCompleted(); } else { contentArrivedWhenSubscriberNotValid(); } } private void contentArrivedWhenSubscriberNotValid() { if (null == state.contentSub) { /* * Cases when auto-read is off and there is lazy subscription, due to mismatched request demands on the * subscriber, it may so happen that we get content without a subscriber, in such cases, we should raise * an error. */ state.raiseErrorOnInputSubscription = CONTENT_ARRIVED_WITH_NO_SUB; } } /*Visible for testing*/State getState() { return state; } @Override public void run() { if (state.contentSub != null) { if (state.contentSub.isUnsubscribed()) { // Content sub exists and unsubscribed, so unsubscribe from input. unsubscribe(); } else if (state.headerSub.isUnsubscribed() && !state.receiveStarted()) { // Header sub unsubscribed before request started, unsubscribe from input. unsubscribe(); } } else if (state.headerSub.isUnsubscribed() && !state.receiveStarted()) { // Header sub unsubscribed before request started, unsubscribe from input. unsubscribe(); } } @Override public void call() { if (channel.eventLoop().inEventLoop()) { run(); } else { channel.eventLoop().execute(this); } } } }