All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.reactivex.netty.protocol.http.ws.client.Ws7To13UpgradeHandler Maven / Gradle / Ivy

There is a newer version: 0.5.3-rc.2
Show newest version
/*
 * 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;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy