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    * https://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.ChannelOutboundHandler;
19  import io.netty.channel.ChannelPromise;
20  import io.netty.util.AsciiString;
21  import io.netty.util.ReferenceCountUtil;
22  import io.netty.util.internal.ObjectUtil;
23  
24  import java.net.SocketAddress;
25  import java.util.Collection;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Set;
29  
30  import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
31  import static io.netty.util.ReferenceCountUtil.release;
32  
33  /**
34   * Client-side handler for handling an HTTP upgrade handshake to another protocol. When the first
35   * HTTP request is sent, this handler will add all appropriate headers to perform an upgrade to the
36   * new protocol. If the upgrade fails (i.e. response is not 101 Switching Protocols), this handler
37   * simply removes itself from the pipeline. If the upgrade is successful, upgrades the pipeline to
38   * the new protocol.
39   */
40  public class HttpClientUpgradeHandler extends HttpObjectAggregator implements ChannelOutboundHandler {
41  
42      /**
43       * User events that are fired to notify about upgrade status.
44       */
45      public enum UpgradeEvent {
46          /**
47           * The Upgrade request was sent to the server.
48           */
49          UPGRADE_ISSUED,
50  
51          /**
52           * The Upgrade to the new protocol was successful.
53           */
54          UPGRADE_SUCCESSFUL,
55  
56          /**
57           * The Upgrade was unsuccessful due to the server not issuing
58           * with a 101 Switching Protocols response.
59           */
60          UPGRADE_REJECTED
61      }
62  
63      /**
64       * The source codec that is used in the pipeline initially.
65       */
66      public interface SourceCodec {
67  
68          /**
69           * Removes or disables the encoder of this codec so that the {@link UpgradeCodec} can send an initial greeting
70           * (if any).
71           */
72          void prepareUpgradeFrom(ChannelHandlerContext ctx);
73  
74          /**
75           * Removes this codec (i.e. all associated handlers) from the pipeline.
76           */
77          void upgradeFrom(ChannelHandlerContext ctx);
78      }
79  
80      /**
81       * A codec that the source can be upgraded to.
82       */
83      public interface UpgradeCodec {
84          /**
85           * Returns the name of the protocol supported by this codec, as indicated by the {@code 'UPGRADE'} header.
86           */
87          CharSequence protocol();
88  
89          /**
90           * Sets any protocol-specific headers required to the upgrade request. Returns the names of
91           * all headers that were added. These headers will be used to populate the CONNECTION header.
92           */
93          Collection<CharSequence> setUpgradeHeaders(ChannelHandlerContext ctx, HttpRequest upgradeRequest);
94  
95          /**
96           * Performs an HTTP protocol upgrade from the source codec. This method is responsible for
97           * adding all handlers required for the new protocol.
98           *
99           * @param ctx the context for the current handler.
100          * @param upgradeResponse the 101 Switching Protocols response that indicates that the server
101          *            has switched to this protocol.
102          */
103         void upgradeTo(ChannelHandlerContext ctx, FullHttpResponse upgradeResponse) throws Exception;
104     }
105 
106     private final SourceCodec sourceCodec;
107     private final UpgradeCodec upgradeCodec;
108     private UpgradeEvent currentUpgradeEvent;
109 
110     /**
111      * Constructs the client upgrade handler.
112      *
113      * @param sourceCodec the codec that is being used initially.
114      * @param upgradeCodec the codec that the client would like to upgrade to.
115      * @param maxContentLength the maximum length of the aggregated content.
116      */
117     public HttpClientUpgradeHandler(SourceCodec sourceCodec, UpgradeCodec upgradeCodec,
118                                     int maxContentLength) {
119         super(maxContentLength);
120         this.sourceCodec = ObjectUtil.checkNotNull(sourceCodec, "sourceCodec");
121         this.upgradeCodec = ObjectUtil.checkNotNull(upgradeCodec, "upgradeCodec");
122     }
123 
124     @Override
125     public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
126         ctx.bind(localAddress, promise);
127     }
128 
129     @Override
130     public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
131                         ChannelPromise promise) throws Exception {
132         ctx.connect(remoteAddress, localAddress, promise);
133     }
134 
135     @Override
136     public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
137         ctx.disconnect(promise);
138     }
139 
140     @Override
141     public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
142         ctx.close(promise);
143     }
144 
145     @Override
146     public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
147         ctx.deregister(promise);
148     }
149 
150     @Override
151     public void read(ChannelHandlerContext ctx) throws Exception {
152         ctx.read();
153     }
154 
155     @Override
156     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
157             throws Exception {
158         if (!(msg instanceof HttpRequest) || currentUpgradeEvent == UpgradeEvent.UPGRADE_SUCCESSFUL) {
159             ctx.write(msg, promise);
160             return;
161         }
162 
163         if (currentUpgradeEvent == UpgradeEvent.UPGRADE_ISSUED) {
164             // Release message before failing the promise.
165             ReferenceCountUtil.release(msg);
166             promise.setFailure(new IllegalStateException(
167                     "Attempting to write HTTP request with upgrade in progress"));
168             return;
169         }
170 
171         currentUpgradeEvent = UpgradeEvent.UPGRADE_ISSUED;
172         setUpgradeRequestHeaders(ctx, (HttpRequest) msg);
173 
174         // Continue writing the request.
175         ctx.write(msg, promise);
176 
177         // Notify that the upgrade request was issued.
178         ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_ISSUED);
179         // Now we wait for the next HTTP response to see if we switch protocols.
180     }
181 
182     @Override
183     public void flush(ChannelHandlerContext ctx) throws Exception {
184         ctx.flush();
185     }
186 
187     @Override
188     protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out)
189             throws Exception {
190         FullHttpResponse response = null;
191         try {
192             if (currentUpgradeEvent != UpgradeEvent.UPGRADE_ISSUED) {
193                 throw new IllegalStateException("Read HTTP response without requesting protocol switch");
194             }
195 
196             if (msg instanceof HttpResponse) {
197                 HttpResponse rep = (HttpResponse) msg;
198                 if (!SWITCHING_PROTOCOLS.equals(rep.status())) {
199                     // The server does not support the requested protocol, just remove this handler
200                     // and continue processing HTTP.
201                     // NOTE: not releasing the response since we're letting it propagate to the
202                     // next handler.
203                     currentUpgradeEvent = null;
204                     ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_REJECTED);
205                     removeThisHandler(ctx);
206                     ctx.fireChannelRead(msg);
207                     return;
208                 }
209             }
210 
211             if (msg instanceof FullHttpResponse) {
212                 response = (FullHttpResponse) msg;
213                 // Need to retain since the base class will release after returning from this method.
214                 response.retain();
215                 out.add(response);
216             } else {
217                 // Call the base class to handle the aggregation of the full request.
218                 super.decode(ctx, msg, out);
219                 if (out.isEmpty()) {
220                     // The full request hasn't been created yet, still awaiting more data.
221                     return;
222                 }
223 
224                 assert out.size() == 1;
225                 response = (FullHttpResponse) out.get(0);
226             }
227 
228             CharSequence upgradeHeader = response.headers().get(HttpHeaderNames.UPGRADE);
229             if (upgradeHeader != null && !AsciiString.contentEqualsIgnoreCase(upgradeCodec.protocol(), upgradeHeader)) {
230                 throw new IllegalStateException(
231                         "Switching Protocols response with unexpected UPGRADE protocol: " + upgradeHeader);
232             }
233 
234             // Upgrade to the new protocol.
235             sourceCodec.prepareUpgradeFrom(ctx);
236             upgradeCodec.upgradeTo(ctx, response);
237 
238             // Let's set currentUpgradeEvent to UPGRADE_SUCCESSFUL as we will notify the pipeline about the successful
239             // upgrade now, which might results in a write that needs to be passed through.
240             currentUpgradeEvent = UpgradeEvent.UPGRADE_SUCCESSFUL;
241 
242             // Notify that the upgrade to the new protocol completed successfully.
243             ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_SUCCESSFUL);
244 
245             // We guarantee UPGRADE_SUCCESSFUL event will be arrived at the next handler
246             // before http2 setting frame and http response.
247             sourceCodec.upgradeFrom(ctx);
248 
249             // We switched protocols, so we're done with the upgrade response.
250             // Release it and clear it from the output.
251             response.release();
252             out.clear();
253             removeThisHandler(ctx);
254         } catch (Throwable t) {
255             release(response);
256             ctx.fireExceptionCaught(t);
257             removeThisHandler(ctx);
258         }
259     }
260 
261     private static void removeThisHandler(ChannelHandlerContext ctx) {
262         ctx.pipeline().remove(ctx.name());
263     }
264 
265     /**
266      * Adds all upgrade request headers necessary for an upgrade to the supported protocols.
267      */
268     private void setUpgradeRequestHeaders(ChannelHandlerContext ctx, HttpRequest request) {
269         // Set the UPGRADE header on the request.
270         request.headers().set(HttpHeaderNames.UPGRADE, upgradeCodec.protocol());
271 
272         // Add all protocol-specific headers to the request.
273         Set<CharSequence> connectionParts = new LinkedHashSet<CharSequence>(2);
274         connectionParts.addAll(upgradeCodec.setUpgradeHeaders(ctx, request));
275 
276         // Set the CONNECTION header from the set of all protocol-specific headers that were added.
277         StringBuilder builder = new StringBuilder();
278         for (CharSequence part : connectionParts) {
279             builder.append(part);
280             builder.append(',');
281         }
282         builder.append(HttpHeaderValues.UPGRADE);
283         request.headers().add(HttpHeaderNames.CONNECTION, builder.toString());
284     }
285 }