/* * Copyright 2016 Netflix, Inc. * * 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 io.reactivex.netty.protocol.http.ws.client; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.websocketx.WebSocket13FrameDecoder; import io.netty.handler.codec.http.websocketx.WebSocket13FrameEncoder; import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13; import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; import io.netty.util.CharsetUtil; import io.reactivex.netty.protocol.http.ws.internal.WsUtils; import static io.netty.handler.codec.http.HttpHeaderNames.*; import static io.netty.handler.codec.http.HttpHeaderNames.UPGRADE; import static io.netty.handler.codec.http.HttpHeaderValues.*; import static io.reactivex.netty.protocol.http.HttpHandlerNames.*; /** * A channel handler to appropriately setup WebSocket upgrade requests and verify upgrade responses. * It also updates the pipeline post a successful upgrade. * * The handshake code here is taken from {@link WebSocketClientHandshaker13} and not used directly because the APIs * do not suit our needs. */ public class Ws7To13UpgradeHandler extends ChannelDuplexHandler { private String expectedChallengeResponseString; private boolean upgraded; @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof HttpRequest) { final HttpRequest request = (HttpRequest) msg; if (request.headers().contains(UPGRADE, WEBSOCKET, false)) { /* * We can safely modify the request here as this request is exclusively for WS upgrades and the following * headers are added for ALL upgrade requests. Since, the handler is single-threaded, these updates do not * step on each other. */ // Get 16 bit nonce and base 64 encode it byte[] nonce = WsUtils.randomBytes(16); String key = WsUtils.base64(nonce); request.headers().set(SEC_WEBSOCKET_KEY, key); String acceptSeed = key + WebSocketClientHandshaker13.MAGIC_GUID; byte[] sha1 = WsUtils.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII)); expectedChallengeResponseString = WsUtils.base64(sha1); String hostHeader = request.headers().get(HOST); if (null != hostHeader) { request.headers().set(SEC_WEBSOCKET_ORIGIN, "http://" + hostHeader); } final ChannelHandlerContext clientCodecCtx = ctx.pipeline().context(HttpClientCodec.getName()); if (null == clientCodecCtx) { promise.tryFailure(new IllegalStateException( "Http client codec not found, can not upgrade to WebSockets.")); return; } final HttpClientCodec codec = (HttpClientCodec) clientCodecCtx.handler(); promise.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { ChannelPipeline p = future.channel().pipeline(); // Remove the encoder part of the codec as the user may start writing frames after this method returns. p.addAfter(clientCodecCtx.name(), WsClientEncoder.getName(), new WebSocket13FrameEncoder(true/*Clients must set this to true*/)); } } }); } } super.write(ctx, msg, promise); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (isUpgradeResponse(msg)) { final HttpResponse response = (HttpResponse) msg; /*Other verifications are done by WebSocketResponse itself.*/ String accept = response.headers().get(SEC_WEBSOCKET_ACCEPT); if (accept == null || !accept.equals(expectedChallengeResponseString)) { throw new WebSocketHandshakeException(String.format( "Invalid challenge. Actual: %s. Expected: %s", accept, expectedChallengeResponseString)); } final ChannelPipeline pipeline = ctx.pipeline(); ChannelHandlerContext codecCtx = pipeline.context(HttpClientCodec.getName()); if (null == codecCtx) { throw new IllegalStateException("Http codec not found, can not upgrade to WebSocket."); } pipeline.addAfter(codecCtx.name(), WsClientDecoder.getName(), new WebSocket13FrameDecoder(false/*Clients must set this to false*/, false, 65555));//TODO: Fix me pipeline.remove(HttpClientCodec.class); upgraded = true; } if (upgraded && msg instanceof HttpContent) { /*Ignore Content once upgraded. The content should not come typically since an Upgrade accept response is empty. The only HttpContent that would come is an empty LastHttpContent that netty generates.*/ ((HttpContent)msg).release(); return; } super.channelRead(ctx, msg); } private static boolean isUpgradeResponse(Object msg) { if (msg instanceof HttpResponse) { HttpResponse response = (HttpResponse) msg; HttpHeaders headers = response.headers(); return response.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS) && headers.contains(CONNECTION, HttpHeaderValues.UPGRADE, true) && headers.contains(UPGRADE, WEBSOCKET, true) && headers.contains(SEC_WEBSOCKET_ACCEPT); } return false; } }