/** * Copyright 2007-2015, Kaazing Corporation. All rights reserved. * * 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.kaazing.k3po.driver.internal.netty.bootstrap.http; import static java.util.Objects.requireNonNull; import static org.jboss.netty.buffer.ChannelBuffers.EMPTY_BUFFER; import static org.jboss.netty.buffer.ChannelBuffers.copiedBuffer; import static org.jboss.netty.channel.Channels.fireChannelClosed; import static org.jboss.netty.channel.Channels.fireChannelDisconnected; import static org.jboss.netty.channel.Channels.fireChannelUnbound; import static org.jboss.netty.channel.Channels.future; import static org.jboss.netty.handler.codec.http.HttpHeaders.getContentLength; import static org.jboss.netty.handler.codec.http.HttpHeaders.isContentLengthSet; import static org.jboss.netty.handler.codec.http.HttpHeaders.isTransferEncodingChunked; import static org.jboss.netty.handler.codec.http.HttpHeaders.setContentLength; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS; import static org.kaazing.k3po.driver.internal.channel.Channels.chainFutures; import static org.kaazing.k3po.driver.internal.channel.Channels.chainWriteCompletes; import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChildChannel.HttpWriteState.CONTENT_BUFFERED; import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChildChannel.HttpWriteState.CONTENT_CHUNKED; import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChildChannel.HttpWriteState.CONTENT_CLOSE; import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChildChannel.HttpWriteState.CONTENT_COMPLETE; import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChildChannel.HttpWriteState.UPGRADED; import java.util.Map; import java.util.Map.Entry; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.DefaultHttpChunk; import org.jboss.netty.handler.codec.http.DefaultHttpChunkTrailer; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpChunk; import org.jboss.netty.handler.codec.http.HttpChunkTrailer; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpHeaders.Names; import org.jboss.netty.handler.codec.http.HttpHeaders.Values; import org.jboss.netty.handler.codec.http.HttpRequestDecoder; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseEncoder; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.HttpVersion; import org.kaazing.k3po.driver.internal.netty.bootstrap.channel.AbstractChannelSink; import org.kaazing.k3po.driver.internal.netty.channel.AbortEvent; import org.kaazing.k3po.driver.internal.netty.channel.FlushEvent; import org.kaazing.k3po.driver.internal.netty.channel.ShutdownOutputEvent; public class HttpChildChannelSink extends AbstractChannelSink { private final Channel transport; private HttpResponse httpBufferedResponse; public HttpChildChannelSink(Channel transport) { this.transport = requireNonNull(transport); } @Override public ChannelFuture execute(ChannelPipeline httpPipeline, Runnable task) { ChannelPipeline pipeline = transport.getPipeline(); ChannelFuture future = pipeline.execute(task); Channel httpChannel = pipeline.getChannel(); ChannelFuture httpFuture = future(httpChannel); chainFutures(future, httpFuture); return httpFuture; } @Override protected void setInterestOpsRequested(ChannelPipeline pipeline, ChannelStateEvent evt) throws Exception { } @Override protected void writeRequested(ChannelPipeline httpPipeline, MessageEvent e) throws Exception { HttpChildChannel httpChildChannel = (HttpChildChannel) httpPipeline.getChannel(); HttpChannelConfig httpChildConfig = httpChildChannel.getConfig(); ChannelFuture httpFuture = e.getFuture(); ChannelBuffer httpContent = (ChannelBuffer) e.getMessage(); int httpReadableBytes = httpContent.readableBytes(); switch (httpChildChannel.writeState()) { case RESPONSE: HttpVersion version = httpChildConfig.getVersion(); HttpResponseStatus status = httpChildConfig.getStatus(); HttpHeaders headers = httpChildConfig.getWriteHeaders(); HttpResponse httpResponse = new DefaultHttpResponse(version, status); if (headers != null) { httpResponse.headers().add(headers); } if (httpResponse.getStatus().getCode() == SWITCHING_PROTOCOLS.getCode()) { httpResponse.setContent(EMPTY_BUFFER); ChannelPipeline pipeline = transport.getPipeline(); pipeline.remove(HttpRequestDecoder.class); transport.write(httpResponse); pipeline.remove(HttpResponseEncoder.class); ChannelFuture future = transport.write(httpContent); httpChildChannel.writeState(UPGRADED); chainWriteCompletes(future, httpFuture, httpReadableBytes); } else if (isContentLengthSet(httpResponse) && httpReadableBytes == getContentLength(httpResponse)) { httpResponse.setContent(httpContent); ChannelFuture future = transport.write(httpResponse); httpChildChannel.writeState(CONTENT_COMPLETE); chainWriteCompletes(future, httpFuture, httpReadableBytes); } else if (isTransferEncodingChunked(httpResponse)) { httpResponse.setChunked(true); transport.write(httpResponse); httpChildChannel.writeState(CONTENT_CHUNKED); HttpChunk httpChunk = new DefaultHttpChunk(httpContent); ChannelFuture future = transport.write(httpChunk); chainWriteCompletes(future, httpFuture, httpReadableBytes); } else if (httpResponse.headers().getAll(Names.CONNECTION).contains(Values.CLOSE)) { httpResponse.setContent(httpContent); ChannelFuture future = transport.write(httpResponse); httpChildChannel.writeState(CONTENT_CLOSE); chainWriteCompletes(future, httpFuture, httpReadableBytes); } else if (httpChildConfig.getMaximumBufferedContentLength() >= httpReadableBytes) { // automatically calculate content-length httpResponse.setContent(httpContent); httpBufferedResponse = httpResponse; httpChildChannel.writeState(CONTENT_BUFFERED); httpFuture.setSuccess(); } else { throw new IllegalStateException("Missing Content-Length, Transfer-Encoding: chunked, or Connection: close"); } break; case CONTENT_BUFFERED: ChannelBuffer httpBufferedContent = httpBufferedResponse.getContent(); int httpBufferedBytes = httpBufferedContent.readableBytes(); if (httpChildConfig.getMaximumBufferedContentLength() >= httpBufferedBytes + httpReadableBytes) { httpBufferedResponse.setContent(copiedBuffer(httpBufferedContent, httpContent)); httpFuture.setSuccess(); } else { throw new IllegalStateException("Exceeded maximum buffered content to calculate content length"); } break; case CONTENT_CHUNKED: case CONTENT_CLOSE: { HttpChunk httpChunk = new DefaultHttpChunk(httpContent); ChannelFuture future = transport.write(httpChunk); chainWriteCompletes(future, httpFuture, httpReadableBytes); break; } case UPGRADED: { ChannelFuture future = transport.write(httpContent); chainWriteCompletes(future, httpFuture, httpReadableBytes); break; } case CONTENT_COMPLETE: throw new IllegalStateException(); } } @Override protected void flushRequested(ChannelPipeline pipeline, FlushEvent evt) throws Exception { HttpChildChannel httpChildChannel = (HttpChildChannel) pipeline.getChannel(); ChannelFuture httpFuture = evt.getFuture(); flushRequested(httpChildChannel, httpFuture); } @Override protected void abortRequested(ChannelPipeline pipeline, final AbortEvent evt) throws Exception { transport.close().addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { evt.getFuture().setSuccess(); } }); } @Override protected void shutdownOutputRequested(ChannelPipeline pipeline, ShutdownOutputEvent evt) throws Exception { HttpChildChannel httpChildChannel = (HttpChildChannel) pipeline.getChannel(); ChannelFuture httpFuture = evt.getFuture(); // TODO: shutdown response output is identical to close semantics (if request fully read already) closeRequested(httpChildChannel, httpFuture); } @Override protected void closeRequested(ChannelPipeline pipeline, ChannelStateEvent evt) throws Exception { HttpChildChannel httpChildChannel = (HttpChildChannel) pipeline.getChannel(); ChannelFuture httpFuture = evt.getFuture(); closeRequested(httpChildChannel, httpFuture); } private void closeRequested(final HttpChildChannel httpChildChannel, ChannelFuture httpFuture) { if (!httpChildChannel.isOpen()) { httpFuture.setSuccess(); return; } ChannelFuture httpCloseFuture = httpChildChannel.getCloseFuture(); if (httpFuture != httpCloseFuture) { chainFutures(httpCloseFuture, httpFuture); } ChannelFuture httpFlushed = future(httpChildChannel); flushRequested(httpChildChannel, httpFlushed); switch (httpChildChannel.writeState()) { case UPGRADED: case CONTENT_CLOSE: // setClosed() chained asynchronously after transport.close() completes transport.close(); break; case CONTENT_CHUNKED: HttpChunkTrailer trailingChunk = new DefaultHttpChunkTrailer(); trailingChunk.trailingHeaders().add(httpChildChannel.getConfig().getWriteTrailers()); ChannelFuture future = transport.write(trailingChunk); httpChildChannel.writeState(CONTENT_COMPLETE); future.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (httpChildChannel.setWriteClosed()) { fireChannelDisconnected(httpChildChannel); fireChannelUnbound(httpChildChannel); fireChannelClosed(httpChildChannel); } } }); break; case CONTENT_COMPLETE: if (httpChildChannel.setWriteClosed()) { fireChannelDisconnected(httpChildChannel); fireChannelUnbound(httpChildChannel); fireChannelClosed(httpChildChannel); } break; default: throw new IllegalStateException("Unexpected state after flushRequested: " + httpChildChannel.writeState()); } } private void flushRequested(HttpChildChannel httpChildChannel, ChannelFuture httpFuture) { switch (httpChildChannel.writeState()) { case RESPONSE: { HttpChannelConfig httpChildConfig = httpChildChannel.getConfig(); HttpVersion version = httpChildConfig.getVersion(); HttpResponseStatus status = httpChildConfig.getStatus(); HttpHeaders headers = httpChildConfig.getWriteHeaders(); HttpResponse httpResponse = new DefaultHttpResponse(version, status); if (headers != null) { httpResponse.headers().add(headers); } HttpResponseStatus httpStatus = httpResponse.getStatus(); int httpStatusCode = (httpStatus != null) ? httpStatus.getCode() : 0; if (httpStatusCode == SWITCHING_PROTOCOLS.getCode()) { ChannelFuture future = transport.write(httpResponse); httpChildChannel.writeState(UPGRADED); ChannelPipeline pipeline = transport.getPipeline(); pipeline.remove(HttpRequestDecoder.class); pipeline.remove(HttpResponseEncoder.class); chainFutures(future, httpFuture); } else if (isTransferEncodingChunked(httpResponse)) { httpResponse.setChunked(true); ChannelFuture future = transport.write(httpResponse); httpChildChannel.writeState(CONTENT_CHUNKED); chainFutures(future, httpFuture); } else if (httpResponse.headers().getAll(Names.CONNECTION).contains(Values.CLOSE)) { ChannelFuture future = transport.write(httpResponse); httpChildChannel.writeState(CONTENT_CLOSE); chainFutures(future, httpFuture); } else { // see RFC-7320 section 3.3 regarding content-length if (httpStatusCode >= 200 && httpChildConfig.getMaximumBufferedContentLength() > 0) { switch (httpStatusCode) { case 204: // NO_CONTENT case 205: // RESET_CONTENT case 304: // NOT_MODIFIED break; default: setContentLength(httpResponse, 0); break; } } ChannelFuture future = transport.write(httpResponse); httpChildChannel.writeState(CONTENT_COMPLETE); chainFutures(future, httpFuture); } break; } case CONTENT_BUFFERED: { HttpResponse httpBufferedResponse = this.httpBufferedResponse; this.httpBufferedResponse = null; ChannelBuffer httpBufferedContent = httpBufferedResponse.getContent(); int httpReadableBytes = httpBufferedContent.readableBytes(); setContentLength(httpBufferedResponse, httpReadableBytes); ChannelFuture future = transport.write(httpBufferedResponse); httpChildChannel.writeState(CONTENT_COMPLETE); chainWriteCompletes(future, httpFuture, httpReadableBytes); break; } case CONTENT_CHUNKED: case CONTENT_CLOSE: case CONTENT_COMPLETE: case UPGRADED: httpFuture.setSuccess(); break; default: break; } } }