View Javadoc
1   /*
2    * Copyright 2014 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    *   http://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.ChannelHandlerContext;
19  import io.netty.handler.codec.DecoderException;
20  import io.netty.util.AsyncMapping;
21  import io.netty.util.DomainNameMapping;
22  import io.netty.util.Mapping;
23  import io.netty.util.ReferenceCountUtil;
24  import io.netty.util.concurrent.Future;
25  import io.netty.util.concurrent.Promise;
26  import io.netty.util.internal.ObjectUtil;
27  import io.netty.util.internal.PlatformDependent;
28  
29  /**
30   * <p>Enables <a href="https://tools.ietf.org/html/rfc3546#section-3.1">SNI
31   * (Server Name Indication)</a> extension for server side SSL. For clients
32   * support SNI, the server could have multiple host name bound on a single IP.
33   * The client will send host name in the handshake data so server could decide
34   * which certificate to choose for the host name.</p>
35   */
36  public class SniHandler extends AbstractSniHandler<SslContext> {
37      private static final Selection EMPTY_SELECTION = new Selection(null, null);
38  
39      protected final AsyncMapping<String, SslContext> mapping;
40  
41      private volatile Selection selection = EMPTY_SELECTION;
42  
43      /**
44       * Creates a SNI detection handler with configured {@link SslContext}
45       * maintained by {@link Mapping}
46       *
47       * @param mapping the mapping of domain name to {@link SslContext}
48       */
49      public SniHandler(Mapping<? super String, ? extends SslContext> mapping) {
50          this(new AsyncMappingAdapter(mapping));
51      }
52  
53      /**
54       * Creates a SNI detection handler with configured {@link SslContext}
55       * maintained by {@link DomainNameMapping}
56       *
57       * @param mapping the mapping of domain name to {@link SslContext}
58       */
59      public SniHandler(DomainNameMapping<? extends SslContext> mapping) {
60          this((Mapping<String, ? extends SslContext>) mapping);
61      }
62  
63      /**
64       * Creates a SNI detection handler with configured {@link SslContext}
65       * maintained by {@link AsyncMapping}
66       *
67       * @param mapping the mapping of domain name to {@link SslContext}
68       */
69      @SuppressWarnings("unchecked")
70      public SniHandler(AsyncMapping<? super String, ? extends SslContext> mapping) {
71          this.mapping = (AsyncMapping<String, SslContext>) ObjectUtil.checkNotNull(mapping, "mapping");
72      }
73  
74      /**
75       * @return the selected hostname
76       */
77      public String hostname() {
78          return selection.hostname;
79      }
80  
81      /**
82       * @return the selected {@link SslContext}
83       */
84      public SslContext sslContext() {
85          return selection.context;
86      }
87  
88      /**
89       * The default implementation will simply call {@link AsyncMapping#map(Object, Promise)} but
90       * users can override this method to implement custom behavior.
91       *
92       * @see AsyncMapping#map(Object, Promise)
93       */
94      @Override
95      protected Future<SslContext> lookup(ChannelHandlerContext ctx, String hostname) throws Exception {
96          return mapping.map(hostname, ctx.executor().<SslContext>newPromise());
97      }
98  
99      @Override
100     protected final void onLookupComplete(ChannelHandlerContext ctx,
101                                           String hostname, Future<SslContext> future) throws Exception {
102         if (!future.isSuccess()) {
103             final Throwable cause = future.cause();
104             if (cause instanceof Error) {
105                 throw (Error) cause;
106             }
107             throw new DecoderException("failed to get the SslContext for " + hostname, cause);
108         }
109 
110         SslContext sslContext = future.getNow();
111         selection = new Selection(sslContext, hostname);
112         try {
113             replaceHandler(ctx, hostname, sslContext);
114         } catch (Throwable cause) {
115             selection = EMPTY_SELECTION;
116             PlatformDependent.throwException(cause);
117         }
118     }
119 
120     /**
121      * The default implementation of this method will simply replace {@code this} {@link SniHandler}
122      * instance with a {@link SslHandler}. Users may override this method to implement custom behavior.
123      *
124      * Please be aware that this method may get called after a client has already disconnected and
125      * custom implementations must take it into consideration when overriding this method.
126      *
127      * It's also possible for the hostname argument to be {@code null}.
128      */
129     protected void replaceHandler(ChannelHandlerContext ctx, String hostname, SslContext sslContext) throws Exception {
130         SslHandler sslHandler = null;
131         try {
132             sslHandler = sslContext.newHandler(ctx.alloc());
133             ctx.pipeline().replace(this, SslHandler.class.getName(), sslHandler);
134             sslHandler = null;
135         } finally {
136             // Since the SslHandler was not inserted into the pipeline the ownership of the SSLEngine was not
137             // transferred to the SslHandler.
138             // See https://github.com/netty/netty/issues/5678
139             if (sslHandler != null) {
140                 ReferenceCountUtil.safeRelease(sslHandler.engine());
141             }
142         }
143     }
144 
145     private static final class AsyncMappingAdapter implements AsyncMapping<String, SslContext> {
146         private final Mapping<? super String, ? extends SslContext> mapping;
147 
148         private AsyncMappingAdapter(Mapping<? super String, ? extends SslContext> mapping) {
149             this.mapping = ObjectUtil.checkNotNull(mapping, "mapping");
150         }
151 
152         @Override
153         public Future<SslContext> map(String input, Promise<SslContext> promise) {
154             final SslContext context;
155             try {
156                 context = mapping.map(input);
157             } catch (Throwable cause) {
158                 return promise.setFailure(cause);
159             }
160             return promise.setSuccess(context);
161         }
162     }
163 
164     private static final class Selection {
165         final SslContext context;
166         final String hostname;
167 
168         Selection(SslContext context, String hostname) {
169             this.context = context;
170             this.hostname = hostname;
171         }
172     }
173 }