1 /*
2 * Copyright 2015 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a 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
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.handler.ssl;
17
18 import io.netty.channel.Channel;
19 import io.netty.channel.ChannelHandlerContext;
20 import io.netty.channel.ChannelInboundHandlerAdapter;
21 import io.netty.channel.ChannelInitializer;
22 import io.netty.channel.ChannelPipeline;
23 import io.netty.channel.socket.ChannelInputShutdownEvent;
24 import io.netty.handler.codec.DecoderException;
25 import io.netty.util.internal.ObjectUtil;
26 import io.netty.util.internal.RecyclableArrayList;
27 import io.netty.util.internal.logging.InternalLogger;
28 import io.netty.util.internal.logging.InternalLoggerFactory;
29
30 import javax.net.ssl.SSLException;
31
32 /**
33 * Configures a {@link ChannelPipeline} depending on the application-level protocol negotiation result of
34 * {@link SslHandler}. For example, you could configure your HTTP pipeline depending on the result of ALPN:
35 * <pre>
36 * public class MyInitializer extends {@link ChannelInitializer}<{@link Channel}> {
37 * private final {@link SslContext} sslCtx;
38 *
39 * public MyInitializer({@link SslContext} sslCtx) {
40 * this.sslCtx = sslCtx;
41 * }
42 *
43 * protected void initChannel({@link Channel} ch) {
44 * {@link ChannelPipeline} p = ch.pipeline();
45 * p.addLast(sslCtx.newHandler(...)); // Adds {@link SslHandler}
46 * p.addLast(new MyNegotiationHandler());
47 * }
48 * }
49 *
50 * public class MyNegotiationHandler extends {@link ApplicationProtocolNegotiationHandler} {
51 * public MyNegotiationHandler() {
52 * super({@link ApplicationProtocolNames}.HTTP_1_1);
53 * }
54 *
55 * protected void configurePipeline({@link ChannelHandlerContext} ctx, String protocol) {
56 * if ({@link ApplicationProtocolNames}.HTTP_2.equals(protocol) {
57 * configureHttp2(ctx);
58 * } else if ({@link ApplicationProtocolNames}.HTTP_1_1.equals(protocol)) {
59 * configureHttp1(ctx);
60 * } else {
61 * throw new IllegalStateException("unknown protocol: " + protocol);
62 * }
63 * }
64 * }
65 * </pre>
66 */
67 public abstract class ApplicationProtocolNegotiationHandler extends ChannelInboundHandlerAdapter {
68
69 private static final InternalLogger logger =
70 InternalLoggerFactory.getInstance(ApplicationProtocolNegotiationHandler.class);
71
72 private final String fallbackProtocol;
73 private final RecyclableArrayList bufferedMessages = RecyclableArrayList.newInstance();
74 private ChannelHandlerContext ctx;
75 private boolean sslHandlerChecked;
76
77 /**
78 * Creates a new instance with the specified fallback protocol name.
79 *
80 * @param fallbackProtocol the name of the protocol to use when
81 * ALPN/NPN negotiation fails or the client does not support ALPN/NPN
82 */
83 protected ApplicationProtocolNegotiationHandler(String fallbackProtocol) {
84 this.fallbackProtocol = ObjectUtil.checkNotNull(fallbackProtocol, "fallbackProtocol");
85 }
86
87 @Override
88 public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
89 this.ctx = ctx;
90 super.handlerAdded(ctx);
91 }
92
93 @Override
94 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
95 fireBufferedMessages();
96 bufferedMessages.recycle();
97 super.handlerRemoved(ctx);
98 }
99
100 @Override
101 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
102 // Let's buffer all data until this handler will be removed from the pipeline.
103 bufferedMessages.add(msg);
104 if (!sslHandlerChecked) {
105 sslHandlerChecked = true;
106 if (ctx.pipeline().get(SslHandler.class) == null) {
107 // Just remove ourself if there is no SslHandler in the pipeline and so we would otherwise
108 // buffer forever.
109 removeSelfIfPresent(ctx);
110 }
111 }
112 }
113
114 /**
115 * Process all backlog into pipeline from List.
116 */
117 private void fireBufferedMessages() {
118 if (!bufferedMessages.isEmpty()) {
119 for (int i = 0; i < bufferedMessages.size(); i++) {
120 ctx.fireChannelRead(bufferedMessages.get(i));
121 }
122 ctx.fireChannelReadComplete();
123 bufferedMessages.clear();
124 }
125 }
126
127 @Override
128 public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
129 if (evt instanceof SslHandshakeCompletionEvent) {
130 SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) evt;
131 try {
132 if (handshakeEvent.isSuccess()) {
133 SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
134 if (sslHandler == null) {
135 throw new IllegalStateException("cannot find an SslHandler in the pipeline (required for "
136 + "application-level protocol negotiation)");
137 }
138 String protocol = sslHandler.applicationProtocol();
139 configurePipeline(ctx, protocol != null ? protocol : fallbackProtocol);
140 } else {
141 // if the event is not produced because of an successful handshake we will receive the same
142 // exception in exceptionCaught(...) and handle it there. This will allow us more fine-grained
143 // control over which exception we propagate down the ChannelPipeline.
144 //
145 // See https://github.com/netty/netty/issues/10342
146 }
147 } catch (Throwable cause) {
148 exceptionCaught(ctx, cause);
149 } finally {
150 // Handshake failures are handled in exceptionCaught(...).
151 if (handshakeEvent.isSuccess()) {
152 removeSelfIfPresent(ctx);
153 }
154 }
155 }
156
157 if (evt instanceof ChannelInputShutdownEvent) {
158 fireBufferedMessages();
159 }
160
161 ctx.fireUserEventTriggered(evt);
162 }
163
164 @Override
165 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
166 fireBufferedMessages();
167 super.channelInactive(ctx);
168 }
169
170 private void removeSelfIfPresent(ChannelHandlerContext ctx) {
171 ChannelPipeline pipeline = ctx.pipeline();
172 if (!ctx.isRemoved()) {
173 pipeline.remove(this);
174 }
175 }
176
177 /**
178 * Invoked on successful initial SSL/TLS handshake. Implement this method to configure your pipeline
179 * for the negotiated application-level protocol.
180 *
181 * @param protocol the name of the negotiated application-level protocol, or
182 * the fallback protocol name specified in the constructor call if negotiation failed or the client
183 * isn't aware of ALPN/NPN extension
184 */
185 protected abstract void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception;
186
187 /**
188 * Invoked on failed initial SSL/TLS handshake.
189 */
190 protected void handshakeFailure(ChannelHandlerContext ctx, Throwable cause) throws Exception {
191 logger.warn("{} TLS handshake failed:", ctx.channel(), cause);
192 ctx.close();
193 }
194
195 @Override
196 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
197 Throwable wrapped;
198 if (cause instanceof DecoderException && ((wrapped = cause.getCause()) instanceof SSLException)) {
199 try {
200 handshakeFailure(ctx, wrapped);
201 return;
202 } finally {
203 removeSelfIfPresent(ctx);
204 }
205 }
206 logger.warn("{} Failed to select the application-level protocol:", ctx.channel(), cause);
207 ctx.fireExceptionCaught(cause);
208 ctx.close();
209 }
210 }