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 }