1 /* |
|
2 * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. |
|
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
4 * |
|
5 * This code is free software; you can redistribute it and/or modify it |
|
6 * under the terms of the GNU General Public License version 2 only, as |
|
7 * published by the Free Software Foundation. Oracle designates this |
|
8 * particular file as subject to the "Classpath" exception as provided |
|
9 * by Oracle in the LICENSE file that accompanied this code. |
|
10 * |
|
11 * This code is distributed in the hope that it will be useful, but WITHOUT |
|
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
14 * version 2 for more details (a copy is included in the LICENSE file that |
|
15 * accompanied this code). |
|
16 * |
|
17 * You should have received a copy of the GNU General Public License version |
|
18 * 2 along with this work; if not, write to the Free Software Foundation, |
|
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
20 * |
|
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
22 * or visit www.oracle.com if you need additional information or have any |
|
23 * questions. |
|
24 */ |
|
25 |
|
26 import java.nio.ByteBuffer; |
|
27 import java.nio.BufferUnderflowException; |
|
28 import java.io.IOException; |
|
29 import javax.net.ssl.*; |
|
30 import java.util.*; |
|
31 |
|
32 import sun.misc.HexDumpEncoder; |
|
33 |
|
34 /** |
|
35 * Instances of this class acts as an explorer of the network data of an |
|
36 * SSL/TLS connection. |
|
37 */ |
|
38 public final class SSLExplorer { |
|
39 |
|
40 // Private constructor prevents construction outside this class. |
|
41 private SSLExplorer() { |
|
42 } |
|
43 |
|
44 /** |
|
45 * The header size of TLS/SSL records. |
|
46 * <P> |
|
47 * The value of this constant is {@value}. |
|
48 */ |
|
49 public final static int RECORD_HEADER_SIZE = 0x05; |
|
50 |
|
51 /** |
|
52 * Returns the required number of bytes in the {@code source} |
|
53 * {@link ByteBuffer} necessary to explore SSL/TLS connection. |
|
54 * <P> |
|
55 * This method tries to parse as few bytes as possible from |
|
56 * {@code source} byte buffer to get the length of an |
|
57 * SSL/TLS record. |
|
58 * <P> |
|
59 * This method accesses the {@code source} parameter in read-only |
|
60 * mode, and does not update the buffer's properties such as capacity, |
|
61 * limit, position, and mark values. |
|
62 * |
|
63 * @param source |
|
64 * a {@link ByteBuffer} containing |
|
65 * inbound or outbound network data for an SSL/TLS connection. |
|
66 * @throws BufferUnderflowException if less than {@code RECORD_HEADER_SIZE} |
|
67 * bytes remaining in {@code source} |
|
68 * @return the required size in byte to explore an SSL/TLS connection |
|
69 */ |
|
70 public final static int getRequiredSize(ByteBuffer source) { |
|
71 |
|
72 ByteBuffer input = source.duplicate(); |
|
73 |
|
74 // Do we have a complete header? |
|
75 if (input.remaining() < RECORD_HEADER_SIZE) { |
|
76 throw new BufferUnderflowException(); |
|
77 } |
|
78 |
|
79 // Is it a handshake message? |
|
80 byte firstByte = input.get(); |
|
81 byte secondByte = input.get(); |
|
82 byte thirdByte = input.get(); |
|
83 if ((firstByte & 0x80) != 0 && thirdByte == 0x01) { |
|
84 // looks like a V2ClientHello |
|
85 // return (((firstByte & 0x7F) << 8) | (secondByte & 0xFF)) + 2; |
|
86 return RECORD_HEADER_SIZE; // Only need the header fields |
|
87 } else { |
|
88 return (((input.get() & 0xFF) << 8) | (input.get() & 0xFF)) + 5; |
|
89 } |
|
90 } |
|
91 |
|
92 /** |
|
93 * Returns the required number of bytes in the {@code source} byte array |
|
94 * necessary to explore SSL/TLS connection. |
|
95 * <P> |
|
96 * This method tries to parse as few bytes as possible from |
|
97 * {@code source} byte array to get the length of an |
|
98 * SSL/TLS record. |
|
99 * |
|
100 * @param source |
|
101 * a byte array containing inbound or outbound network data for |
|
102 * an SSL/TLS connection. |
|
103 * @param offset |
|
104 * the start offset in array {@code source} at which the |
|
105 * network data is read from. |
|
106 * @param length |
|
107 * the maximum number of bytes to read. |
|
108 * |
|
109 * @throws BufferUnderflowException if less than {@code RECORD_HEADER_SIZE} |
|
110 * bytes remaining in {@code source} |
|
111 * @return the required size in byte to explore an SSL/TLS connection |
|
112 */ |
|
113 public final static int getRequiredSize(byte[] source, |
|
114 int offset, int length) throws IOException { |
|
115 |
|
116 ByteBuffer byteBuffer = |
|
117 ByteBuffer.wrap(source, offset, length).asReadOnlyBuffer(); |
|
118 return getRequiredSize(byteBuffer); |
|
119 } |
|
120 |
|
121 /** |
|
122 * Launch and explore the security capabilities from byte buffer. |
|
123 * <P> |
|
124 * This method tries to parse as few records as possible from |
|
125 * {@code source} byte buffer to get the {@link SSLCapabilities} |
|
126 * of an SSL/TLS connection. |
|
127 * <P> |
|
128 * Please NOTE that this method must be called before any handshaking |
|
129 * occurs. The behavior of this method is not defined in this release |
|
130 * if the handshake has begun, or has completed. |
|
131 * <P> |
|
132 * This method accesses the {@code source} parameter in read-only |
|
133 * mode, and does not update the buffer's properties such as capacity, |
|
134 * limit, position, and mark values. |
|
135 * |
|
136 * @param source |
|
137 * a {@link ByteBuffer} containing |
|
138 * inbound or outbound network data for an SSL/TLS connection. |
|
139 * |
|
140 * @throws IOException on network data error |
|
141 * @throws BufferUnderflowException if not enough source bytes available |
|
142 * to make a complete exploration. |
|
143 * |
|
144 * @return the explored {@link SSLCapabilities} of the SSL/TLS |
|
145 * connection |
|
146 */ |
|
147 public final static SSLCapabilities explore(ByteBuffer source) |
|
148 throws IOException { |
|
149 |
|
150 ByteBuffer input = source.duplicate(); |
|
151 |
|
152 // Do we have a complete header? |
|
153 if (input.remaining() < RECORD_HEADER_SIZE) { |
|
154 throw new BufferUnderflowException(); |
|
155 } |
|
156 |
|
157 // Is it a handshake message? |
|
158 byte firstByte = input.get(); |
|
159 byte secondByte = input.get(); |
|
160 byte thirdByte = input.get(); |
|
161 if ((firstByte & 0x80) != 0 && thirdByte == 0x01) { |
|
162 // looks like a V2ClientHello |
|
163 return exploreV2HelloRecord(input, |
|
164 firstByte, secondByte, thirdByte); |
|
165 } else if (firstByte == 22) { // 22: handshake record |
|
166 return exploreTLSRecord(input, |
|
167 firstByte, secondByte, thirdByte); |
|
168 } else { |
|
169 throw new SSLException("Not handshake record"); |
|
170 } |
|
171 } |
|
172 |
|
173 /** |
|
174 * Launch and explore the security capabilities from byte array. |
|
175 * <P> |
|
176 * Please NOTE that this method must be called before any handshaking |
|
177 * occurs. The behavior of this method is not defined in this release |
|
178 * if the handshake has begun, or has completed. Once handshake has |
|
179 * begun, or has completed, the security capabilities can not and |
|
180 * should not be launched with this method. |
|
181 * |
|
182 * @param source |
|
183 * a byte array containing inbound or outbound network data for |
|
184 * an SSL/TLS connection. |
|
185 * @param offset |
|
186 * the start offset in array {@code source} at which the |
|
187 * network data is read from. |
|
188 * @param length |
|
189 * the maximum number of bytes to read. |
|
190 * |
|
191 * @throws IOException on network data error |
|
192 * @throws BufferUnderflowException if not enough source bytes available |
|
193 * to make a complete exploration. |
|
194 * @return the explored {@link SSLCapabilities} of the SSL/TLS |
|
195 * connection |
|
196 * |
|
197 * @see #explore(ByteBuffer) |
|
198 */ |
|
199 public final static SSLCapabilities explore(byte[] source, |
|
200 int offset, int length) throws IOException { |
|
201 ByteBuffer byteBuffer = |
|
202 ByteBuffer.wrap(source, offset, length).asReadOnlyBuffer(); |
|
203 return explore(byteBuffer); |
|
204 } |
|
205 |
|
206 /* |
|
207 * uint8 V2CipherSpec[3]; |
|
208 * struct { |
|
209 * uint16 msg_length; // The highest bit MUST be 1; |
|
210 * // the remaining bits contain the length |
|
211 * // of the following data in bytes. |
|
212 * uint8 msg_type; // MUST be 1 |
|
213 * Version version; |
|
214 * uint16 cipher_spec_length; // It cannot be zero and MUST be a |
|
215 * // multiple of the V2CipherSpec length. |
|
216 * uint16 session_id_length; // This field MUST be empty. |
|
217 * uint16 challenge_length; // SHOULD use a 32-byte challenge |
|
218 * V2CipherSpec cipher_specs[V2ClientHello.cipher_spec_length]; |
|
219 * opaque session_id[V2ClientHello.session_id_length]; |
|
220 * opaque challenge[V2ClientHello.challenge_length; |
|
221 * } V2ClientHello; |
|
222 */ |
|
223 private static SSLCapabilities exploreV2HelloRecord( |
|
224 ByteBuffer input, byte firstByte, byte secondByte, |
|
225 byte thirdByte) throws IOException { |
|
226 |
|
227 // We only need the header. We have already had enough source bytes. |
|
228 // int recordLength = (firstByte & 0x7F) << 8) | (secondByte & 0xFF); |
|
229 try { |
|
230 // Is it a V2ClientHello? |
|
231 if (thirdByte != 0x01) { |
|
232 throw new SSLException( |
|
233 "Unsupported or Unrecognized SSL record"); |
|
234 } |
|
235 |
|
236 // What's the hello version? |
|
237 byte helloVersionMajor = input.get(); |
|
238 byte helloVersionMinor = input.get(); |
|
239 |
|
240 // 0x00: major version of SSLv20 |
|
241 // 0x02: minor version of SSLv20 |
|
242 // |
|
243 // SNIServerName is an extension, SSLv20 doesn't support extension. |
|
244 return new SSLCapabilitiesImpl((byte)0x00, (byte)0x02, |
|
245 helloVersionMajor, helloVersionMinor, |
|
246 Collections.<SNIServerName>emptyList()); |
|
247 } catch (BufferUnderflowException bufe) { |
|
248 throw new SSLProtocolException( |
|
249 "Invalid handshake record"); |
|
250 } |
|
251 } |
|
252 |
|
253 /* |
|
254 * struct { |
|
255 * uint8 major; |
|
256 * uint8 minor; |
|
257 * } ProtocolVersion; |
|
258 * |
|
259 * enum { |
|
260 * change_cipher_spec(20), alert(21), handshake(22), |
|
261 * application_data(23), (255) |
|
262 * } ContentType; |
|
263 * |
|
264 * struct { |
|
265 * ContentType type; |
|
266 * ProtocolVersion version; |
|
267 * uint16 length; |
|
268 * opaque fragment[TLSPlaintext.length]; |
|
269 * } TLSPlaintext; |
|
270 */ |
|
271 private static SSLCapabilities exploreTLSRecord( |
|
272 ByteBuffer input, byte firstByte, byte secondByte, |
|
273 byte thirdByte) throws IOException { |
|
274 |
|
275 // Is it a handshake message? |
|
276 if (firstByte != 22) { // 22: handshake record |
|
277 throw new SSLException("Not handshake record"); |
|
278 } |
|
279 |
|
280 // We need the record version to construct SSLCapabilities. |
|
281 byte recordMajorVersion = secondByte; |
|
282 byte recordMinorVersion = thirdByte; |
|
283 |
|
284 // Is there enough data for a full record? |
|
285 int recordLength = getInt16(input); |
|
286 if (recordLength > input.remaining()) { |
|
287 throw new BufferUnderflowException(); |
|
288 } |
|
289 |
|
290 // We have already had enough source bytes. |
|
291 try { |
|
292 return exploreHandshake(input, |
|
293 recordMajorVersion, recordMinorVersion, recordLength); |
|
294 } catch (BufferUnderflowException bufe) { |
|
295 throw new SSLProtocolException( |
|
296 "Invalid handshake record"); |
|
297 } |
|
298 } |
|
299 |
|
300 /* |
|
301 * enum { |
|
302 * hello_request(0), client_hello(1), server_hello(2), |
|
303 * certificate(11), server_key_exchange (12), |
|
304 * certificate_request(13), server_hello_done(14), |
|
305 * certificate_verify(15), client_key_exchange(16), |
|
306 * finished(20) |
|
307 * (255) |
|
308 * } HandshakeType; |
|
309 * |
|
310 * struct { |
|
311 * HandshakeType msg_type; |
|
312 * uint24 length; |
|
313 * select (HandshakeType) { |
|
314 * case hello_request: HelloRequest; |
|
315 * case client_hello: ClientHello; |
|
316 * case server_hello: ServerHello; |
|
317 * case certificate: Certificate; |
|
318 * case server_key_exchange: ServerKeyExchange; |
|
319 * case certificate_request: CertificateRequest; |
|
320 * case server_hello_done: ServerHelloDone; |
|
321 * case certificate_verify: CertificateVerify; |
|
322 * case client_key_exchange: ClientKeyExchange; |
|
323 * case finished: Finished; |
|
324 * } body; |
|
325 * } Handshake; |
|
326 */ |
|
327 private static SSLCapabilities exploreHandshake( |
|
328 ByteBuffer input, byte recordMajorVersion, |
|
329 byte recordMinorVersion, int recordLength) throws IOException { |
|
330 |
|
331 // What is the handshake type? |
|
332 byte handshakeType = input.get(); |
|
333 if (handshakeType != 0x01) { // 0x01: client_hello message |
|
334 throw new IllegalStateException("Not initial handshaking"); |
|
335 } |
|
336 |
|
337 // What is the handshake body length? |
|
338 int handshakeLength = getInt24(input); |
|
339 |
|
340 // Theoretically, a single handshake message might span multiple |
|
341 // records, but in practice this does not occur. |
|
342 if (handshakeLength > (recordLength - 4)) { // 4: handshake header size |
|
343 throw new SSLException("Handshake message spans multiple records"); |
|
344 } |
|
345 |
|
346 input = input.duplicate(); |
|
347 input.limit(handshakeLength + input.position()); |
|
348 return exploreClientHello(input, |
|
349 recordMajorVersion, recordMinorVersion); |
|
350 } |
|
351 |
|
352 /* |
|
353 * struct { |
|
354 * uint32 gmt_unix_time; |
|
355 * opaque random_bytes[28]; |
|
356 * } Random; |
|
357 * |
|
358 * opaque SessionID<0..32>; |
|
359 * |
|
360 * uint8 CipherSuite[2]; |
|
361 * |
|
362 * enum { null(0), (255) } CompressionMethod; |
|
363 * |
|
364 * struct { |
|
365 * ProtocolVersion client_version; |
|
366 * Random random; |
|
367 * SessionID session_id; |
|
368 * CipherSuite cipher_suites<2..2^16-2>; |
|
369 * CompressionMethod compression_methods<1..2^8-1>; |
|
370 * select (extensions_present) { |
|
371 * case false: |
|
372 * struct {}; |
|
373 * case true: |
|
374 * Extension extensions<0..2^16-1>; |
|
375 * }; |
|
376 * } ClientHello; |
|
377 */ |
|
378 private static SSLCapabilities exploreClientHello( |
|
379 ByteBuffer input, |
|
380 byte recordMajorVersion, |
|
381 byte recordMinorVersion) throws IOException { |
|
382 |
|
383 List<SNIServerName> snList = Collections.<SNIServerName>emptyList(); |
|
384 |
|
385 // client version |
|
386 byte helloMajorVersion = input.get(); |
|
387 byte helloMinorVersion = input.get(); |
|
388 |
|
389 // ignore random |
|
390 int position = input.position(); |
|
391 input.position(position + 32); // 32: the length of Random |
|
392 |
|
393 // ignore session id |
|
394 ignoreByteVector8(input); |
|
395 |
|
396 // ignore cipher_suites |
|
397 ignoreByteVector16(input); |
|
398 |
|
399 // ignore compression methods |
|
400 ignoreByteVector8(input); |
|
401 |
|
402 if (input.remaining() > 0) { |
|
403 snList = exploreExtensions(input); |
|
404 } |
|
405 |
|
406 return new SSLCapabilitiesImpl( |
|
407 recordMajorVersion, recordMinorVersion, |
|
408 helloMajorVersion, helloMinorVersion, snList); |
|
409 } |
|
410 |
|
411 /* |
|
412 * struct { |
|
413 * ExtensionType extension_type; |
|
414 * opaque extension_data<0..2^16-1>; |
|
415 * } Extension; |
|
416 * |
|
417 * enum { |
|
418 * server_name(0), max_fragment_length(1), |
|
419 * client_certificate_url(2), trusted_ca_keys(3), |
|
420 * truncated_hmac(4), status_request(5), (65535) |
|
421 * } ExtensionType; |
|
422 */ |
|
423 private static List<SNIServerName> exploreExtensions(ByteBuffer input) |
|
424 throws IOException { |
|
425 |
|
426 int length = getInt16(input); // length of extensions |
|
427 while (length > 0) { |
|
428 int extType = getInt16(input); // extenson type |
|
429 int extLen = getInt16(input); // length of extension data |
|
430 |
|
431 if (extType == 0x00) { // 0x00: type of server name indication |
|
432 return exploreSNIExt(input, extLen); |
|
433 } else { // ignore other extensions |
|
434 ignoreByteVector(input, extLen); |
|
435 } |
|
436 |
|
437 length -= extLen + 4; |
|
438 } |
|
439 |
|
440 return Collections.<SNIServerName>emptyList(); |
|
441 } |
|
442 |
|
443 /* |
|
444 * struct { |
|
445 * NameType name_type; |
|
446 * select (name_type) { |
|
447 * case host_name: HostName; |
|
448 * } name; |
|
449 * } ServerName; |
|
450 * |
|
451 * enum { |
|
452 * host_name(0), (255) |
|
453 * } NameType; |
|
454 * |
|
455 * opaque HostName<1..2^16-1>; |
|
456 * |
|
457 * struct { |
|
458 * ServerName server_name_list<1..2^16-1> |
|
459 * } ServerNameList; |
|
460 */ |
|
461 private static List<SNIServerName> exploreSNIExt(ByteBuffer input, |
|
462 int extLen) throws IOException { |
|
463 |
|
464 Map<Integer, SNIServerName> sniMap = new LinkedHashMap<>(); |
|
465 |
|
466 int remains = extLen; |
|
467 if (extLen >= 2) { // "server_name" extension in ClientHello |
|
468 int listLen = getInt16(input); // length of server_name_list |
|
469 if (listLen == 0 || listLen + 2 != extLen) { |
|
470 throw new SSLProtocolException( |
|
471 "Invalid server name indication extension"); |
|
472 } |
|
473 |
|
474 remains -= 2; // 0x02: the length field of server_name_list |
|
475 while (remains > 0) { |
|
476 int code = getInt8(input); // name_type |
|
477 int snLen = getInt16(input); // length field of server name |
|
478 if (snLen > remains) { |
|
479 throw new SSLProtocolException( |
|
480 "Not enough data to fill declared vector size"); |
|
481 } |
|
482 byte[] encoded = new byte[snLen]; |
|
483 input.get(encoded); |
|
484 |
|
485 SNIServerName serverName; |
|
486 switch (code) { |
|
487 case StandardConstants.SNI_HOST_NAME: |
|
488 if (encoded.length == 0) { |
|
489 throw new SSLProtocolException( |
|
490 "Empty HostName in server name indication"); |
|
491 } |
|
492 serverName = new SNIHostName(encoded); |
|
493 break; |
|
494 default: |
|
495 serverName = new UnknownServerName(code, encoded); |
|
496 } |
|
497 // check for duplicated server name type |
|
498 if (sniMap.put(serverName.getType(), serverName) != null) { |
|
499 throw new SSLProtocolException( |
|
500 "Duplicated server name of type " + |
|
501 serverName.getType()); |
|
502 } |
|
503 |
|
504 remains -= encoded.length + 3; // NameType: 1 byte |
|
505 // HostName length: 2 bytes |
|
506 } |
|
507 } else if (extLen == 0) { // "server_name" extension in ServerHello |
|
508 throw new SSLProtocolException( |
|
509 "Not server name indication extension in client"); |
|
510 } |
|
511 |
|
512 if (remains != 0) { |
|
513 throw new SSLProtocolException( |
|
514 "Invalid server name indication extension"); |
|
515 } |
|
516 |
|
517 return Collections.<SNIServerName>unmodifiableList( |
|
518 new ArrayList<>(sniMap.values())); |
|
519 } |
|
520 |
|
521 private static int getInt8(ByteBuffer input) { |
|
522 return input.get(); |
|
523 } |
|
524 |
|
525 private static int getInt16(ByteBuffer input) { |
|
526 return ((input.get() & 0xFF) << 8) | (input.get() & 0xFF); |
|
527 } |
|
528 |
|
529 private static int getInt24(ByteBuffer input) { |
|
530 return ((input.get() & 0xFF) << 16) | ((input.get() & 0xFF) << 8) | |
|
531 (input.get() & 0xFF); |
|
532 } |
|
533 |
|
534 private static void ignoreByteVector8(ByteBuffer input) { |
|
535 ignoreByteVector(input, getInt8(input)); |
|
536 } |
|
537 |
|
538 private static void ignoreByteVector16(ByteBuffer input) { |
|
539 ignoreByteVector(input, getInt16(input)); |
|
540 } |
|
541 |
|
542 private static void ignoreByteVector24(ByteBuffer input) { |
|
543 ignoreByteVector(input, getInt24(input)); |
|
544 } |
|
545 |
|
546 private static void ignoreByteVector(ByteBuffer input, int length) { |
|
547 if (length != 0) { |
|
548 int position = input.position(); |
|
549 input.position(position + length); |
|
550 } |
|
551 } |
|
552 |
|
553 private static class UnknownServerName extends SNIServerName { |
|
554 UnknownServerName(int code, byte[] encoded) { |
|
555 super(code, encoded); |
|
556 } |
|
557 } |
|
558 |
|
559 private static final class SSLCapabilitiesImpl extends SSLCapabilities { |
|
560 private final static Map<Integer, String> versionMap = new HashMap<>(5); |
|
561 |
|
562 private final String recordVersion; |
|
563 private final String helloVersion; |
|
564 List<SNIServerName> sniNames; |
|
565 |
|
566 static { |
|
567 versionMap.put(0x0002, "SSLv2Hello"); |
|
568 versionMap.put(0x0300, "SSLv3"); |
|
569 versionMap.put(0x0301, "TLSv1"); |
|
570 versionMap.put(0x0302, "TLSv1.1"); |
|
571 versionMap.put(0x0303, "TLSv1.2"); |
|
572 } |
|
573 |
|
574 SSLCapabilitiesImpl(byte recordMajorVersion, byte recordMinorVersion, |
|
575 byte helloMajorVersion, byte helloMinorVersion, |
|
576 List<SNIServerName> sniNames) { |
|
577 |
|
578 int version = (recordMajorVersion << 8) | recordMinorVersion; |
|
579 this.recordVersion = versionMap.get(version) != null ? |
|
580 versionMap.get(version) : |
|
581 unknownVersion(recordMajorVersion, recordMinorVersion); |
|
582 |
|
583 version = (helloMajorVersion << 8) | helloMinorVersion; |
|
584 this.helloVersion = versionMap.get(version) != null ? |
|
585 versionMap.get(version) : |
|
586 unknownVersion(helloMajorVersion, helloMinorVersion); |
|
587 |
|
588 this.sniNames = sniNames; |
|
589 } |
|
590 |
|
591 @Override |
|
592 public String getRecordVersion() { |
|
593 return recordVersion; |
|
594 } |
|
595 |
|
596 @Override |
|
597 public String getHelloVersion() { |
|
598 return helloVersion; |
|
599 } |
|
600 |
|
601 @Override |
|
602 public List<SNIServerName> getServerNames() { |
|
603 if (!sniNames.isEmpty()) { |
|
604 return Collections.<SNIServerName>unmodifiableList(sniNames); |
|
605 } |
|
606 |
|
607 return sniNames; |
|
608 } |
|
609 |
|
610 private static String unknownVersion(byte major, byte minor) { |
|
611 return "Unknown-" + ((int)major) + "." + ((int)minor); |
|
612 } |
|
613 } |
|
614 } |
|
615 |
|