View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5    * "License"); you may not use this file except in compliance with the License. You may obtain a
6    * 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 distributed under the License
11   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing permissions and limitations under
13   * the License.
14   */
15  package io.netty.handler.codec.http;
16  
17  import io.netty.channel.ChannelHandlerContext;
18  import io.netty.channel.ChannelPromise;
19  import io.netty.handler.codec.AsciiString;
20  
21  import java.util.Collection;
22  import java.util.LinkedHashSet;
23  import java.util.List;
24  import java.util.Set;
25  
26  import static io.netty.handler.codec.http.HttpResponseStatus.*;
27  import static io.netty.util.ReferenceCountUtil.*;
28  
29  /**
30   * Client-side handler for handling an HTTP upgrade handshake to another protocol. When the first
31   * HTTP request is sent, this handler will add all appropriate headers to perform an upgrade to the
32   * new protocol. If the upgrade fails (i.e. response is not 101 Switching Protocols), this handler
33   * simply removes itself from the pipeline. If the upgrade is successful, upgrades the pipeline to
34   * the new protocol.
35   */
36  public class HttpClientUpgradeHandler extends HttpObjectAggregator {
37  
38      /**
39       * User events that are fired to notify about upgrade status.
40       */
41      public enum UpgradeEvent {
42          /**
43           * The Upgrade request was sent to the server.
44           */
45          UPGRADE_ISSUED,
46  
47          /**
48           * The Upgrade to the new protocol was successful.
49           */
50          UPGRADE_SUCCESSFUL,
51  
52          /**
53           * The Upgrade was unsuccessful due to the server not issuing
54           * with a 101 Switching Protocols response.
55           */
56          UPGRADE_REJECTED
57      }
58  
59      /**
60       * The source codec that is used in the pipeline initially.
61       */
62      public interface SourceCodec {
63          /**
64           * Removes this codec (i.e. all associated handlers) from the pipeline.
65           */
66          void upgradeFrom(ChannelHandlerContext ctx);
67      }
68  
69      /**
70       * A codec that the source can be upgraded to.
71       */
72      public interface UpgradeCodec {
73          /**
74           * Returns the name of the protocol supported by this codec, as indicated by the {@code 'UPGRADE'} header.
75           */
76          String protocol();
77  
78          /**
79           * Sets any protocol-specific headers required to the upgrade request. Returns the names of
80           * all headers that were added. These headers will be used to populate the CONNECTION header.
81           */
82          Collection<String> setUpgradeHeaders(ChannelHandlerContext ctx, HttpRequest upgradeRequest);
83  
84          /**
85           * Performs an HTTP protocol upgrade from the source codec. This method is responsible for
86           * adding all handlers required for the new protocol.
87           *
88           * @param ctx the context for the current handler.
89           * @param upgradeResponse the 101 Switching Protocols response that indicates that the server
90           *            has switched to this protocol.
91           */
92          void upgradeTo(ChannelHandlerContext ctx, FullHttpResponse upgradeResponse) throws Exception;
93      }
94  
95      private final SourceCodec sourceCodec;
96      private final UpgradeCodec upgradeCodec;
97      private boolean upgradeRequested;
98  
99      /**
100      * Constructs the client upgrade handler.
101      *
102      * @param sourceCodec the codec that is being used initially.
103      * @param upgradeCodec the codec that the client would like to upgrade to.
104      * @param maxContentLength the maximum length of the aggregated content.
105      */
106     public HttpClientUpgradeHandler(SourceCodec sourceCodec, UpgradeCodec upgradeCodec,
107             int maxContentLength) {
108         super(maxContentLength);
109         if (sourceCodec == null) {
110             throw new NullPointerException("sourceCodec");
111         }
112         if (upgradeCodec == null) {
113             throw new NullPointerException("upgradeCodec");
114         }
115         this.sourceCodec = sourceCodec;
116         this.upgradeCodec = upgradeCodec;
117     }
118 
119     @Override
120     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
121             throws Exception {
122         if (!(msg instanceof HttpRequest)) {
123             super.write(ctx, msg, promise);
124             return;
125         }
126 
127         if (upgradeRequested) {
128             promise.setFailure(new IllegalStateException(
129                     "Attempting to write HTTP request with upgrade in progress"));
130             return;
131         }
132 
133         upgradeRequested = true;
134         setUpgradeRequestHeaders(ctx, (HttpRequest) msg);
135 
136         // Continue writing the request.
137         super.write(ctx, msg, promise);
138 
139         // Notify that the upgrade request was issued.
140         ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_ISSUED);
141         // Now we wait for the next HTTP response to see if we switch protocols.
142     }
143 
144     @Override
145     protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out)
146             throws Exception {
147         FullHttpResponse response = null;
148         try {
149             if (!upgradeRequested) {
150                 throw new IllegalStateException("Read HTTP response without requesting protocol switch");
151             }
152 
153             if (msg instanceof FullHttpResponse) {
154                 response = (FullHttpResponse) msg;
155                 // Need to retain since the base class will release after returning from this method.
156                 response.retain();
157                 out.add(response);
158             } else {
159                 // Call the base class to handle the aggregation of the full request.
160                 super.decode(ctx, msg, out);
161                 if (out.isEmpty()) {
162                     // The full request hasn't been created yet, still awaiting more data.
163                     return;
164                 }
165 
166                 assert out.size() == 1;
167                 response = (FullHttpResponse) out.get(0);
168             }
169 
170             if (!SWITCHING_PROTOCOLS.equals(response.status())) {
171                 // The server does not support the requested protocol, just remove this handler
172                 // and continue processing HTTP.
173                 // NOTE: not releasing the response since we're letting it propagate to the
174                 // next handler.
175                 ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_REJECTED);
176                 removeThisHandler(ctx);
177                 return;
178             }
179 
180             CharSequence upgradeHeader = response.headers().get(HttpHeaderNames.UPGRADE);
181             if (upgradeHeader == null) {
182                 throw new IllegalStateException(
183                         "Switching Protocols response missing UPGRADE header");
184             }
185             if (!AsciiString.equalsIgnoreCase(upgradeCodec.protocol(), upgradeHeader)) {
186                 throw new IllegalStateException(
187                         "Switching Protocols response with unexpected UPGRADE protocol: "
188                                 + upgradeHeader);
189             }
190 
191             // Upgrade to the new protocol.
192             sourceCodec.upgradeFrom(ctx);
193             upgradeCodec.upgradeTo(ctx, response);
194 
195             // Notify that the upgrade to the new protocol completed successfully.
196             ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_SUCCESSFUL);
197 
198             // We switched protocols, so we're done with the upgrade response.
199             // Release it and clear it from the output.
200             response.release();
201             out.clear();
202             removeThisHandler(ctx);
203         } catch (Throwable t) {
204             release(response);
205             ctx.fireExceptionCaught(t);
206             removeThisHandler(ctx);
207         }
208     }
209 
210     private static void removeThisHandler(ChannelHandlerContext ctx) {
211         ctx.pipeline().remove(ctx.name());
212     }
213 
214     /**
215      * Adds all upgrade request headers necessary for an upgrade to the supported protocols.
216      */
217     private void setUpgradeRequestHeaders(ChannelHandlerContext ctx, HttpRequest request) {
218         // Set the UPGRADE header on the request.
219         request.headers().set(HttpHeaderNames.UPGRADE, upgradeCodec.protocol());
220 
221         // Add all protocol-specific headers to the request.
222         Set<String> connectionParts = new LinkedHashSet<String>(2);
223         connectionParts.addAll(upgradeCodec.setUpgradeHeaders(ctx, request));
224 
225         // Set the CONNECTION header from the set of all protocol-specific headers that were added.
226         StringBuilder builder = new StringBuilder();
227         for (String part : connectionParts) {
228             builder.append(part);
229             builder.append(',');
230         }
231         builder.append(HttpHeaderNames.UPGRADE);
232         request.headers().set(HttpHeaderNames.CONNECTION, builder.toString());
233     }
234 }