1 /*
  2  * Copyright (c) 2023, 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.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 package jdk.httpclient.test.lib.common;
 24 
 25 import java.io.IOException;
 26 import java.net.InetAddress;
 27 import java.util.Objects;
 28 import java.util.Set;
 29 
 30 import javax.net.ssl.SNIHostName;
 31 import javax.net.ssl.SNIMatcher;
 32 import javax.net.ssl.SNIServerName;
 33 import javax.net.ssl.StandardConstants;
 34 
 35 import jdk.internal.net.http.common.Logger;
 36 import jdk.internal.net.http.common.Utils;
 37 
 38 /**
 39  * A (server side) SNI host name matcher. Implementation is based on the expectations set in
 40  * section 3 of RFC-6066.
 41  * A server can be configured with an instance of this class.
 42  * <p>
 43  * The RFC states:
 44  * {@code
 45  * Currently, the only server names supported are DNS hostnames; however, this does not imply
 46  * any dependency of TLS on DNS,
 47  * ....
 48  * TLS MAY treat provided server names as opaque data and pass the names and types to the application.
 49  * }
 50  * <p>
 51  * The implementation in this class doesn't mandate the configured/recognized SNI host name as DNS
 52  * resolvable. However, the {@code ServerNameMatcher} can be configured to treat the SNI host name
 53  * as DNS resolvable by passing {@code true} to the {@code attemptDNSResolution} parameter of
 54  * the {@link #ServerNameMatcher(boolean, String) constructor}
 55  */
 56 public class ServerNameMatcher extends SNIMatcher {
 57 
 58     private final Logger debug;
 59     private final boolean attemptDNSResolution;
 60     private final Set<String> recognizedSNINames;
 61 
 62     /**
 63      * Creates a ServerNameMatcher which recognizes the passed {@code recognizedSNIName}
 64      *
 65      * @param recognizedSNIName The SNI host name
 66      */
 67     public ServerNameMatcher(final String recognizedSNIName) {
 68         this(false, recognizedSNIName);
 69     }
 70 
 71     /**
 72      * Creates a ServerNameMatcher which recognizes the passed SNI host name
 73      * If {@code attemptDNSResolution} is {@code true}, then when
 74      * {@link #matches(SNIServerName) matching} a client requested SNI name against the server
 75      * recognized SNI name, the implementation will, as a last resort do a DNS resolution of the
 76      * client requested SNI name and the server recognized SNI name and compare them to
 77      * try and find a match. If {@code attemptDNSResolution} is false, then no DNS resolution is
 78      * attempted and instead the SNI names are literally compared.
 79      *
 80      * @param attemptDNSResolution If true then a DNS resolution will be attempted during
 81      *                             {@link #matches(SNIServerName) SNI matching}
 82      * @param recognizedSNIName    SNI host name
 83      */
 84     public ServerNameMatcher(final boolean attemptDNSResolution,
 85                              final String recognizedSNIName) {
 86         super(StandardConstants.SNI_HOST_NAME);
 87         Objects.requireNonNull(recognizedSNIName);
 88         this.debug = Utils.getDebugLogger(() -> "SNIMatcher");
 89         this.recognizedSNINames = Set.of(recognizedSNIName);
 90         this.attemptDNSResolution = attemptDNSResolution;
 91     }
 92 
 93     /**
 94      * @param clientRequestedSNI the SNI name requested by the client
 95      *                           {@return true if the {@code clientRequestedSNI} is recognized by
 96      *                           the server. false otherwise}
 97      */
 98     @Override
 99     public boolean matches(final SNIServerName clientRequestedSNI) {
100         Objects.requireNonNull(clientRequestedSNI);
101         if (!SNIHostName.class.isInstance(clientRequestedSNI)) {
102             if (debug.on()) {
103                 debug.log("SNI match (against " + recognizedSNINames + ")" +
104                         " failed - not a SNIHostName: " + clientRequestedSNI);
105             }
106             // we only support SNIHostName type
107             return false;
108         }
109         final String requestedName = ((SNIHostName) clientRequestedSNI).getAsciiName();
110         if (recognizedSNINames.contains(requestedName)) {
111             if (debug.on()) {
112                 debug.log("SNI match (against " + recognizedSNINames + ") passed: "
113                         + clientRequestedSNI);
114             }
115             return true;
116         }
117         if (attemptDNSResolution) {
118             final boolean res = matchesAfterDNSResolution(requestedName);
119             if (debug.on()) {
120                 debug.log("SNI match (against " + recognizedSNINames + ") "
121                         + (res ? "passed" : "failed") + ": " + clientRequestedSNI);
122             }
123             return res;
124         }
125         if (debug.on()) {
126             debug.log("SNI match (against " + recognizedSNINames + ") failed: " + clientRequestedSNI);
127         }
128         return false;
129     }
130 
131     private boolean matchesAfterDNSResolution(final String clientRequestedSNI) {
132         final InetAddress clientRequestedAddr;
133         try {
134             clientRequestedAddr = InetAddress.getByName(clientRequestedSNI);
135         } catch (IOException e) {
136             return false;
137         }
138         for (final String recognizedSNIName : recognizedSNINames) {
139             final InetAddress serverRecognizedAddr;
140             try {
141                 serverRecognizedAddr = InetAddress.getByName(recognizedSNIName);
142             } catch (IOException e) {
143                 // try next
144                 continue;
145             }
146             if (serverRecognizedAddr.equals(clientRequestedAddr)) {
147                 return true;
148             }
149         }
150         return false;
151     }
152 }