1/*
2 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/AbstractVerifier.java $
3 * $Revision: 653041 $
4 * $Date: 2008-05-03 03:39:28 -0700 (Sat, 03 May 2008) $
5 *
6 * ====================================================================
7 * Licensed to the Apache Software Foundation (ASF) under one
8 * or more contributor license agreements.  See the NOTICE file
9 * distributed with this work for additional information
10 * regarding copyright ownership.  The ASF licenses this file
11 * to you under the Apache License, Version 2.0 (the
12 * "License"); you may not use this file except in compliance
13 * with the License.  You may obtain a copy of the License at
14 *
15 *   http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing,
18 * software distributed under the License is distributed on an
19 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20 * KIND, either express or implied.  See the License for the
21 * specific language governing permissions and limitations
22 * under the License.
23 * ====================================================================
24 *
25 * This software consists of voluntary contributions made by many
26 * individuals on behalf of the Apache Software Foundation.  For more
27 * information on the Apache Software Foundation, please see
28 * <http://www.apache.org/>.
29 *
30 */
31
32package org.apache.http.conn.ssl;
33
34import org.apache.http.conn.util.InetAddressUtils;
35
36import java.io.IOException;
37import java.io.InputStream;
38import java.security.cert.Certificate;
39import java.security.cert.CertificateParsingException;
40import java.security.cert.X509Certificate;
41import java.util.Arrays;
42import java.util.Collection;
43import java.util.Iterator;
44import java.util.LinkedList;
45import java.util.List;
46import java.util.Locale;
47import java.util.logging.Logger;
48import java.util.logging.Level;
49
50import javax.net.ssl.DistinguishedNameParser;
51import javax.net.ssl.SSLException;
52import javax.net.ssl.SSLSession;
53import javax.net.ssl.SSLSocket;
54
55/**
56 * Abstract base class for all standard {@link X509HostnameVerifier}
57 * implementations.
58 *
59 * @author Julius Davies
60 *
61 * @deprecated Please use {@link java.net.URL#openConnection} instead.
62 *     Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a>
63 *     for further details.
64 */
65@Deprecated
66public abstract class AbstractVerifier implements X509HostnameVerifier {
67
68    /**
69     * This contains a list of 2nd-level domains that aren't allowed to
70     * have wildcards when combined with country-codes.
71     * For example: [*.co.uk].
72     * <p/>
73     * The [*.co.uk] problem is an interesting one.  Should we just hope
74     * that CA's would never foolishly allow such a certificate to happen?
75     * Looks like we're the only implementation guarding against this.
76     * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
77     */
78    private final static String[] BAD_COUNTRY_2LDS =
79          { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
80            "lg", "ne", "net", "or", "org" };
81
82    static {
83        // Just in case developer forgot to manually sort the array.  :-)
84        Arrays.sort(BAD_COUNTRY_2LDS);
85    }
86
87    public AbstractVerifier() {
88        super();
89    }
90
91    public final void verify(String host, SSLSocket ssl)
92          throws IOException {
93        if(host == null) {
94            throw new NullPointerException("host to verify is null");
95        }
96
97        SSLSession session = ssl.getSession();
98        Certificate[] certs = session.getPeerCertificates();
99        X509Certificate x509 = (X509Certificate) certs[0];
100        verify(host, x509);
101    }
102
103    public final boolean verify(String host, SSLSession session) {
104        try {
105            Certificate[] certs = session.getPeerCertificates();
106            X509Certificate x509 = (X509Certificate) certs[0];
107            verify(host, x509);
108            return true;
109        }
110        catch(SSLException e) {
111            return false;
112        }
113    }
114
115    public final void verify(String host, X509Certificate cert)
116          throws SSLException {
117        String[] cns = getCNs(cert);
118        String[] subjectAlts = getDNSSubjectAlts(cert);
119        verify(host, cns, subjectAlts);
120    }
121
122    public final void verify(final String host, final String[] cns,
123                             final String[] subjectAlts,
124                             final boolean strictWithSubDomains)
125          throws SSLException {
126
127        // Build the list of names we're going to check.  Our DEFAULT and
128        // STRICT implementations of the HostnameVerifier only use the
129        // first CN provided.  All other CNs are ignored.
130        // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
131        LinkedList<String> names = new LinkedList<String>();
132        if(cns != null && cns.length > 0 && cns[0] != null) {
133            names.add(cns[0]);
134        }
135        if(subjectAlts != null) {
136            for (String subjectAlt : subjectAlts) {
137                if (subjectAlt != null) {
138                    names.add(subjectAlt);
139                }
140            }
141        }
142
143        if(names.isEmpty()) {
144            String msg = "Certificate for <" + host + "> doesn't contain CN or DNS subjectAlt";
145            throw new SSLException(msg);
146        }
147
148        // StringBuffer for building the error message.
149        StringBuffer buf = new StringBuffer();
150
151        // We're can be case-insensitive when comparing the host we used to
152        // establish the socket to the hostname in the certificate.
153        String hostName = host.trim().toLowerCase(Locale.ENGLISH);
154        boolean match = false;
155        for(Iterator<String> it = names.iterator(); it.hasNext();) {
156            // Don't trim the CN, though!
157            String cn = it.next();
158            cn = cn.toLowerCase(Locale.ENGLISH);
159            // Store CN in StringBuffer in case we need to report an error.
160            buf.append(" <");
161            buf.append(cn);
162            buf.append('>');
163            if(it.hasNext()) {
164                buf.append(" OR");
165            }
166
167            // The CN better have at least two dots if it wants wildcard
168            // action.  It also can't be [*.co.uk] or [*.co.jp] or
169            // [*.org.uk], etc...
170            boolean doWildcard = cn.startsWith("*.") &&
171                                 cn.indexOf('.', 2) != -1 &&
172                                 acceptableCountryWildcard(cn) &&
173                                 !InetAddressUtils.isIPv4Address(host);
174
175            if(doWildcard) {
176                match = hostName.endsWith(cn.substring(1));
177                if(match && strictWithSubDomains) {
178                    // If we're in strict mode, then [*.foo.com] is not
179                    // allowed to match [a.b.foo.com]
180                    match = countDots(hostName) == countDots(cn);
181                }
182            } else {
183                match = hostName.equals(cn);
184            }
185            if(match) {
186                break;
187            }
188        }
189        if(!match) {
190            throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf);
191        }
192    }
193
194    public static boolean acceptableCountryWildcard(String cn) {
195        int cnLen = cn.length();
196        if(cnLen >= 7 && cnLen <= 9) {
197            // Look for the '.' in the 3rd-last position:
198            if(cn.charAt(cnLen - 3) == '.') {
199                // Trim off the [*.] and the [.XX].
200                String s = cn.substring(2, cnLen - 3);
201                // And test against the sorted array of bad 2lds:
202                int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s);
203                return x < 0;
204            }
205        }
206        return true;
207    }
208
209    public static String[] getCNs(X509Certificate cert) {
210        DistinguishedNameParser dnParser =
211                new DistinguishedNameParser(cert.getSubjectX500Principal());
212        List<String> cnList = dnParser.getAllMostSpecificFirst("cn");
213
214        if(!cnList.isEmpty()) {
215            String[] cns = new String[cnList.size()];
216            cnList.toArray(cns);
217            return cns;
218        } else {
219            return null;
220        }
221    }
222
223
224    /**
225     * Extracts the array of SubjectAlt DNS names from an X509Certificate.
226     * Returns null if there aren't any.
227     * <p/>
228     * Note:  Java doesn't appear able to extract international characters
229     * from the SubjectAlts.  It can only extract international characters
230     * from the CN field.
231     * <p/>
232     * (Or maybe the version of OpenSSL I'm using to test isn't storing the
233     * international characters correctly in the SubjectAlts?).
234     *
235     * @param cert X509Certificate
236     * @return Array of SubjectALT DNS names stored in the certificate.
237     */
238    public static String[] getDNSSubjectAlts(X509Certificate cert) {
239        LinkedList<String> subjectAltList = new LinkedList<String>();
240        Collection<List<?>> c = null;
241        try {
242            c = cert.getSubjectAlternativeNames();
243        }
244        catch(CertificateParsingException cpe) {
245            Logger.getLogger(AbstractVerifier.class.getName())
246                    .log(Level.FINE, "Error parsing certificate.", cpe);
247        }
248        if(c != null) {
249            for (List<?> aC : c) {
250                List<?> list = aC;
251                int type = ((Integer) list.get(0)).intValue();
252                // If type is 2, then we've got a dNSName
253                if (type == 2) {
254                    String s = (String) list.get(1);
255                    subjectAltList.add(s);
256                }
257            }
258        }
259        if(!subjectAltList.isEmpty()) {
260            String[] subjectAlts = new String[subjectAltList.size()];
261            subjectAltList.toArray(subjectAlts);
262            return subjectAlts;
263        } else {
264            return null;
265        }
266    }
267
268    /**
269     * Counts the number of dots "." in a string.
270     * @param s  string to count dots from
271     * @return  number of dots
272     */
273    public static int countDots(final String s) {
274        int count = 0;
275        for(int i = 0; i < s.length(); i++) {
276            if(s.charAt(i) == '.') {
277                count++;
278            }
279        }
280        return count;
281    }
282
283}
284