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.ReferenceCountUtil;
25 import io.netty.util.internal.PlatformDependent;
26 import io.netty.util.internal.logging.InternalLogger;
27 import io.netty.util.internal.logging.InternalLoggerFactory;
28
29 import java.net.IDN;
30 import java.util.List;
31 import java.util.Locale;
32
33 /**
34 * <p>Enables <a href="https://tools.ietf.org/html/rfc3546#section-3.1">SNI
35 * (Server Name Indication)</a> extension for server side SSL. For clients
36 * support SNI, the server could have multiple host name bound on a single IP.
37 * The client will send host name in the handshake data so server could decide
38 * which certificate to choose for the host name. </p>
39 */
40 public class SniHandler extends ByteToMessageDecoder {
41
42 // Maximal number of ssl records to inspect before fallback to the default SslContext.
43 private static final int MAX_SSL_RECORDS = 4;
44
45 private static final InternalLogger logger =
46 InternalLoggerFactory.getInstance(SniHandler.class);
47
48 private static final Selection EMPTY_SELECTION = new Selection(null, null);
49
50 private final DomainNameMapping<SslContext> mapping;
51
52 private boolean handshakeFailed;
53
54 private volatile Selection selection = EMPTY_SELECTION;
55
56 /**
57 * Create a SNI detection handler with configured {@link SslContext}
58 * maintained by {@link DomainNameMapping}
59 *
60 * @param mapping the mapping of domain name to {@link SslContext}
61 */
62 @SuppressWarnings("unchecked")
63 public SniHandler(DomainNameMapping<? extends SslContext> mapping) {
64 if (mapping == null) {
65 throw new NullPointerException("mapping");
66 }
67
68 this.mapping = (DomainNameMapping<SslContext>) mapping;
69 }
70
71 /**
72 * @return the selected hostname
73 */
74 public String hostname() {
75 return selection.hostname;
76 }
77
78 /**
79 * @return the selected sslcontext
80 */
81 public SslContext sslContext() {
82 return selection.context;
83 }
84
85 @Override
86 protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
87 if (!handshakeFailed) {
88 final int writerIndex = in.writerIndex();
89 try {
90 loop:
91 for (int i = 0; i < MAX_SSL_RECORDS; i++) {
92 final int readerIndex = in.readerIndex();
93 final int readableBytes = writerIndex - readerIndex;
94 if (readableBytes < SslUtils.SSL_RECORD_HEADER_LENGTH) {
95 // Not enough data to determine the record type and length.
96 return;
97 }
98
99 final int command = in.getUnsignedByte(readerIndex);
100
101 // tls, but not handshake command
102 switch (command) {
103 case SslUtils.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC:
104 case SslUtils.SSL_CONTENT_TYPE_ALERT:
105 final int len = SslUtils.getEncryptedPacketLength(in, readerIndex);
106
107 // Not an SSL/TLS packet
108 if (len == SslUtils.NOT_ENCRYPTED) {
109 handshakeFailed = true;
110 NotSslRecordException e = new NotSslRecordException(
111 "not an SSL/TLS record: " + ByteBufUtil.hexDump(in));
112 in.skipBytes(in.readableBytes());
113
114 SslUtils.notifyHandshakeFailure(ctx, e, true);
115 throw e;
116 }
117 if (len == SslUtils.NOT_ENOUGH_DATA ||
118 writerIndex - readerIndex - SslUtils.SSL_RECORD_HEADER_LENGTH < len) {
119 // Not enough data
120 return;
121 }
122 // increase readerIndex and try again.
123 in.skipBytes(len);
124 continue;
125 case SslUtils.SSL_CONTENT_TYPE_HANDSHAKE:
126 final int majorVersion = in.getUnsignedByte(readerIndex + 1);
127
128 // SSLv3 or TLS
129 if (majorVersion == 3) {
130 final int packetLength = in.getUnsignedShort(readerIndex + 3) +
131 SslUtils.SSL_RECORD_HEADER_LENGTH;
132
133 if (readableBytes < packetLength) {
134 // client hello incomplete; try again to decode once more data is ready.
135 return;
136 }
137
138 // See https://tools.ietf.org/html/rfc5246#section-7.4.1.2
139 //
140 // Decode the ssl client hello packet.
141 // We have to skip bytes until SessionID (which sum to 43 bytes).
142 //
143 // struct {
144 // ProtocolVersion client_version;
145 // Random random;
146 // SessionID session_id;
147 // CipherSuite cipher_suites<2..2^16-2>;
148 // CompressionMethod compression_methods<1..2^8-1>;
149 // select (extensions_present) {
150 // case false:
151 // struct {};
152 // case true:
153 // Extension extensions<0..2^16-1>;
154 // };
155 // } ClientHello;
156 //
157
158 final int endOffset = readerIndex + packetLength;
159 int offset = readerIndex + 43;
160
161 if (endOffset - offset < 6) {
162 break loop;
163 }
164
165 final int sessionIdLength = in.getUnsignedByte(offset);
166 offset += sessionIdLength + 1;
167
168 final int cipherSuitesLength = in.getUnsignedShort(offset);
169 offset += cipherSuitesLength + 2;
170
171 final int compressionMethodLength = in.getUnsignedByte(offset);
172 offset += compressionMethodLength + 1;
173
174 final int extensionsLength = in.getUnsignedShort(offset);
175 offset += 2;
176 final int extensionsLimit = offset + extensionsLength;
177
178 if (extensionsLimit > endOffset) {
179 // Extensions should never exceed the record boundary.
180 break loop;
181 }
182
183 for (;;) {
184 if (extensionsLimit - offset < 4) {
185 break loop;
186 }
187
188 final int extensionType = in.getUnsignedShort(offset);
189 offset += 2;
190
191 final int extensionLength = in.getUnsignedShort(offset);
192 offset += 2;
193
194 if (extensionsLimit - offset < extensionLength) {
195 break loop;
196 }
197
198 // SNI
199 // See https://tools.ietf.org/html/rfc6066#page-6
200 if (extensionType == 0) {
201 offset += 2;
202 if (extensionsLimit - offset < 3) {
203 break loop;
204 }
205
206 final int serverNameType = in.getUnsignedByte(offset);
207 offset++;
208
209 if (serverNameType == 0) {
210 final int serverNameLength = in.getUnsignedShort(offset);
211 offset += 2;
212
213 if (extensionsLimit - offset < serverNameLength) {
214 break loop;
215 }
216
217 final String hostname = in.toString(offset, serverNameLength,
218 CharsetUtil.UTF_8);
219
220 try {
221 select(ctx, IDN.toASCII(hostname,
222 IDN.ALLOW_UNASSIGNED).toLowerCase(Locale.US));
223 } catch (Throwable t) {
224 PlatformDependent.throwException(t);
225 }
226 return;
227 } else {
228 // invalid enum value
229 break loop;
230 }
231 }
232
233 offset += extensionLength;
234 }
235 }
236 // Fall-through
237 default:
238 //not tls, ssl or application data, do not try sni
239 break loop;
240 }
241 }
242 } catch (NotSslRecordException e) {
243 // Just rethrow as in this case we also closed the channel and this is consistent with SslHandler.
244 throw e;
245 } catch (Exception e) {
246 // unexpected encoding, ignore sni and use default
247 if (logger.isDebugEnabled()) {
248 logger.debug("Unexpected client hello packet: " + ByteBufUtil.hexDump(in), e);
249 }
250 }
251 // Just select the default SslContext
252 select(ctx, null);
253 }
254 }
255
256 private void select(ChannelHandlerContext ctx, String hostname) {
257 SslHandler sslHandler = null;
258 SslContext selectedContext = mapping.map(hostname);
259 selection = new Selection(selectedContext, hostname);
260 try {
261 sslHandler = selection.context.newHandler(ctx.alloc());
262 ctx.pipeline().replace(this, SslHandler.class.getName(), sslHandler);
263 } catch (Throwable cause) {
264 selection = EMPTY_SELECTION;
265 // Since the SslHandler was not inserted into the pipeline the ownership of the SSLEngine was not
266 // transferred to the SslHandler.
267 // See https://github.com/netty/netty/issues/5678
268 if (sslHandler != null) {
269 ReferenceCountUtil.safeRelease(sslHandler.engine());
270 }
271 PlatformDependent.throwException(cause);
272 }
273 }
274
275 private static final class Selection {
276 final SslContext context;
277 final String hostname;
278
279 Selection(SslContext context, String hostname) {
280 this.context = context;
281 this.hostname = hostname;
282 }
283 }
284 }