View Javadoc

    1   /*
    2    * Copyright 2012 The Netty Project
    3    *
    4    * The Netty Project licenses this file to you under the Apache License,
    5    * version 2.0 (the "License"); you may not use this file except in compliance
    6    * with the License. You may obtain a copy of the License at:
    7    *
    8    *   http://www.apache.org/licenses/LICENSE-2.0
    9    *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    13   * License for the specific language governing permissions and limitations
    14   * under the License.
    15   */
    16  package org.jboss.netty.handler.codec.http.websocketx;
    17
    18  import java.net.URI;
    19  import java.nio.ByteBuffer;
    20  import java.util.Arrays;
    21  import java.util.Map;
    22
    23  import org.jboss.netty.buffer.ChannelBuffers;
    24  import org.jboss.netty.channel.Channel;
    25  import org.jboss.netty.channel.ChannelFuture;
    26  import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
    27  import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
    28  import org.jboss.netty.handler.codec.http.HttpHeaders.Values;
    29  import org.jboss.netty.handler.codec.http.HttpMethod;
    30  import org.jboss.netty.handler.codec.http.HttpRequest;
    31  import org.jboss.netty.handler.codec.http.HttpRequestEncoder;
    32  import org.jboss.netty.handler.codec.http.HttpResponse;
    33  import org.jboss.netty.handler.codec.http.HttpResponseDecoder;
    34  import org.jboss.netty.handler.codec.http.HttpResponseStatus;
    35  import org.jboss.netty.handler.codec.http.HttpVersion;
    36
    37  /**
    38   * <p>
    39   * Performs client side opening and closing handshakes for web socket specification version <a
    40   * href="http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00" >draft-ietf-hybi-thewebsocketprotocol-
    41   * 00</a>
    42   * </p>
    43   * <p>
    44   * A very large portion of this code was taken from the Netty 3.2 HTTP example.
    45   * </p>
    46   */
    47  public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
    48
    49      private byte[] expectedChallengeResponseBytes;
    50
    51      /**
    52       * Constructor with default values
    53       *
    54       * @param webSocketURL
    55       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
    56       *            sent to this URL.
    57       * @param version
    58       *            Version of web socket specification to use to connect to the server
    59       * @param subprotocol
    60       *            Sub protocol request sent to the server.
    61       * @param customHeaders
    62       *            Map of custom headers to add to the client request
    63       */
    64      public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
    65              Map<String, String> customHeaders) {
    66          this(webSocketURL, version, subprotocol, customHeaders, Long.MAX_VALUE);
    67      }
    68
    69      /**
    70       * Constructor
    71       *
    72       * @param webSocketURL
    73       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
    74       *            sent to this URL.
    75       * @param version
    76       *            Version of web socket specification to use to connect to the server
    77       * @param subprotocol
    78       *            Sub protocol request sent to the server.
    79       * @param customHeaders
    80       *            Map of custom headers to add to the client request
    81       * @param maxFramePayloadLength
    82       *            Maximum length of a frame's payload
    83       */
    84      public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
    85              Map<String, String> customHeaders, long maxFramePayloadLength) {
    86          super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
    87      }
    88
    89
    90      /**
    91       * <p>
    92       * Sends the opening request to the server:
    93       * </p>
    94       *
    95       * <pre>
    96       * GET /demo HTTP/1.1
    97       * Upgrade: WebSocket
    98       * Connection: Upgrade
    99       * Host: example.com
    100      * Origin: http://example.com
    101      * Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
    102      * Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
    103      *
    104      * ^n:ds[4U
    105      * </pre>
    106      *
    107      * @param channel
    108      *            Channel into which we can write our request
    109      */
    110     @Override
    111     public ChannelFuture handshake(Channel channel) {
    112         // Make keys
    113         int spaces1 = WebSocketUtil.randomNumber(1, 12);
    114         int spaces2 = WebSocketUtil.randomNumber(1, 12);
    115
    116         int max1 = Integer.MAX_VALUE / spaces1;
    117         int max2 = Integer.MAX_VALUE / spaces2;
    118
    119         int number1 = WebSocketUtil.randomNumber(0, max1);
    120         int number2 = WebSocketUtil.randomNumber(0, max2);
    121
    122         int product1 = number1 * spaces1;
    123         int product2 = number2 * spaces2;
    124
    125         String key1 = Integer.toString(product1);
    126         String key2 = Integer.toString(product2);
    127
    128         key1 = insertRandomCharacters(key1);
    129         key2 = insertRandomCharacters(key2);
    130
    131         key1 = insertSpaces(key1, spaces1);
    132         key2 = insertSpaces(key2, spaces2);
    133
    134         byte[] key3 = WebSocketUtil.randomBytes(8);
    135
    136         ByteBuffer buffer = ByteBuffer.allocate(4);
    137         buffer.putInt(number1);
    138         byte[] number1Array = buffer.array();
    139         buffer = ByteBuffer.allocate(4);
    140         buffer.putInt(number2);
    141         byte[] number2Array = buffer.array();
    142
    143         byte[] challenge = new byte[16];
    144         System.arraycopy(number1Array, 0, challenge, 0, 4);
    145         System.arraycopy(number2Array, 0, challenge, 4, 4);
    146         System.arraycopy(key3, 0, challenge, 8, 8);
    147         expectedChallengeResponseBytes = WebSocketUtil.md5(challenge);
    148
    149         // Get path
    150         URI wsURL = getWebSocketUrl();
    151         String path = wsURL.getPath();
    152         if (wsURL.getQuery() != null && wsURL.getQuery().length() > 0) {
    153             path = wsURL.getPath() + "?" + wsURL.getQuery();
    154         }
    155
    156         // Format request
    157         HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
    158         request.addHeader(Names.UPGRADE, Values.WEBSOCKET);
    159         request.addHeader(Names.CONNECTION, Values.UPGRADE);
    160         request.addHeader(Names.HOST, wsURL.getHost());
    161
    162         int wsPort = wsURL.getPort();
    163         String originValue = "http://" + wsURL.getHost();
    164         if (wsPort != 80 && wsPort != 443) {
    165             // if the port is not standard (80/443) its needed to add the port to the header.
    166             // See http://tools.ietf.org/html/rfc6454#section-6.2
    167             originValue = originValue + ":" + wsPort;
    168         }
    169         request.addHeader(Names.ORIGIN, originValue);
    170
    171         request.addHeader(Names.SEC_WEBSOCKET_KEY1, key1);
    172         request.addHeader(Names.SEC_WEBSOCKET_KEY2, key2);
    173         String expectedSubprotocol = getExpectedSubprotocol();
    174         if (expectedSubprotocol != null && !expectedSubprotocol.equals("")) {
    175             request.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
    176         }
    177
    178         if (customHeaders != null) {
    179             for (String header : customHeaders.keySet()) {
    180                 request.addHeader(header, customHeaders.get(header));
    181             }
    182         }
    183
    184         request.setContent(ChannelBuffers.copiedBuffer(key3));
    185
    186         ChannelFuture future = channel.write(request);
    187
    188         channel.getPipeline().replace(HttpRequestEncoder.class, "ws-encoder", new WebSocket00FrameEncoder());
    189
    190         return future;
    191     }
    192
    193     /**
    194      * <p>
    195      * Process server response:
    196      * </p>
    197      *
    198      * <pre>
    199      * HTTP/1.1 101 WebSocket Protocol Handshake
    200      * Upgrade: WebSocket
    201      * Connection: Upgrade
    202      * Sec-WebSocket-Origin: http://example.com
    203      * Sec-WebSocket-Location: ws://example.com/demo
    204      * Sec-WebSocket-Protocol: sample
    205      *
    206      * 8jKS'y:G*Co,Wxa-
    207      * </pre>
    208      *
    209      * @param channel
    210      *            Channel
    211      * @param response
    212      *            HTTP response returned from the server for the request sent by beginOpeningHandshake00().
    213      * @throws WebSocketHandshakeException
    214      */
    215     @Override
    216     public void finishHandshake(Channel channel, HttpResponse response) throws WebSocketHandshakeException {
    217         final HttpResponseStatus status = new HttpResponseStatus(101, "WebSocket Protocol Handshake");
    218
    219         if (!response.getStatus().equals(status)) {
    220             throw new WebSocketHandshakeException("Invalid handshake response status: " + response.getStatus());
    221         }
    222
    223         String upgrade = response.getHeader(Names.UPGRADE);
    224         if (upgrade == null || !upgrade.equals(Values.WEBSOCKET)) {
    225             throw new WebSocketHandshakeException("Invalid handshake response upgrade: "
    226                     + response.getHeader(Names.UPGRADE));
    227         }
    228
    229         String connection = response.getHeader(Names.CONNECTION);
    230         if (connection == null || !connection.equals(Values.UPGRADE)) {
    231             throw new WebSocketHandshakeException("Invalid handshake response connection: "
    232                     + response.getHeader(Names.CONNECTION));
    233         }
    234
    235         byte[] challenge = response.getContent().array();
    236         if (!Arrays.equals(challenge, expectedChallengeResponseBytes)) {
    237             throw new WebSocketHandshakeException("Invalid challenge");
    238         }
    239
    240         String subprotocol = response.getHeader(Names.SEC_WEBSOCKET_PROTOCOL);
    241         setActualSubprotocol(subprotocol);
    242
    243         setHandshakeComplete();
    244
    245         channel.getPipeline().get(HttpResponseDecoder.class).replace("ws-decoder",
    246                 new WebSocket00FrameDecoder(getMaxFramePayloadLength()));
    247
    248
    249     }
    250
    251     private static String insertRandomCharacters(String key) {
    252         int count = WebSocketUtil.randomNumber(1, 12);
    253
    254         char[] randomChars = new char[count];
    255         int randCount = 0;
    256         while (randCount < count) {
    257             int rand = (int) (Math.random() * 0x7e + 0x21);
    258             if (0x21 < rand && rand < 0x2f || 0x3a < rand && rand < 0x7e) {
    259                 randomChars[randCount] = (char) rand;
    260                 randCount += 1;
    261             }
    262         }
    263
    264         for (int i = 0; i < count; i++) {
    265             int split = WebSocketUtil.randomNumber(0, key.length());
    266             String part1 = key.substring(0, split);
    267             String part2 = key.substring(split);
    268             key = part1 + randomChars[i] + part2;
    269         }
    270
    271         return key;
    272     }
    273
    274     private static String insertSpaces(String key, int spaces) {
    275         for (int i = 0; i < spaces; i++) {
    276             int split = WebSocketUtil.randomNumber(1, key.length() - 1);
    277             String part1 = key.substring(0, split);
    278             String part2 = key.substring(split);
    279             key = part1 + " " + part2;
    280         }
    281
    282         return key;
    283     }
    284
    285 }