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