/** * 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.bbosh; 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.fireChannelOpen; import static org.jboss.netty.channel.Channels.pipeline; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.CREATED; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.SERVICE_UNAVAILABLE; import static org.kaazing.k3po.driver.internal.netty.bootstrap.bbosh.BBoshHttpHeaders.getIntHeader; import static org.kaazing.k3po.driver.internal.netty.channel.LocationFactories.changeSchemeOnly; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NavigableMap; import java.util.UUID; import java.util.concurrent.TimeUnit; 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.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.SimpleChannelHandler; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpMethod; import org.kaazing.k3po.driver.internal.netty.bootstrap.BootstrapFactory; import org.kaazing.k3po.driver.internal.netty.bootstrap.ServerBootstrap; import org.kaazing.k3po.driver.internal.netty.bootstrap.bbosh.BBoshHttpHeaders.Names; import org.kaazing.k3po.driver.internal.netty.bootstrap.bbosh.BBoshHttpHeaders.Values; import org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChannelConfig; import org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChildChannel; import org.kaazing.k3po.driver.internal.netty.channel.ChannelAddress; import org.kaazing.k3po.driver.internal.netty.channel.ChannelAddressFactory; import org.kaazing.k3po.driver.internal.netty.channel.LocationFactory; public class BBoshHandshakeChildChannelSource extends SimpleChannelHandler { private static final LocationFactory CHANGE_SCHEME_ONLY = changeSchemeOnly("bbosh"); private final NavigableMap<URI, BBoshServerChannel> bboshBindings; private ChannelAddressFactory addressFactory; private BootstrapFactory bootstrapFactory; public BBoshHandshakeChildChannelSource(NavigableMap<URI, BBoshServerChannel> bboshBindings) { this.bboshBindings = bboshBindings; } public void setAddressFactory(ChannelAddressFactory addressFactory) { this.addressFactory = addressFactory; } public void setBootstrapFactory(BootstrapFactory bootstrapFactory) { this.bootstrapFactory = bootstrapFactory; } @Override public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { final HttpChildChannel httpChannel = (HttpChildChannel) ctx.getChannel(); final HttpChannelConfig httpConfig = httpChannel.getConfig(); ChannelAddress httpHandshakeLocalAddress = httpChannel.getLocalAddress(); URI httpHandshakeLocalURI = httpHandshakeLocalAddress.getLocation(); ChannelAddress httpHandshakeRemoteAddress = httpChannel.getRemoteAddress(); URI httpHandshakeRemoteURI = httpHandshakeRemoteAddress.getLocation(); // require no trailing path info (handles expired BBOSH connections) if (!httpHandshakeRemoteURI.equals(httpHandshakeLocalURI)) { httpConfig.setStatus(NOT_FOUND); httpChannel.close(); return; } final URI handshakeLocalURI = CHANGE_SCHEME_ONLY.createURI(httpHandshakeLocalURI); Entry<URI, BBoshServerChannel> binding = bboshBindings.floorEntry(handshakeLocalURI); if (binding == null) { httpConfig.setStatus(NOT_FOUND); httpChannel.close(); return; } if (httpConfig.getMethod() != HttpMethod.POST) { httpConfig.setStatus(METHOD_NOT_ALLOWED); httpChannel.close(); return; } HttpHeaders httpReadHeaders = httpConfig.getReadHeaders(); if (!Values.BBOSH_1_0.equals(httpReadHeaders.get(Names.X_PROTOCOL))) { httpConfig.setStatus(BAD_REQUEST); httpChannel.close(); return; } if (!Values.APPLICATION_OCTET_STREAM.equals(httpReadHeaders.get(Names.ACCEPT))) { httpConfig.setStatus(BAD_REQUEST); httpChannel.close(); return; } List<BBoshStrategy> acceptStrategies = readAcceptStrategies(httpReadHeaders); BBoshStrategy strategy = null; for (BBoshStrategy acceptStrategy : acceptStrategies) { switch (acceptStrategy.getKind()) { case POLLING: strategy = acceptStrategy; break; default: // TODO: support other strategies break; } } final BBoshStrategy negotiatedStrategy = strategy; if (strategy == null) { httpConfig.setStatus(BAD_REQUEST); httpChannel.close(); return; } switch (strategy.getKind()) { case POLLING: if (strategy.getInterval(TimeUnit.SECONDS) < 5L) { httpConfig.setStatus(BAD_REQUEST); httpChannel.close(); return; } break; case LONG_POLLING: if (strategy.getInterval(TimeUnit.SECONDS) < 30L) { httpConfig.setStatus(BAD_REQUEST); httpChannel.close(); return; } break; case STREAMING: break; } UUID uuid = UUID.randomUUID(); String connectionId = uuid.toString(); String httpHandshakeLocalPath = httpHandshakeLocalURI.getPath(); if (!httpHandshakeLocalPath.endsWith("/")) { httpHandshakeLocalPath += "/"; } final String connectionPath = format("%s%s", httpHandshakeLocalPath, connectionId); int sequenceNo = getIntHeader(httpReadHeaders, Names.X_SEQUENCE_NO); int nextSequenceNo = sequenceNo + 1; BBoshServerChannel parent = binding.getValue(); ChannelFactory factory = parent.getFactory(); ChannelConfig config = parent.getConfig(); ChannelPipelineFactory pipelineFactory = config.getPipelineFactory(); ChannelPipeline pipeline = pipelineFactory.getPipeline(); BBoshPollingChildChannelSink sink = new BBoshPollingChildChannelSink(nextSequenceNo); final BBoshChildChannel channel = new BBoshChildChannel(parent, factory, pipeline, sink); fireChannelOpen(channel); final URI httpConnectionLocalURI = httpHandshakeLocalURI.resolve(connectionPath); final ChannelAddress httpConnectionLocalAddress = addressFactory.newChannelAddress(httpConnectionLocalURI); ServerBootstrap server = bootstrapFactory.newServerBootstrap("http"); server.setPipeline(pipeline(new BBoshPollingChildChannelSource(channel))); ChannelFuture httpBindFuture = server.bindAsync(httpConnectionLocalAddress); httpBindFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture httpBindFuture) throws Exception { if (httpBindFuture.isSuccess()) { final URI connectionLocalURI = handshakeLocalURI.resolve(connectionPath); Map<String, Object> options = new HashMap<>(); options.put("bbosh.transport", httpConnectionLocalURI); ChannelAddress connectionLocalAddress = addressFactory.newChannelAddress(connectionLocalURI, options); channel.setLocalAddress(connectionLocalAddress); channel.setBound(); fireChannelBound(channel, connectionLocalAddress); ChannelAddress connectionRemoteAddress = connectionLocalAddress.newEphemeralAddress(); channel.setRemoteAddress(connectionRemoteAddress); channel.setConnected(); fireChannelConnected(channel, connectionRemoteAddress); // TODO: use interval to timeout connection before first polling request httpConfig.setStatus(CREATED); httpConfig.setMaximumBufferedContentLength(8192); HttpHeaders httpWriteHeaders = httpConfig.getWriteHeaders(); httpWriteHeaders.set(Names.CACHE_CONTROL, Values.NO_CACHE); httpWriteHeaders.set(Names.CONTENT_TYPE, Values.APPLICATION_OCTET_STREAM); httpWriteHeaders.set(Names.LOCATION, connectionPath); httpWriteHeaders.set(Names.X_STRATEGY, negotiatedStrategy.toString()); httpChannel.close(); final Channel httpBindChannel = httpBindFuture.getChannel(); channel.getCloseFuture().addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { // unbind channel-specific handler httpBindChannel.close(); } }); } else { channel.setClosed(); fireChannelClosed(channel); httpConfig.setStatus(SERVICE_UNAVAILABLE); httpChannel.close(); } } }); } private static List<BBoshStrategy> readAcceptStrategies(HttpHeaders httpHeaders) { List<String> strategyValues = httpHeaders.getAll(Names.X_ACCEPT_STRATEGY); List<BBoshStrategy> strategies = new ArrayList<>(3); for (String strategyValue : strategyValues) { String[] strategyValueParts = strategyValue.split(",\\s+"); for (String strategyValuePart : strategyValueParts) { BBoshStrategy strategy = BBoshStrategy.valueOf(strategyValuePart); strategies.add(strategy); } } return strategies; } }