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.netty5.handler.ssl;
17
18 import io.netty5.buffer.api.Buffer;
19 import io.netty5.channel.ChannelHandlerContext;
20 import io.netty5.util.concurrent.Future;
21
22 import java.nio.charset.StandardCharsets;
23 import java.util.Locale;
24
25 /**
26 * <p>Enables <a href="https://tools.ietf.org/html/rfc3546#section-3.1">SNI
27 * (Server Name Indication)</a> extension for server side SSL. For clients
28 * support SNI, the server could have multiple host name bound on a single IP.
29 * The client will send host name in the handshake data so server could decide
30 * which certificate to choose for the host name.</p>
31 */
32 public abstract class AbstractSniHandler<T> extends SslClientHelloHandler<T> {
33
34 private static String extractSniHostname(Buffer in) {
35 // See https://tools.ietf.org/html/rfc5246#section-7.4.1.2
36 //
37 // Decode the ssl client hello packet.
38 //
39 // struct {
40 // ProtocolVersion client_version;
41 // Random random;
42 // SessionID session_id;
43 // CipherSuite cipher_suites<2..2^16-2>;
44 // CompressionMethod compression_methods<1..2^8-1>;
45 // select (extensions_present) {
46 // case false:
47 // struct {};
48 // case true:
49 // Extension extensions<0..2^16-1>;
50 // };
51 // } ClientHello;
52 //
53
54 // We have to skip bytes until SessionID (which sum to 34 bytes in this case).
55 int offset = in.readerOffset();
56 int endOffset = in.writerOffset();
57 offset += 34;
58
59 if (endOffset - offset >= 6) {
60 final int sessionIdLength = in.getUnsignedByte(offset);
61 offset += sessionIdLength + 1;
62
63 final int cipherSuitesLength = in.getUnsignedShort(offset);
64 offset += cipherSuitesLength + 2;
65
66 final int compressionMethodLength = in.getUnsignedByte(offset);
67 offset += compressionMethodLength + 1;
68
69 final int extensionsLength = in.getUnsignedShort(offset);
70 offset += 2;
71 final int extensionsLimit = offset + extensionsLength;
72
73 // Extensions should never exceed the record boundary.
74 if (extensionsLimit <= endOffset) {
75 while (extensionsLimit - offset >= 4) {
76 final int extensionType = in.getUnsignedShort(offset);
77 offset += 2;
78
79 final int extensionLength = in.getUnsignedShort(offset);
80 offset += 2;
81
82 if (extensionsLimit - offset < extensionLength) {
83 break;
84 }
85
86 // SNI
87 // See https://tools.ietf.org/html/rfc6066#page-6
88 if (extensionType == 0) {
89 offset += 2;
90 if (extensionsLimit - offset < 3) {
91 break;
92 }
93
94 final int serverNameType = in.getUnsignedByte(offset);
95 offset++;
96
97 if (serverNameType == 0) {
98 final int serverNameLength = in.getUnsignedShort(offset);
99 offset += 2;
100
101 if (extensionsLimit - offset < serverNameLength) {
102 break;
103 }
104
105 String hostname = in.copy(offset, serverNameLength).toString(StandardCharsets.US_ASCII);
106 return hostname.toLowerCase(Locale.US);
107 } else {
108 // invalid enum value
109 break;
110 }
111 }
112
113 offset += extensionLength;
114 }
115 }
116 }
117 return null;
118 }
119
120 private String hostname;
121
122 @Override
123 protected Future<T> lookup(ChannelHandlerContext ctx, Buffer clientHello) throws Exception {
124 hostname = clientHello == null ? null : extractSniHostname(clientHello);
125
126 return lookup(ctx, hostname);
127 }
128
129 @Override
130 protected void onLookupComplete(ChannelHandlerContext ctx, Future<? extends T> future) throws Exception {
131 try {
132 onLookupComplete(ctx, hostname, future);
133 } finally {
134 fireSniCompletionEvent(
135 // If this handler was removed as part of onLookupComplete(...) we should fire the
136 // event from the beginning of the pipeline as otherwise this will fail.
137 ctx.isRemoved() ? ctx.pipeline().firstContext() : ctx, hostname, future);
138 }
139 }
140
141 /**
142 * Kicks off a lookup for the given SNI value and returns a {@link Future} which in turn will
143 * notify the {@link #onLookupComplete(ChannelHandlerContext, String, Future)} on completion.
144 *
145 * @see #onLookupComplete(ChannelHandlerContext, String, Future)
146 */
147 protected abstract Future<T> lookup(ChannelHandlerContext ctx, String hostname) throws Exception;
148
149 /**
150 * Called upon completion of the {@link #lookup(ChannelHandlerContext, String)} {@link Future}.
151 *
152 * @see #lookup(ChannelHandlerContext, String)
153 */
154 protected abstract void onLookupComplete(ChannelHandlerContext ctx,
155 String hostname, Future<? extends T> future) throws Exception;
156
157 private static void fireSniCompletionEvent(ChannelHandlerContext ctx, String hostname, Future<?> future) {
158 Throwable cause = future.cause();
159 if (cause == null) {
160 ctx.fireChannelInboundEvent(new SniCompletionEvent(hostname));
161 } else {
162 ctx.fireChannelInboundEvent(new SniCompletionEvent(hostname, cause));
163 }
164 }
165 }