/* * Copyright 2012 Jason Miller * * 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 jj.http.server.websocket; import static jj.http.server.PipelineStages.*; import static jj.server.ServerLocation.Virtual; import java.util.Set; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import io.netty.handler.codec.http.websocketx.WebSocketVersion; import javax.inject.Inject; import javax.inject.Singleton; import jj.http.server.HttpServerResponse; import jj.http.server.uri.URIMatch; import jj.resource.ResourceFinder; /** * @author jason * */ @Singleton public class WebSocketConnectionMaker { private final WebSocketFrameHandlerCreator handlerCreator; private final ResourceFinder resourceFinder; private final ChannelHandlerContext ctx; private final FullHttpRequest request; private final HttpServerResponse response; private final WebSocketServerHandshakerFactory handshakerFactory; private final Set<Class<? extends WebSocketConnectionHost>> webSocketConnectionHostClasses; @Inject WebSocketConnectionMaker( final WebSocketFrameHandlerCreator handlerCreator, final ResourceFinder resourceFinder, final ChannelHandlerContext ctx, final FullHttpRequest request, final HttpServerResponse response, final WebSocketServerHandshakerFactory handshakerFactory, final Set<Class<? extends WebSocketConnectionHost>> webSocketConnectionHostClasses ) { this.handlerCreator = handlerCreator; this.resourceFinder = resourceFinder; this.ctx = ctx; this.request = request; this.response = response; this.handshakerFactory = handshakerFactory; this.webSocketConnectionHostClasses = webSocketConnectionHostClasses; } public void handshakeWebsocket() { final WebSocketServerHandshaker handshaker = handshakerFactory.newHandshaker(request); if (handshaker == null) { response .header(HttpHeaderNames.SEC_WEBSOCKET_VERSION, WebSocketVersion.V13.toHttpHeaderValue()) .sendError(HttpResponseStatus.UPGRADE_REQUIRED); } else { doHandshake(ctx, request, handshaker); } } private void doHandshake( final ChannelHandlerContext ctx, final FullHttpRequest request, final WebSocketServerHandshaker handshaker ) { handshaker.handshake(ctx.channel(), request).addListener(new ChannelFutureListener() { private boolean isHandshakeFailure(ChannelFuture future) { return future.cause() != null && future.cause() instanceof WebSocketHandshakeException; } @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { URIMatch uriMatch = new URIMatch(request.uri()); WebSocketConnectionHost host = null; for (Class<? extends WebSocketConnectionHost> hostClass : webSocketConnectionHostClasses) { host = resourceFinder.findResource(hostClass, Virtual, uriMatch.name); if (host != null) break; } if (host == null) { // 1011 indicates that a server is terminating the connection because // it encountered an unexpected condition that prevented it from // fulfilling the request. ctx.writeAndFlush(new CloseWebSocketFrame(1011, null)).addListener(CLOSE); // TODO: is closing here the right thing? or do we count on the client closing the connection // to avoid the time_wait state? } else if (!uriMatch.sha1.equals(host.sha1())) { ctx.writeAndFlush(new TextWebSocketFrame("jj-reload")) .addListener(f -> { // 1001 indicates that an endpoint is "going away", such as a server // going down or a browser having navigated away from a page. ctx.writeAndFlush(new CloseWebSocketFrame(1001, null)).addListener(CLOSE); // TODO: is closing here the right thing? or do we count on the client closing the connection // to avoid the time_wait state? }); } else { ctx.pipeline().replace( JJEngine.toString(), JJWebsocketHandler.toString(), handlerCreator.createHandler(handshaker, host) ); } } else if (isHandshakeFailure(future)) { response.sendError(HttpResponseStatus.BAD_REQUEST); } else { ctx.close(); } } }); } }