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.buffer.api.BufferAllocator;
18  import io.netty5.channel.internal.DelegatingChannelHandlerContext;
19  import io.netty5.util.Resource;
20  import io.netty5.util.Send;
21  import io.netty5.channel.ChannelFutureListeners;
22  import io.netty5.channel.ChannelHandlerContext;
23  import io.netty5.util.concurrent.Future;
24  
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.List;
28  
29  import static io.netty5.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
30  import static io.netty5.handler.codec.http.HttpVersion.HTTP_1_1;
31  import static io.netty5.util.AsciiString.containsAllContentEqualsIgnoreCase;
32  import static io.netty5.util.AsciiString.containsContentEqualsIgnoreCase;
33  import static io.netty5.util.internal.StringUtil.COMMA;
34  import static java.util.Objects.requireNonNull;
35  
36  /**
37   * A server-side handler that receives HTTP requests and optionally performs a protocol switch if
38   * the requested protocol is supported. Once an upgrade is performed, this handler removes itself
39   * from the pipeline.
40   */
41  public class HttpServerUpgradeHandler<C extends HttpContent<C>> extends HttpObjectAggregator<C> {
42  
43      /**
44       * The source codec that is used in the pipeline initially.
45       */
46      public interface SourceCodec {
47          /**
48           * Removes this codec (i.e. all associated handlers) from the pipeline.
49           */
50          void upgradeFrom(ChannelHandlerContext ctx);
51      }
52  
53      /**
54       * A codec that the source can be upgraded to.
55       */
56      public interface UpgradeCodec {
57          /**
58           * Gets all protocol-specific headers required by this protocol for a successful upgrade.
59           * Any supplied header will be required to appear in the {@link HttpHeaderNames#CONNECTION} header as well.
60           */
61          Collection<CharSequence> requiredUpgradeHeaders();
62  
63          /**
64           * Prepares the {@code upgradeHeaders} for a protocol update based upon the contents of {@code upgradeRequest}.
65           * This method returns a boolean value to proceed or abort the upgrade in progress. If {@code false} is
66           * returned, the upgrade is aborted and the {@code upgradeRequest} will be passed through the inbound pipeline
67           * as if no upgrade was performed. If {@code true} is returned, the upgrade will proceed to the next
68           * step which invokes {@link #upgradeTo}. When returning {@code true}, you can add headers to
69           * the {@code upgradeHeaders} so that they are added to the 101 Switching protocols response.
70           */
71          boolean prepareUpgradeResponse(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest,
72                                      HttpHeaders upgradeHeaders);
73  
74          /**
75           * Performs an HTTP protocol upgrade from the source codec. This method is responsible for
76           * adding all handlers required for the new protocol.
77           *
78           * @param ctx the context for the current handler.
79           * @param upgradeRequest the request that triggered the upgrade to this protocol.
80           */
81          void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest);
82      }
83  
84      /**
85       * Creates a new {@link UpgradeCodec} for the requested protocol name.
86       */
87      public interface UpgradeCodecFactory {
88          /**
89           * Invoked by {@link HttpServerUpgradeHandler} for all the requested protocol names in the order of
90           * the client preference. The first non-{@code null} {@link UpgradeCodec} returned by this method
91           * will be selected.
92           *
93           * @return a new {@link UpgradeCodec}, or {@code null} if the specified protocol name is not supported
94           */
95          UpgradeCodec newUpgradeCodec(CharSequence protocol);
96      }
97  
98      /**
99       * User event that is fired to notify about the completion of an HTTP upgrade
100      * to another protocol. Contains the original upgrade request so that the response
101      * (if required) can be sent using the new protocol.
102      */
103     public static final class UpgradeEvent implements Resource<UpgradeEvent> {
104         private final CharSequence protocol;
105         private final FullHttpRequest upgradeRequest;
106 
107         UpgradeEvent(CharSequence protocol, FullHttpRequest upgradeRequest) {
108             this.protocol = protocol;
109             this.upgradeRequest = upgradeRequest;
110         }
111 
112         /**
113          * The protocol that the channel has been upgraded to.
114          */
115         public CharSequence protocol() {
116             return protocol;
117         }
118 
119         /**
120          * Gets the request that triggered the protocol upgrade.
121          */
122         public FullHttpRequest upgradeRequest() {
123             return upgradeRequest;
124         }
125 
126         @Override
127         public Send<UpgradeEvent> send() {
128             return upgradeRequest.send().map(UpgradeEvent.class, req -> new UpgradeEvent(protocol, req));
129         }
130 
131         public UpgradeEvent copy() {
132             return new UpgradeEvent(protocol, upgradeRequest.copy());
133         }
134 
135         @Override
136         public void close() {
137             upgradeRequest.close();
138         }
139 
140         @Override
141         public boolean isAccessible() {
142             return upgradeRequest.isAccessible();
143         }
144 
145         @Override
146         public UpgradeEvent touch(Object hint) {
147             upgradeRequest.touch(hint);
148             return this;
149         }
150 
151         @Override
152         public String toString() {
153             return "UpgradeEvent [protocol=" + protocol + ", upgradeRequest=" + upgradeRequest + ']';
154         }
155     }
156 
157     private final SourceCodec sourceCodec;
158     private final UpgradeCodecFactory upgradeCodecFactory;
159     private final boolean validateHeaders;
160     private boolean handlingUpgrade;
161 
162     /**
163      * Constructs the upgrader with the supported codecs.
164      * <p>
165      * The handler instantiated by this constructor will reject an upgrade request with non-empty content.
166      * It should not be a concern because an upgrade request is most likely a GET request.
167      * If you have a client that sends a non-GET upgrade request, please consider using
168      * {@link #HttpServerUpgradeHandler(SourceCodec, UpgradeCodecFactory, int)} to specify the maximum
169      * length of the content of an upgrade request.
170      * </p>
171      *
172      * @param sourceCodec the codec that is being used initially
173      * @param upgradeCodecFactory the factory that creates a new upgrade codec
174      *                            for one of the requested upgrade protocols
175      */
176     public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory) {
177         this(sourceCodec, upgradeCodecFactory, 0);
178     }
179 
180     /**
181      * Constructs the upgrader with the supported codecs.
182      *
183      * @param sourceCodec the codec that is being used initially
184      * @param upgradeCodecFactory the factory that creates a new upgrade codec
185      *                            for one of the requested upgrade protocols
186      * @param maxContentLength the maximum length of the content of an upgrade request
187      */
188     public HttpServerUpgradeHandler(
189             SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory, int maxContentLength) {
190         this(sourceCodec, upgradeCodecFactory, maxContentLength, true);
191     }
192 
193     /**
194      * Constructs the upgrader with the supported codecs.
195      *
196      * @param sourceCodec the codec that is being used initially
197      * @param upgradeCodecFactory the factory that creates a new upgrade codec
198      *                            for one of the requested upgrade protocols
199      * @param maxContentLength the maximum length of the content of an upgrade request
200      * @param validateHeaders validate the header names and values of the upgrade response.
201      */
202     public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory,
203                                     int maxContentLength, boolean validateHeaders) {
204         super(maxContentLength);
205 
206         this.sourceCodec = requireNonNull(sourceCodec, "sourceCodec");
207         this.upgradeCodecFactory = requireNonNull(upgradeCodecFactory, "upgradeCodecFactory");
208         this.validateHeaders = validateHeaders;
209     }
210 
211     @Override
212     protected void decodeAndClose(final ChannelHandlerContext ctx, HttpObject msg)
213             throws Exception {
214 
215         if (!handlingUpgrade) {
216             // Not handling an upgrade request yet. Check if we received a new upgrade request.
217             if (msg instanceof HttpRequest) {
218                 HttpRequest req = (HttpRequest) msg;
219                 if (req.headers().contains(HttpHeaderNames.UPGRADE) &&
220                     shouldHandleUpgradeRequest(req)) {
221                     handlingUpgrade = true;
222                 } else {
223                     ctx.fireChannelRead(msg);
224                     return;
225                 }
226             } else {
227                 ctx.fireChannelRead(msg);
228                 return;
229             }
230         }
231 
232         FullHttpRequest fullRequest;
233         if (msg instanceof FullHttpRequest) {
234             fullRequest = (FullHttpRequest) msg;
235             tryUpgrade(ctx, fullRequest);
236         } else {
237             // Call the base class to handle the aggregation of the full request.
238             super.decodeAndClose(new DelegatingChannelHandlerContext(ctx) {
239                 @Override
240                 public ChannelHandlerContext fireChannelRead(Object msg) {
241                     // Finished aggregating the full request, get it from the output list.
242                     handlingUpgrade = false;
243                     tryUpgrade(ctx, (FullHttpRequest) msg);
244                     return this;
245                 }
246             }, msg);
247         }
248     }
249 
250     private void tryUpgrade(ChannelHandlerContext ctx, FullHttpRequest request) {
251         if (!upgrade(ctx, request)) {
252 
253             // The upgrade did not succeed, just allow the full request to propagate to the
254             // next handler.
255             ctx.fireChannelRead(request);
256         }
257     }
258 
259     /**
260      * Determines whether the specified upgrade {@link HttpRequest} should be handled by this handler or not.
261      * This method will be invoked only when the request contains an {@code Upgrade} header.
262      * It always returns {@code true} by default, which means any request with an {@code Upgrade} header
263      * will be handled. You can override this method to ignore certain {@code Upgrade} headers, for example:
264      * <pre>{@code
265      * @Override
266      * protected boolean isUpgradeRequest(HttpRequest req) {
267      *   // Do not handle WebSocket upgrades.
268      *   return !req.headers().contains(HttpHeaderNames.UPGRADE, "websocket", false);
269      * }
270      * }</pre>
271      */
272     protected boolean shouldHandleUpgradeRequest(HttpRequest req) {
273         return true;
274     }
275 
276     /**
277      * Attempts to upgrade to the protocol(s) identified by the {@link HttpHeaderNames#UPGRADE} header (if provided
278      * in the request).
279      *
280      * @param ctx the context for this handler.
281      * @param request the HTTP request.
282      * @return {@code true} if the upgrade occurred, otherwise {@code false}.
283      */
284     private boolean upgrade(final ChannelHandlerContext ctx, final FullHttpRequest request) {
285         // Select the best protocol based on those requested in the UPGRADE header.
286         final List<CharSequence> requestedProtocols = splitHeader(request.headers().get(HttpHeaderNames.UPGRADE));
287         final int numRequestedProtocols = requestedProtocols.size();
288         UpgradeCodec upgradeCodec = null;
289         CharSequence upgradeProtocol = null;
290         for (int i = 0; i < numRequestedProtocols; i ++) {
291             final CharSequence p = requestedProtocols.get(i);
292             final UpgradeCodec c = upgradeCodecFactory.newUpgradeCodec(p);
293             if (c != null) {
294                 upgradeProtocol = p;
295                 upgradeCodec = c;
296                 break;
297             }
298         }
299 
300         if (upgradeCodec == null) {
301             // None of the requested protocols are supported, don't upgrade.
302             return false;
303         }
304 
305         // Make sure the CONNECTION header is present.
306         List<String> connectionHeaderValues = request.headers().getAll(HttpHeaderNames.CONNECTION);
307 
308         if (connectionHeaderValues == null || connectionHeaderValues.isEmpty()) {
309             return false;
310         }
311 
312         final StringBuilder concatenatedConnectionValue = new StringBuilder(connectionHeaderValues.size() * 10);
313         for (CharSequence connectionHeaderValue : connectionHeaderValues) {
314             concatenatedConnectionValue.append(connectionHeaderValue).append(COMMA);
315         }
316         concatenatedConnectionValue.setLength(concatenatedConnectionValue.length() - 1);
317 
318         // Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers.
319         Collection<CharSequence> requiredHeaders = upgradeCodec.requiredUpgradeHeaders();
320         List<CharSequence> values = splitHeader(concatenatedConnectionValue);
321         if (!containsContentEqualsIgnoreCase(values, HttpHeaderNames.UPGRADE) ||
322                 !containsAllContentEqualsIgnoreCase(values, requiredHeaders)) {
323             return false;
324         }
325 
326         // Ensure that all required protocol-specific headers are found in the request.
327         for (CharSequence requiredHeader : requiredHeaders) {
328             if (!request.headers().contains(requiredHeader)) {
329                 return false;
330             }
331         }
332 
333         // Prepare and send the upgrade response. Wait for this write to complete before upgrading,
334         // since we need the old codec in-place to properly encode the response.
335         final FullHttpResponse upgradeResponse = createUpgradeResponse(ctx.bufferAllocator(), upgradeProtocol);
336         if (!upgradeCodec.prepareUpgradeResponse(ctx, request, upgradeResponse.headers())) {
337             return false;
338         }
339 
340         // After writing the upgrade response we immediately prepare the
341         // pipeline for the next protocol to avoid a race between completion
342         // of the write future and receiving data before the pipeline is
343         // restructured.
344         Future<Void> writeComplete = ctx.writeAndFlush(upgradeResponse);
345         // Perform the upgrade to the new protocol.
346         sourceCodec.upgradeFrom(ctx);
347         upgradeCodec.upgradeTo(ctx, request);
348 
349         // Notify that the upgrade has occurred.
350         ctx.fireChannelInboundEvent(new UpgradeEvent(upgradeProtocol, request));
351 
352         // Remove this handler from the pipeline.
353         ctx.pipeline().remove(HttpServerUpgradeHandler.this);
354 
355         // Add the listener last to avoid firing upgrade logic after
356         // the channel is already closed since the listener may fire
357         // immediately if the write failed eagerly.
358         writeComplete.addListener(ctx.channel(), ChannelFutureListeners.CLOSE_ON_FAILURE);
359         return true;
360     }
361 
362     /**
363      * Creates the 101 Switching Protocols response message.
364      */
365     private FullHttpResponse createUpgradeResponse(BufferAllocator allocator, CharSequence upgradeProtocol) {
366         DefaultFullHttpResponse res = new DefaultFullHttpResponse(
367                 HTTP_1_1, SWITCHING_PROTOCOLS, allocator.allocate(0), validateHeaders);
368         res.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE);
369         res.headers().add(HttpHeaderNames.UPGRADE, upgradeProtocol);
370         return res;
371     }
372 
373     /**
374      * Splits a comma-separated header value. The returned set is case-insensitive and contains each
375      * part with whitespace removed.
376      */
377     private static List<CharSequence> splitHeader(CharSequence header) {
378         final StringBuilder builder = new StringBuilder(header.length());
379         final List<CharSequence> protocols = new ArrayList<>(4);
380         for (int i = 0; i < header.length(); ++i) {
381             char c = header.charAt(i);
382             if (Character.isWhitespace(c)) {
383                 // Don't include any whitespace.
384                 continue;
385             }
386             if (c == ',') {
387                 // Add the string and reset the builder for the next protocol.
388                 protocols.add(builder.toString());
389                 builder.setLength(0);
390             } else {
391                 builder.append(c);
392             }
393         }
394 
395         // Add the last protocol
396         if (builder.length() > 0) {
397             protocols.add(builder.toString());
398         }
399 
400         return protocols;
401     }
402 }