View Javadoc
1   /*
2    * Copyright 2017 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.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandlerContext;
20  import io.netty.util.CharsetUtil;
21  import io.netty.util.concurrent.Future;
22  import io.netty.util.concurrent.ScheduledFuture;
23  
24  import java.util.Locale;
25  import java.util.concurrent.TimeUnit;
26  
27  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
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 abstract class AbstractSniHandler<T> extends SslClientHelloHandler<T> {
37  
38      // Package-private for testing purposes.
39      static String extractSniHostname(ByteBuf in) {
40          // See https://tools.ietf.org/html/rfc5246#section-7.4.1.2
41          //
42          // Decode the ssl client hello packet.
43          //
44          // struct {
45          //    ProtocolVersion client_version;
46          //    Random random;
47          //    SessionID session_id;
48          //    CipherSuite cipher_suites<2..2^16-2>;
49          //    CompressionMethod compression_methods<1..2^8-1>;
50          //    select (extensions_present) {
51          //        case false:
52          //            struct {};
53          //        case true:
54          //            Extension extensions<0..2^16-1>;
55          //    };
56          // } ClientHello;
57          //
58  
59          // We have to skip bytes until SessionID (which sum to 34 bytes in this case).
60          int offset = in.readerIndex();
61          int endOffset = in.writerIndex();
62          offset += 34;
63  
64          if (endOffset - offset >= 6) {
65              final int sessionIdLength = in.getUnsignedByte(offset);
66              offset += sessionIdLength + 1;
67  
68              if (endOffset - offset >= 2) {
69                  final int cipherSuitesLength = in.getUnsignedShort(offset);
70                  offset += cipherSuitesLength + 2;
71  
72                  if (endOffset - offset >= 1) {
73                      final int compressionMethodLength = in.getUnsignedByte(offset);
74                      offset += compressionMethodLength + 1;
75  
76                      if (endOffset - offset >= 2) {
77                          final int extensionsLength = in.getUnsignedShort(offset);
78                          offset += 2;
79                          final int extensionsLimit = offset + extensionsLength;
80  
81                          // Extensions should never exceed the record boundary.
82                          if (extensionsLimit <= endOffset) {
83                              while (extensionsLimit - offset >= 4) {
84                                  final int extensionType = in.getUnsignedShort(offset);
85                                  offset += 2;
86  
87                                  final int extensionLength = in.getUnsignedShort(offset);
88                                  offset += 2;
89  
90                                  if (extensionsLimit - offset < extensionLength) {
91                                      break;
92                                  }
93  
94                                  // SNI
95                                  // See https://tools.ietf.org/html/rfc6066#page-6
96                                  if (extensionType == 0) {
97                                      offset += 2;
98                                      if (extensionsLimit - offset < 3) {
99                                          break;
100                                     }
101 
102                                     final int serverNameType = in.getUnsignedByte(offset);
103                                     offset++;
104 
105                                     if (serverNameType == 0) {
106                                         final int serverNameLength = in.getUnsignedShort(offset);
107                                         offset += 2;
108 
109                                         if (extensionsLimit - offset < serverNameLength) {
110                                             break;
111                                         }
112 
113                                         final String hostname = in.toString(
114                                                 offset, serverNameLength, CharsetUtil.US_ASCII);
115                                         return hostname.toLowerCase(Locale.US);
116                                     } else {
117                                         // invalid enum value
118                                         break;
119                                     }
120                                 }
121 
122                                 offset += extensionLength;
123                             }
124                         }
125                     }
126                 }
127             }
128         }
129         return null;
130     }
131 
132     static final long DEFAULT_HANDSHAKE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
133     protected final long handshakeTimeoutMillis;
134     private ScheduledFuture<?> timeoutFuture;
135     private String hostname;
136 
137     /**
138      * @param handshakeTimeoutMillis    the handshake timeout in milliseconds
139      */
140     protected AbstractSniHandler(long handshakeTimeoutMillis) {
141         this(DEFAULT_MAX_CLIENT_HELLO_LENGTH, handshakeTimeoutMillis);
142     }
143 
144     /**
145      * @param maxClientHelloLength     the maximum length of the client hello message.
146      * @param handshakeTimeoutMillis    the handshake timeout in milliseconds
147      */
148     protected AbstractSniHandler(int maxClientHelloLength, long handshakeTimeoutMillis) {
149         super(maxClientHelloLength);
150         this.handshakeTimeoutMillis = checkPositiveOrZero(handshakeTimeoutMillis, "handshakeTimeoutMillis");
151     }
152 
153     public AbstractSniHandler() {
154         this(DEFAULT_MAX_CLIENT_HELLO_LENGTH, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
155     }
156 
157     @Override
158     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
159         if (ctx.channel().isActive()) {
160             checkStartTimeout(ctx);
161         }
162     }
163 
164     @Override
165     public void channelActive(ChannelHandlerContext ctx) throws Exception {
166         ctx.fireChannelActive();
167         checkStartTimeout(ctx);
168     }
169 
170     private void checkStartTimeout(final ChannelHandlerContext ctx) {
171         if (handshakeTimeoutMillis <= 0 || timeoutFuture != null) {
172             return;
173         }
174         timeoutFuture = ctx.executor().schedule(new Runnable() {
175             @Override
176             public void run() {
177                 if (ctx.channel().isActive()) {
178                     SslHandshakeTimeoutException exception = new SslHandshakeTimeoutException(
179                         "handshake timed out after " + handshakeTimeoutMillis + "ms");
180                     ctx.fireUserEventTriggered(new SniCompletionEvent(exception));
181                     ctx.close();
182                 }
183             }
184         }, handshakeTimeoutMillis, TimeUnit.MILLISECONDS);
185     }
186 
187     @Override
188     protected Future<T> lookup(ChannelHandlerContext ctx, ByteBuf clientHello) throws Exception {
189         hostname = clientHello == null ? null : extractSniHostname(clientHello);
190 
191         return lookup(ctx, hostname);
192     }
193 
194     @Override
195     protected void onLookupComplete(ChannelHandlerContext ctx, Future<T> future) throws Exception {
196         if (timeoutFuture != null) {
197             timeoutFuture.cancel(false);
198         }
199         try {
200             onLookupComplete(ctx, hostname, future);
201         } finally {
202             fireSniCompletionEvent(ctx, hostname, future);
203         }
204     }
205 
206     /**
207      * Kicks off a lookup for the given SNI value and returns a {@link Future} which in turn will
208      * notify the {@link #onLookupComplete(ChannelHandlerContext, String, Future)} on completion.
209      *
210      * @see #onLookupComplete(ChannelHandlerContext, String, Future)
211      */
212     protected abstract Future<T> lookup(ChannelHandlerContext ctx, String hostname) throws Exception;
213 
214     /**
215      * Called upon completion of the {@link #lookup(ChannelHandlerContext, String)} {@link Future}.
216      *
217      * @see #lookup(ChannelHandlerContext, String)
218      */
219     protected abstract void onLookupComplete(ChannelHandlerContext ctx,
220                                              String hostname, Future<T> future) throws Exception;
221 
222     private static void fireSniCompletionEvent(ChannelHandlerContext ctx, String hostname, Future<?> future) {
223         Throwable cause = future.cause();
224         if (cause == null) {
225             ctx.fireUserEventTriggered(new SniCompletionEvent(hostname));
226         } else {
227             ctx.fireUserEventTriggered(new SniCompletionEvent(hostname, cause));
228         }
229     }
230 }