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 }