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