/**
* 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.lang.String.format;
import static org.jboss.netty.buffer.ChannelBuffers.copiedBuffer;
import static org.jboss.netty.channel.Channels.fireChannelBound;
import static org.jboss.netty.channel.Channels.fireChannelClosed;
import static org.jboss.netty.channel.Channels.fireChannelConnected;
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.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.HttpClientChannel.HttpState.CONTENT_BUFFERED;
import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpClientChannel.HttpState.CONTENT_CHUNKED;
import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpClientChannel.HttpState.CONTENT_COMPLETE;
import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpClientChannel.HttpState.CONTENT_STREAMED;
import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpClientChannel.HttpState.UPGRADEABLE;
import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpRequestForm.ABSOLUTE_FORM;
import static org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpRequestForm.ORIGIN_FORM;
import java.net.URI;
import java.net.URISyntaxException;
import org.jboss.netty.bootstrap.ClientBootstrap;
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.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
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.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.kaazing.k3po.driver.internal.behavior.handler.codec.http.QueryStringEncoder;
import org.kaazing.k3po.driver.internal.netty.bootstrap.BootstrapFactory;
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.ChannelAddress;
import org.kaazing.k3po.driver.internal.netty.channel.FlushEvent;
import org.kaazing.k3po.driver.internal.netty.channel.ShutdownOutputEvent;
public class HttpClientChannelSink extends AbstractChannelSink {
private final ChannelPipelineFactory pipelineFactory;
private final BootstrapFactory bootstrapFactory;
private Channel transport;
private HttpRequest httpBufferedRequest;
public HttpClientChannelSink(BootstrapFactory bootstrapFactory, ChannelPipelineFactory pipelineFactory) {
this.bootstrapFactory = bootstrapFactory;
this.pipelineFactory = pipelineFactory;
}
@Override
public ChannelFuture execute(ChannelPipeline httpPipeline, Runnable task) {
if (transport != null) {
ChannelPipeline pipeline = transport.getPipeline();
ChannelFuture future = pipeline.execute(task);
Channel httpChannel = pipeline.getChannel();
ChannelFuture httpFuture = future(httpChannel);
chainFutures(future, httpFuture);
return httpFuture;
}
return super.execute(httpPipeline, task);
}
@Override
protected void setInterestOpsRequested(ChannelPipeline pipeline, ChannelStateEvent evt) throws Exception {
ChannelFuture httpFuture = evt.getFuture();
HttpClientChannel httpClientChannel = (HttpClientChannel) evt.getChannel();
httpClientChannel.setInterestOpsNow((int) evt.getValue());
httpFuture.setSuccess();
}
@Override
protected void bindRequested(ChannelPipeline pipeline, ChannelStateEvent evt) throws Exception {
ChannelFuture httpBindFuture = evt.getFuture();
HttpClientChannel httpConnectChannel = (HttpClientChannel) evt.getChannel();
ChannelAddress httpLocalAddress = (ChannelAddress) evt.getValue();
httpConnectChannel.setLocalAddress(httpLocalAddress);
httpConnectChannel.setBound();
fireChannelBound(httpConnectChannel, httpLocalAddress);
httpBindFuture.setSuccess();
}
@Override
protected void connectRequested(ChannelPipeline pipeline, ChannelStateEvent evt) throws Exception {
final HttpClientChannel httpConnectChannel = (HttpClientChannel) evt.getChannel();
final ChannelFuture httpConnectFuture = evt.getFuture();
final ChannelAddress httpRemoteAddress = (ChannelAddress) evt.getValue();
ChannelAddress address = httpRemoteAddress.getTransport();
String schemeName = address.getLocation().getScheme();
String httpSchemeName = httpRemoteAddress.getLocation().getScheme();
ClientBootstrap bootstrap = bootstrapFactory.newClientBootstrap(schemeName);
bootstrap.setPipelineFactory(pipelineFactory);
bootstrap.setOption(format("%s.nextProtocol", schemeName), httpSchemeName);
// TODO: reuse connections with keep-alive
ChannelFuture connectFuture = bootstrap.connect(address);
connectFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture connectFuture) throws Exception {
if (connectFuture.isSuccess()) {
transport = connectFuture.getChannel();
ChannelPipeline pipeline = transport.getPipeline();
ChannelHandlerContext ctx = pipeline.getContext(HttpClientChannelSource.class);
HttpClientChannelSource channelSource = (HttpClientChannelSource) ctx.getHandler();
if (!httpConnectChannel.isBound()) {
ChannelAddress httpLocalAddress = httpRemoteAddress;
httpConnectChannel.setLocalAddress(httpLocalAddress);
httpConnectChannel.setBound();
fireChannelBound(httpConnectChannel, httpLocalAddress);
}
channelSource.setHttpChannel(httpConnectChannel);
httpConnectChannel.setRemoteAddress(httpRemoteAddress);
httpConnectChannel.setConnected();
httpConnectFuture.setSuccess();
fireChannelConnected(httpConnectChannel, httpRemoteAddress);
} else {
httpConnectFuture.setFailure(connectFuture.getCause());
}
}
});
}
@Override
protected void writeRequested(ChannelPipeline pipeline, MessageEvent e) throws Exception {
HttpClientChannel httpClientChannel = (HttpClientChannel) pipeline.getChannel();
HttpChannelConfig httpClientConfig = httpClientChannel.getConfig();
ChannelFuture httpFuture = e.getFuture();
ChannelBuffer httpContent = (ChannelBuffer) e.getMessage();
int httpReadableBytes = httpContent.readableBytes();
switch (httpClientChannel.state()) {
case REQUEST:
HttpVersion version = httpClientConfig.getVersion();
HttpMethod method = httpClientConfig.getMethod();
HttpHeaders headers = httpClientConfig.getWriteHeaders();
String targetURI = getTargetURI(httpClientChannel);
HttpRequest httpRequest = new DefaultHttpRequest(version, method, targetURI);
HttpHeaders httpRequestHeaders = httpRequest.headers();
if (httpClientConfig.hasWriteHeaders()) {
httpRequestHeaders.add(headers);
}
if (isContentLengthSet(httpRequest)) {
httpRequest.setContent(httpContent);
ChannelFuture future = transport.write(httpRequest);
if (httpReadableBytes == getContentLength(httpRequest)) {
httpClientChannel.state(CONTENT_COMPLETE);
} else {
httpClientChannel.state(CONTENT_STREAMED);
}
chainWriteCompletes(future, httpFuture, httpReadableBytes);
} else if (isTransferEncodingChunked(httpRequest)) {
httpRequest.setChunked(true);
transport.write(httpRequest);
httpClientChannel.state(CONTENT_CHUNKED);
HttpChunk httpChunk = new DefaultHttpChunk(httpContent);
ChannelFuture future = transport.write(httpChunk);
chainWriteCompletes(future, httpFuture, httpReadableBytes);
} else if (httpRequest.headers().contains(Names.UPGRADE)) {
httpRequest.setContent(httpContent);
ChannelFuture future = transport.write(httpRequest);
httpClientChannel.state(UPGRADEABLE);
chainWriteCompletes(future, httpFuture, httpReadableBytes);
} else if (httpClientConfig.getMaximumBufferedContentLength() >= httpReadableBytes) {
// automatically calculate content-length
httpRequest.setContent(httpContent);
httpBufferedRequest = httpRequest;
httpClientChannel.state(CONTENT_BUFFERED);
httpFuture.setSuccess();
} else {
throw new IllegalStateException("Missing Upgrade, Content-Length, Transfer-Encoding: chunked");
}
break;
case CONTENT_BUFFERED:
ChannelBuffer httpBufferedContent = httpBufferedRequest.getContent();
int httpBufferedBytes = httpBufferedContent.readableBytes();
if (httpClientConfig.getMaximumBufferedContentLength() >= httpBufferedBytes + httpReadableBytes) {
httpBufferedRequest.setContent(copiedBuffer(httpBufferedContent, httpContent));
httpFuture.setSuccess();
} else {
throw new IllegalStateException("Exceeded maximum buffered content to calculate content length");
}
break;
case CONTENT_CHUNKED: {
HttpChunk httpChunk = new DefaultHttpChunk(httpContent);
ChannelFuture future = transport.write(httpChunk);
chainWriteCompletes(future, httpFuture, httpReadableBytes);
break;
}
case CONTENT_STREAMED: {
// TODO: verify content size does not exceed Content-Length value
HttpChunk httpChunk = new DefaultHttpChunk(httpContent);
ChannelFuture future = transport.write(httpChunk);
chainWriteCompletes(future, httpFuture, httpReadableBytes);
break;
}
case UPGRADEABLE: {
ChannelFuture future = transport.write(httpContent);
chainWriteCompletes(future, httpFuture, httpReadableBytes);
break;
}
case CONTENT_COMPLETE:
throw new IllegalStateException("attempted write after request content complete");
}
}
@Override
protected void shutdownOutputRequested(ChannelPipeline pipeline, ShutdownOutputEvent evt) throws Exception {
HttpClientChannel httpClientChannel = (HttpClientChannel) pipeline.getChannel();
ChannelFuture httpFuture = evt.getFuture();
shutdownOutputRequested(httpClientChannel, httpFuture);
}
@Override
protected void flushRequested(ChannelPipeline pipeline, FlushEvent evt) throws Exception {
HttpClientChannel httpClientChannel = (HttpClientChannel) pipeline.getChannel();
ChannelFuture httpFuture = evt.getFuture();
flushRequested(httpClientChannel, httpFuture);
}
@Override
protected void abortRequested(ChannelPipeline pipeline, final AbortEvent evt) throws Exception {
HttpClientChannel channel = (HttpClientChannel) pipeline.getChannel();
// We flush before an abort, as we don't expect script authors
// to purposely add a write before an abort and not expect it
// to make it on the wire
ChannelFuture flushFuture = Channels.future(channel);
flushRequested(channel, flushFuture);
flushFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
ChannelFuture disconnect = transport.disconnect();
chainFutures(disconnect, evt.getFuture());
}
});
};
@Override
protected void closeRequested(ChannelPipeline pipeline, ChannelStateEvent evt) throws Exception {
HttpClientChannel httpClientChannel = (HttpClientChannel) pipeline.getChannel();
ChannelFuture httpFuture = evt.getFuture();
httpFuture.setSuccess();
switch (httpClientChannel.state()) {
case UPGRADEABLE:
transport.close();
break;
default:
ChannelFuture inputShutdown = future(httpClientChannel);
shutdownOutputRequested(httpClientChannel, inputShutdown);
break;
}
// TODO: extends states to model request and response status separately
boolean wasConnected = httpClientChannel.isConnected();
boolean wasBound = httpClientChannel.isBound();
if (httpClientChannel.setClosed()) {
if (wasConnected) {
fireChannelDisconnected(httpClientChannel);
}
if (wasBound) {
fireChannelUnbound(httpClientChannel);
}
fireChannelClosed(httpClientChannel);
}
}
private void shutdownOutputRequested(HttpClientChannel httpClientChannel, ChannelFuture httpFuture) throws Exception {
switch (httpClientChannel.state()) {
case CONTENT_CHUNKED:
DefaultHttpChunkTrailer trailingChunk = new DefaultHttpChunkTrailer();
HttpHeaders writeTrailers = httpClientChannel.getConfig().getWriteTrailers();
trailingChunk.trailingHeaders().add(writeTrailers);
ChannelFuture future = transport.write(trailingChunk);
httpClientChannel.state(CONTENT_COMPLETE);
chainFutures(future, httpFuture);
break;
default:
flushRequested(httpClientChannel, httpFuture);
break;
}
}
private void flushRequested(HttpClientChannel httpClientChannel, ChannelFuture httpFuture) throws Exception {
switch (httpClientChannel.state()) {
case REQUEST: {
HttpChannelConfig httpClientConfig = httpClientChannel.getConfig();
HttpVersion version = httpClientConfig.getVersion();
HttpMethod method = httpClientConfig.getMethod();
HttpHeaders headers = httpClientConfig.getWriteHeaders();
String targetURI = getTargetURI(httpClientChannel);
HttpRequest httpRequest = new DefaultHttpRequest(version, method, targetURI);
HttpHeaders httpRequestHeaders = httpRequest.headers();
if (httpClientConfig.hasWriteHeaders()) {
httpRequestHeaders.add(headers);
}
if (isContentLengthSet(httpRequest)) {
ChannelFuture future = transport.write(httpRequest);
if (getContentLength(httpRequest) == 0) {
httpClientChannel.state(CONTENT_COMPLETE);
} else {
httpClientChannel.state(CONTENT_STREAMED);
}
chainFutures(future, httpFuture);
} else if (isTransferEncodingChunked(httpRequest)) {
httpRequest.setChunked(true);
ChannelFuture future = transport.write(httpRequest);
httpClientChannel.state(CONTENT_CHUNKED);
chainFutures(future, httpFuture);
} else if (httpRequestHeaders.contains(Names.UPGRADE)) {
ChannelFuture future = transport.write(httpRequest);
httpClientChannel.state(UPGRADEABLE);
chainFutures(future, httpFuture);
} else if ("GET".equalsIgnoreCase(method.getName()) || "HEAD".equalsIgnoreCase(method.getName())) {
// no content and no content-length
ChannelFuture future = transport.write(httpRequest);
httpClientChannel.state(CONTENT_COMPLETE);
chainFutures(future, httpFuture);
} else if (httpClientConfig.getMaximumBufferedContentLength() > 0) {
// no content and content-length: 0
setContentLength(httpRequest, 0);
ChannelFuture future = transport.write(httpRequest);
httpClientChannel.state(CONTENT_COMPLETE);
chainFutures(future, httpFuture);
} else {
throw new IllegalStateException("Missing Upgrade, Content-Length, or Transfer-Encoding: chunked");
}
break;
}
case CONTENT_BUFFERED: {
HttpRequest httpBufferedRequest = this.httpBufferedRequest;
this.httpBufferedRequest = null;
if (httpBufferedRequest != null) {
ChannelBuffer httpBufferedContent = httpBufferedRequest.getContent();
int httpReadableBytes = httpBufferedContent.readableBytes();
setContentLength(httpBufferedRequest, httpReadableBytes);
ChannelFuture future = transport.write(httpBufferedRequest);
httpClientChannel.state(CONTENT_COMPLETE);
chainWriteCompletes(future, httpFuture, httpReadableBytes);
} else {
throw new IllegalStateException("No buffered content");
}
break;
}
case UPGRADEABLE:
case CONTENT_COMPLETE:
httpFuture.setSuccess();
break;
default:
break;
}
}
private static String getTargetURI(HttpClientChannel httpClientChannel) throws URISyntaxException {
HttpChannelConfig httpClientConfig = httpClientChannel.getConfig();
HttpRequestForm requestForm = httpClientConfig.getRequestForm();
if (requestForm == null) {
// See RFC-7230, section 5.3.1 origin-form and section 5.3.2 absolute-form
// default to origin-form when Host header present, otherwise absolute-form
if (httpClientConfig.hasWriteHeaders() && httpClientConfig.getWriteHeaders().contains(Names.HOST)) {
requestForm = ORIGIN_FORM;
} else {
requestForm = ABSOLUTE_FORM;
}
}
QueryStringEncoder query = httpClientConfig.getWriteQuery();
ChannelAddress httpRemoteAddress = httpClientChannel.getRemoteAddress();
URI httpRemoteURI = query != null ? query.toUri() : httpRemoteAddress.getLocation();
switch (requestForm) {
case ORIGIN_FORM:
String requestPath = httpRemoteURI.getPath();
String requestQuery = httpRemoteURI.getQuery();
return (requestQuery != null) ? format("%s?%s", requestPath, requestQuery) : requestPath;
case ABSOLUTE_FORM:
default:
return httpRemoteURI.toString();
}
}
}