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.buffer.ByteBuf;
19  import io.netty.buffer.ByteBufUtil;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.handler.codec.ByteToMessageDecoder;
22  import io.netty.util.CharsetUtil;
23  import io.netty.util.DomainNameMapping;
24  import io.netty.util.internal.logging.InternalLogger;
25  import io.netty.util.internal.logging.InternalLoggerFactory;
26  
27  import java.net.IDN;
28  import java.util.List;
29  import java.util.Locale;
30  
31  /**
32   * <p>Enables <a href="https://tools.ietf.org/html/rfc3546#section-3.1">SNI
33   * (Server Name Indication)</a> extension for server side SSL. For clients
34   * support SNI, the server could have multiple host name bound on a single IP.
35   * The client will send host name in the handshake data so server could decide
36   * which certificate to choose for the host name. </p>
37   */
38  public class SniHandler extends ByteToMessageDecoder {
39  
40      private static final InternalLogger logger =
41              InternalLoggerFactory.getInstance(SniHandler.class);
42  
43      private final DomainNameMapping<SslContext> mapping;
44  
45      private boolean handshaken;
46      private volatile String hostname;
47      private volatile SslContext selectedContext;
48  
49      /**
50       * Create a SNI detection handler with configured {@link SslContext}
51       * maintained by {@link DomainNameMapping}
52       *
53       * @param mapping the mapping of domain name to {@link SslContext}
54       */
55      @SuppressWarnings("unchecked")
56      public SniHandler(DomainNameMapping<? extends SslContext> mapping) {
57          if (mapping == null) {
58              throw new NullPointerException("mapping");
59          }
60  
61          this.mapping = (DomainNameMapping<SslContext>) mapping;
62          handshaken = false;
63      }
64  
65      /**
66       * @return the selected hostname
67       */
68      public String hostname() {
69          return hostname;
70      }
71  
72      /**
73       * @return the selected sslcontext
74       */
75      public SslContext sslContext() {
76          return selectedContext;
77      }
78  
79      @Override
80      protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
81          if (!handshaken && in.readableBytes() >= 5) {
82              String hostname = sniHostNameFromHandshakeInfo(in);
83              if (hostname != null) {
84                  hostname = IDN.toASCII(hostname, IDN.ALLOW_UNASSIGNED).toLowerCase(Locale.US);
85              }
86              this.hostname = hostname;
87  
88              // the mapping will return default context when this.hostname is null
89              selectedContext = mapping.map(hostname);
90          }
91  
92          if (handshaken) {
93              SslHandler sslHandler = selectedContext.newHandler(ctx.alloc());
94              ctx.pipeline().replace(this, SslHandler.class.getName(), sslHandler);
95          }
96      }
97  
98      private String sniHostNameFromHandshakeInfo(ByteBuf in) {
99          int readerIndex = in.readerIndex();
100         try {
101             int command = in.getUnsignedByte(readerIndex);
102 
103             // tls, but not handshake command
104             switch (command) {
105                 case SslConstants.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC:
106                 case SslConstants.SSL_CONTENT_TYPE_ALERT:
107                 case SslConstants.SSL_CONTENT_TYPE_APPLICATION_DATA:
108                     return null;
109                 case SslConstants.SSL_CONTENT_TYPE_HANDSHAKE:
110                     break;
111                 default:
112                     //not tls or sslv3, do not try sni
113                     handshaken = true;
114                     return null;
115             }
116 
117             int majorVersion = in.getUnsignedByte(readerIndex + 1);
118 
119             // SSLv3 or TLS
120             if (majorVersion == 3) {
121 
122                 int packetLength = in.getUnsignedShort(readerIndex + 3) + 5;
123 
124                 if (in.readableBytes() >= packetLength) {
125                     // decode the ssl client hello packet
126                     // we have to skip some var-length fields
127                     int offset = readerIndex + 43;
128 
129                     int sessionIdLength = in.getUnsignedByte(offset);
130                     offset += sessionIdLength + 1;
131 
132                     int cipherSuitesLength = in.getUnsignedShort(offset);
133                     offset += cipherSuitesLength + 2;
134 
135                     int compressionMethodLength = in.getUnsignedByte(offset);
136                     offset += compressionMethodLength + 1;
137 
138                     int extensionsLength = in.getUnsignedShort(offset);
139                     offset += 2;
140                     int extensionsLimit = offset + extensionsLength;
141 
142                     while (offset < extensionsLimit) {
143                         int extensionType = in.getUnsignedShort(offset);
144                         offset += 2;
145 
146                         int extensionLength = in.getUnsignedShort(offset);
147                         offset += 2;
148 
149                         // SNI
150                         if (extensionType == 0) {
151                             handshaken = true;
152                             int serverNameType = in.getUnsignedByte(offset + 2);
153                             if (serverNameType == 0) {
154                                 int serverNameLength = in.getUnsignedShort(offset + 3);
155                                 return in.toString(offset + 5, serverNameLength,
156                                         CharsetUtil.UTF_8);
157                             } else {
158                                 // invalid enum value
159                                 return null;
160                             }
161                         }
162 
163                         offset += extensionLength;
164                     }
165 
166                     handshaken = true;
167                     return null;
168                 } else {
169                     // client hello incomplete
170                     return null;
171                 }
172             } else {
173                 handshaken = true;
174                 return null;
175             }
176         } catch (Throwable e) {
177             // unexpected encoding, ignore sni and use default
178             if (logger.isDebugEnabled()) {
179                 logger.debug("Unexpected client hello packet: " + ByteBufUtil.hexDump(in), e);
180             }
181             handshaken = true;
182             return null;
183         }
184     }
185 }