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.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}&lt;{@link Channel}&gt; {
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 }