/**
* 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.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.fireChannelOpen;
import static org.jboss.netty.channel.Channels.fireChannelUnbound;
import static org.jboss.netty.channel.Channels.fireExceptionCaught;
import static org.jboss.netty.channel.Channels.fireMessageReceived;
import static org.jboss.netty.channel.Channels.future;
import static org.jboss.netty.channel.Channels.write;
import static org.jboss.netty.handler.codec.http.HttpHeaders.getContentLength;
import static org.jboss.netty.handler.codec.http.HttpHeaders.getHost;
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.HttpResponseStatus.BAD_REQUEST;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
import static org.kaazing.k3po.driver.internal.channel.Channels.remoteAddress;
import static org.kaazing.k3po.driver.internal.netty.channel.Channels.fireInputShutdown;
import java.net.URI;
import java.util.Map.Entry;
import java.util.NavigableMap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelConfig;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelFuture;
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.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
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.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.kaazing.k3po.driver.internal.behavior.handler.codec.http.QueryStringEncoder;
import org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChildChannel.HttpReadState;
import org.kaazing.k3po.driver.internal.netty.channel.ChannelAddress;
import org.kaazing.k3po.driver.internal.netty.channel.Channels;
public class HttpChildChannelSource extends HttpChannelHandler {
private final NavigableMap<ChannelAddress, HttpServerChannel> httpBindings;
private volatile HttpChildChannel httpChildChannel;
public HttpChildChannelSource(NavigableMap<ChannelAddress, HttpServerChannel> httpBindings) {
this.httpBindings = httpBindings;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
if (httpChildChannel != null) {
HttpChildChannel httpChildChannel = this.httpChildChannel;
this.httpChildChannel = null;
if (httpChildChannel.setReadClosed() || httpChildChannel.setWriteClosed()) {
fireExceptionCaught(httpChildChannel, e.getCause());
fireChannelClosed(httpChildChannel);
}
}
Channel channel = ctx.getChannel();
channel.close();
}
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
HttpChildChannel httpChildChannel = this.httpChildChannel;
if (httpChildChannel != null) {
this.httpChildChannel = null;
switch (httpChildChannel.readState()) {
case UPGRADED:
if (httpChildChannel.setReadClosed()) {
fireChannelDisconnected(httpChildChannel);
fireChannelUnbound(httpChildChannel);
fireChannelClosed(httpChildChannel);
}
break;
case CONTENT_COMPLETE:
break;
default:
Channels.fireChannelAborted(httpChildChannel);
break;
}
switch (httpChildChannel.writeState()) {
case UPGRADED:
case CONTENT_CLOSE:
if (httpChildChannel.setWriteClosed()) {
fireChannelDisconnected(httpChildChannel);
fireChannelUnbound(httpChildChannel);
fireChannelClosed(httpChildChannel);
}
break;
case CONTENT_COMPLETE:
break;
default:
Channels.fireChannelAborted(httpChildChannel);
break;
}
}
}
@Override
protected void httpMessageReceived(ChannelHandlerContext ctx, MessageEvent e, HttpRequest httpRequest) throws Exception {
HttpVersion version = httpRequest.getProtocolVersion();
URI httpLocation = getEffectiveURI(httpRequest);
if (httpLocation == null) {
// see RFC-7230 section 5.4 Host
HttpResponse httpResponse = new DefaultHttpResponse(version, BAD_REQUEST);
ChannelFuture future = future(ctx.getChannel());
write(ctx, future, httpResponse);
return;
}
// channel's local address is resolved address so get the bind address from
// server channel's attachment
ChannelAddress transportCandidate = (ChannelAddress) ctx.getChannel().getParent().getAttachment();
ChannelAddress candidate = new ChannelAddress(httpLocation, transportCandidate);
Entry<ChannelAddress, HttpServerChannel> httpBinding = httpBindings.floorEntry(candidate);
if (httpBinding == null) {
HttpResponse httpResponse = new DefaultHttpResponse(version, NOT_FOUND);
ChannelFuture future = future(ctx.getChannel());
write(ctx, future, httpResponse);
return;
}
HttpServerChannel parent = httpBinding.getValue();
ChannelFactory factory = parent.getFactory();
ChannelConfig config = parent.getConfig();
ChannelPipelineFactory pipelineFactory = config.getPipelineFactory();
ChannelPipeline pipeline = pipelineFactory.getPipeline();
ChannelAddress httpLocalAddress = parent.getLocalAddress();
Channel transport = ctx.getChannel();
ChannelAddress remoteAddress = remoteAddress(transport);
ChannelAddress httpRemoteAddress = new ChannelAddress(httpLocation, remoteAddress, true);
HttpChildChannelSink sink = new HttpChildChannelSink(transport);
HttpChildChannel httpChildChannel = new HttpChildChannel(parent, factory, pipeline, sink);
HttpChannelConfig httpChildConfig = httpChildChannel.getConfig();
httpChildConfig.setMethod(httpRequest.getMethod());
httpChildConfig.setVersion(version);
httpChildConfig.getReadHeaders().set(httpRequest.headers());
httpChildConfig.setReadQuery(new QueryStringDecoder(httpRequest.getUri()));
httpChildConfig.setWriteQuery(new QueryStringEncoder(httpRequest.getUri()));
httpChildConfig.setStatus(HttpResponseStatus.OK);
this.httpChildChannel = httpChildChannel;
ChannelBuffer content = httpRequest.getContent();
// update read state before firing channel events
if (isTransferEncodingChunked(httpRequest)) {
httpChildChannel.readState(HttpReadState.CONTENT_CHUNKED);
}
else if (isContentLengthSet(httpRequest)) {
long contentLength = getContentLength(httpRequest);
contentLength -= content.readableBytes();
if (contentLength > 0) {
httpChildChannel.readState(HttpReadState.CONTENT_CHUNKED);
}
else {
httpChildChannel.readState(HttpReadState.CONTENT_COMPLETE);
}
}
else {
// see RFC-7230 section 3.3
// content indicated by presence of Content-Length or Transfer-Encoding
httpChildChannel.readState(HttpReadState.CONTENT_COMPLETE);
}
fireChannelOpen(httpChildChannel);
httpChildChannel.setLocalAddress(httpLocalAddress);
httpChildChannel.setBound();
fireChannelBound(httpChildChannel, httpLocalAddress);
httpChildChannel.setRemoteAddress(httpRemoteAddress);
httpChildChannel.setConnected();
fireChannelConnected(httpChildChannel, httpRemoteAddress);
if (content.readable()) {
fireMessageReceived(httpChildChannel, content);
}
// note: status may be set in reaction to one of the above events, such as CONNECTED
// so defer status code check until this point
if (httpChildConfig.getStatus().getCode() == SWITCHING_PROTOCOLS.getCode()) {
httpChildChannel.readState(HttpReadState.UPGRADED);
}
switch (httpChildChannel.readState()) {
case CONTENT_COMPLETE:
fireInputShutdown(httpChildChannel);
this.httpChildChannel = null;
if (httpChildChannel.setReadClosed()) {
fireChannelDisconnected(httpChildChannel);
fireChannelUnbound(httpChildChannel);
fireChannelClosed(httpChildChannel);
}
break;
default:
break;
}
}
@Override
protected void httpMessageReceived(ChannelHandlerContext ctx, MessageEvent e, HttpChunk chunk) throws Exception {
ChannelBuffer content = chunk.getContent();
if (content.readable()) {
fireMessageReceived(httpChildChannel, content);
}
boolean last = chunk.isLast();
if (last) {
HttpHeaders trailingHeaders = ((HttpChunkTrailer) chunk).trailingHeaders();
httpChildChannel.getConfig().getReadTrailers().add(trailingHeaders);
HttpChildChannel httpChildChannel = this.httpChildChannel;
httpChildChannel.readState(HttpReadState.CONTENT_COMPLETE);
this.httpChildChannel = null;
fireInputShutdown(httpChildChannel);
if (httpChildChannel.setReadClosed()) {
fireChannelDisconnected(httpChildChannel);
fireChannelUnbound(httpChildChannel);
fireChannelClosed(httpChildChannel);
}
}
}
@Override
protected void httpMessageReceived(ChannelHandlerContext ctx, MessageEvent e, ChannelBuffer message) throws Exception {
if (message.readable()) {
// after 101 switching protocols
fireMessageReceived(httpChildChannel, message);
}
}
private static URI getEffectiveURI(HttpRequest httpRequest) {
URI requestURI = URI.create(httpRequest.getUri());
if (requestURI.isAbsolute()) {
return requestURI;
}
String host = getHost(httpRequest);
return (host != null) ? URI.create(format("http://%s%s", host, requestURI)) : null;
}
}