View Javadoc

    1   /*
    2    * Copyright 2010 Red Hat, Inc.
    3    *
    4    * Red Hat licenses this file to you under the Apache License, version 2.0
    5    * (the "License"); you may not use this file except in compliance with the
    6    * 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.example.http.websocket;
    17
    18  import static org.jboss.netty.handler.codec.http.HttpHeaders.*;
    19  import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;
    20  import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.*;
    21  import static org.jboss.netty.handler.codec.http.HttpMethod.*;
    22  import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*;
    23  import static org.jboss.netty.handler.codec.http.HttpVersion.*;
    24
    25  import java.security.MessageDigest;
    26
    27  import org.jboss.netty.buffer.ChannelBuffer;
    28  import org.jboss.netty.buffer.ChannelBuffers;
    29  import org.jboss.netty.channel.ChannelFuture;
    30  import org.jboss.netty.channel.ChannelFutureListener;
    31  import org.jboss.netty.channel.ChannelHandlerContext;
    32  import org.jboss.netty.channel.ChannelPipeline;
    33  import org.jboss.netty.channel.ExceptionEvent;
    34  import org.jboss.netty.channel.MessageEvent;
    35  import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
    36  import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
    37  import org.jboss.netty.handler.codec.http.HttpHeaders;
    38  import org.jboss.netty.handler.codec.http.HttpRequest;
    39  import org.jboss.netty.handler.codec.http.HttpResponse;
    40  import org.jboss.netty.handler.codec.http.HttpResponseStatus;
    41  import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
    42  import org.jboss.netty.handler.codec.http.HttpHeaders.Values;
    43  import org.jboss.netty.handler.codec.http.websocket.DefaultWebSocketFrame;
    44  import org.jboss.netty.handler.codec.http.websocket.WebSocketFrame;
    45  import org.jboss.netty.handler.codec.http.websocket.WebSocketFrameDecoder;
    46  import org.jboss.netty.handler.codec.http.websocket.WebSocketFrameEncoder;
    47  import org.jboss.netty.util.CharsetUtil;
    48
    49  /**
    50   * @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
    51   * @author <a href="http://gleamynode.net/">Trustin Lee</a>
    52   *
    53   * @version $Rev: 2314 $, $Date: 2010-06-22 16:02:27 +0900 (Tue, 22 Jun 2010) $
    54   */
    55  public class WebSocketServerHandler extends SimpleChannelUpstreamHandler {
    56
    57      private static final String WEBSOCKET_PATH = "/websocket";
    58
    59      @Override
    60      public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
    61          Object msg = e.getMessage();
    62          if (msg instanceof HttpRequest) {
    63              handleHttpRequest(ctx, (HttpRequest) msg);
    64          } else if (msg instanceof WebSocketFrame) {
    65              handleWebSocketFrame(ctx, (WebSocketFrame) msg);
    66          }
    67      }
    68
    69      private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
    70          // Allow only GET methods.
    71          if (req.getMethod() != GET) {
    72              sendHttpResponse(
    73                      ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN));
    74              return;
    75          }
    76
    77          // Send the demo page.
    78          if (req.getUri().equals("/")) {
    79              HttpResponse res = new DefaultHttpResponse(HTTP_1_1, OK);
    80
    81              ChannelBuffer content =
    82                  WebSocketServerIndexPage.getContent(getWebSocketLocation(req));
    83
    84              res.setHeader(CONTENT_TYPE, "text/html; charset=UTF-8");
    85              setContentLength(res, content.readableBytes());
    86
    87              res.setContent(content);
    88              sendHttpResponse(ctx, req, res);
    89              return;
    90          }
    91
    92          // Serve the WebSocket handshake request.
    93          if (req.getUri().equals(WEBSOCKET_PATH) &&
    94              Values.UPGRADE.equalsIgnoreCase(req.getHeader(CONNECTION)) &&
    95              WEBSOCKET.equalsIgnoreCase(req.getHeader(Names.UPGRADE))) {
    96
    97              // Create the WebSocket handshake response.
    98              HttpResponse res = new DefaultHttpResponse(
    99                      HTTP_1_1,
    100                     new HttpResponseStatus(101, "Web Socket Protocol Handshake"));
    101             res.addHeader(Names.UPGRADE, WEBSOCKET);
    102             res.addHeader(CONNECTION, Values.UPGRADE);
    103
    104             // Fill in the headers and contents depending on handshake method.
    105             if (req.containsHeader(SEC_WEBSOCKET_KEY1) &&
    106                 req.containsHeader(SEC_WEBSOCKET_KEY2)) {
    107                 // New handshake method with a challenge:
    108                 res.addHeader(SEC_WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
    109                 res.addHeader(SEC_WEBSOCKET_LOCATION, getWebSocketLocation(req));
    110                 String protocol = req.getHeader(SEC_WEBSOCKET_PROTOCOL);
    111                 if (protocol != null) {
    112                     res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol);
    113                 }
    114
    115                 // Calculate the answer of the challenge.
    116                 String key1 = req.getHeader(SEC_WEBSOCKET_KEY1);
    117                 String key2 = req.getHeader(SEC_WEBSOCKET_KEY2);
    118                 int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length());
    119                 int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length());
    120                 long c = req.getContent().readLong();
    121                 ChannelBuffer input = ChannelBuffers.buffer(16);
    122                 input.writeInt(a);
    123                 input.writeInt(b);
    124                 input.writeLong(c);
    125                 ChannelBuffer output = ChannelBuffers.wrappedBuffer(
    126                         MessageDigest.getInstance("MD5").digest(input.array()));
    127                 res.setContent(output);
    128             } else {
    129                 // Old handshake method with no challenge:
    130                 res.addHeader(WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
    131                 res.addHeader(WEBSOCKET_LOCATION, getWebSocketLocation(req));
    132                 String protocol = req.getHeader(WEBSOCKET_PROTOCOL);
    133                 if (protocol != null) {
    134                     res.addHeader(WEBSOCKET_PROTOCOL, protocol);
    135                 }
    136             }
    137
    138             // Upgrade the connection and send the handshake response.
    139             ChannelPipeline p = ctx.getChannel().getPipeline();
    140             p.remove("aggregator");
    141             p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder());
    142
    143             ctx.getChannel().write(res);
    144
    145             p.replace("encoder", "wsencoder", new WebSocketFrameEncoder());
    146             return;
    147         }
    148
    149         // Send an error page otherwise.
    150         sendHttpResponse(
    151                 ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN));
    152     }
    153
    154     private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
    155         // Send the uppercased string back.
    156         ctx.getChannel().write(
    157                 new DefaultWebSocketFrame(frame.getTextData().toUpperCase()));
    158     }
    159
    160     private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
    161         // Generate an error page if response status code is not OK (200).
    162         if (res.getStatus().getCode() != 200) {
    163             res.setContent(
    164                     ChannelBuffers.copiedBuffer(
    165                             res.getStatus().toString(), CharsetUtil.UTF_8));
    166             setContentLength(res, res.getContent().readableBytes());
    167         }
    168
    169         // Send the response and close the connection if necessary.
    170         ChannelFuture f = ctx.getChannel().write(res);
    171         if (!isKeepAlive(req) || res.getStatus().getCode() != 200) {
    172             f.addListener(ChannelFutureListener.CLOSE);
    173         }
    174     }
    175
    176     @Override
    177     public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
    178             throws Exception {
    179         e.getCause().printStackTrace();
    180         e.getChannel().close();
    181     }
    182
    183     private String getWebSocketLocation(HttpRequest req) {
    184         return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + WEBSOCKET_PATH;
    185     }
    186 }